前言
本篇將完成DAO層的設(shè)計(jì)與開發(fā)粘优,包括:
- 數(shù)據(jù)庫抒痒、DAO實(shí)體與接口設(shè)計(jì)與編碼
- 基于MyBatis實(shí)現(xiàn)DAO編程
- MyBatis與Spring整合
- DAO層單元測試
一匠题、數(shù)據(jù)庫設(shè)計(jì)與編碼
打開Eclipse乳蓄,在src\main下建立一個(gè)文件夾sql蚊逢,用于存放建表語句缩功,新建一個(gè)SQL文件schema.sql晴及,先創(chuàng)建一個(gè)秒殺商品的庫存表
-- 數(shù)據(jù)庫初始化腳本
-- 創(chuàng)建數(shù)據(jù)庫
CREATE DATABASE seckill;
-- 使用數(shù)據(jù)庫
USE seckill;
--創(chuàng)建秒殺庫存表
CREATE TABLE seckill(
`seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品庫存id',
`name` varchar(120) NOT NULL COMMENT '商品名稱',
`number` int NOT NULL COMMENT '庫存數(shù)量',
`start_time` timestamp NOT NULL COMMENT '秒殺開始時(shí)間',
`end_time` timestamp NOT NULL COMMENT '秒殺結(jié)束時(shí)間',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (seckill_id),
key idx_start_time(start_time),
key idx_end_time(end_time),
key idx_create_time(create_time)
)ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒殺庫存表';
主鍵為seckill_id,再單獨(dú)對start_time嫡锌、end_time虑稼、create_time三列單獨(dú)建立索引琳钉,最后顯式的設(shè)置MySQL引擎為InnoDB、自增主鍵初始值設(shè)置為1000蛛倦、編碼方式為utf8歌懒,并添加注釋
** MySQL默認(rèn)的有很多引擎,只有InnoDB支持事務(wù) **
可以插入幾條數(shù)據(jù)
-- 初始化數(shù)據(jù)
INSERT INTO
seckill(name,number,start_time,end_time)
VALUES
('1000秒殺iPhone6S',100,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('500秒殺MBP',200,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('300秒殺iPad',100,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('200秒殺小米MIX',300,'2017-01-01 00:00:00','2017-01-02 00:00:00');
建立秒殺成功明細(xì)表胰蝠,記錄秒殺成功的用戶信息和商品信息
-- 秒殺成功明細(xì)表
-- 用戶登錄認(rèn)證相關(guān)的信息
CREATE TABLE success_killed(
`seckill_id` bigint NOT NULL COMMENT '秒殺商品id',
`user_phone` bigint NOT NULL COMMENT '用戶手機(jī)號(hào)',
`state` tinyint NOT NULL DEFAULT -1 COMMENT '狀態(tài)標(biāo)識(shí): -1:無效 0:成功 1:已付款 2:已發(fā)貨',
`create_time` timestamp NOT NULL COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY(seckill_id,user_phone),/*聯(lián)合主鍵 防止用戶重復(fù)秒殺*/
key idx_create_time(create_time)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒殺成功明細(xì)表';
create_time就是秒殺成功的時(shí)間
因?yàn)閕d和phone可以唯一確定一個(gè)用戶歼培,所以這里要用到聯(lián)合主鍵,防止用戶重復(fù)秒殺一個(gè)商品茸塞,當(dāng)然以后也可以為此做過濾
數(shù)據(jù)庫的設(shè)計(jì)完成了躲庄,可以在控制臺(tái)或者數(shù)據(jù)庫管理工具輸入上述SQL語句
二、DAO層相關(guān)接口編碼
先在java目錄下建立兩個(gè)包:
- org.seckill.entity:數(shù)據(jù)庫對應(yīng)的實(shí)體包
- org.seckill.dao:DAO層接口包
在org.seckill.entity包下新建實(shí)體類Seckill钾虐,對應(yīng)數(shù)據(jù)庫中的seckill表
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
@Override
public String toString() {
return "Seckill [seckillId=" + seckillId +
", name=" + name +
", number=" + number +
", startTime=" + startTime+
", endTime=" + endTime +
", createTime=" + createTime +
"]";
}
}
然后直接生成getter和setter方法噪窘,并復(fù)寫toString方法
同樣在org.seckill.entity包下新建實(shí)體類SuccessKilled,對應(yīng)數(shù)據(jù)庫中的success_killed表
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
private Seckill seckill;
@Override
public String toString() {
return "SuccessKilled [seckillId=" + seckillId +
", userPhone=" + userPhone +
", state=" + state +
", createTime=" + createTime +
"]";
}
}
直接生成getter和setter方法效扫,并復(fù)寫toString方法
private Seckill seckill;
這里實(shí)例化了一個(gè)Seckill類的對象倔监,因?yàn)楫?dāng)用戶成功秒殺一個(gè)商品時(shí),可能需要完全拿到Seckill的實(shí)體
接著在org.seckill.dao包下新建接口SeckillDao菌仁,因?yàn)樵跀?shù)據(jù)庫中seckill表記錄的是秒殺商品的庫存浩习,所以當(dāng)用戶秒殺成功時(shí),應(yīng)該對數(shù)據(jù)庫進(jìn)行操作济丘,也就是減庫存
/**
* 減庫存
* @param seckillId
* @param killTime
* @return 返回受影響的行數(shù)
*/
int reduceNumber(long seckillId, Date killTime);
還可以查詢秒殺庫存表的信息
/**
* 根據(jù)id查詢秒殺對象
* @param seckillId 秒殺商品id
* @return
*/
Seckill queryById(long seckillId);
/**
* 根據(jù)偏移量查詢秒殺商品列表
* @param offset 初始位置
* @param limit 查詢個(gè)數(shù)
* @return
*/
List<Seckill> queryAll(int offset, int limit);
偏移量就是用戶可以設(shè)置初始位置offset谱秽,查詢limit個(gè)數(shù)據(jù)
在org.seckill.dao包下新建接口SuccessKilledDao,當(dāng)有一個(gè)用戶在規(guī)定時(shí)間內(nèi)成功秒殺一個(gè)商品時(shí)摹迷,進(jìn)行記錄疟赊,并且可以根據(jù)id查詢相應(yīng)的信息
/**
* 插入購買明細(xì),可過濾重復(fù)
* @param seckillId
* @param userPhone
* @return 返回受影響的行數(shù)峡碉,返回0表示沒有插入數(shù)據(jù)
*/
int insertSuccessKilled(long seckillId, long userPhone);
/**
* 根據(jù)id查詢SuccessKilled并攜帶Seckill實(shí)體
* @param seckill
* @return
*/
SuccessKilled queryByIdWithSeckill(long seckillId);
對于insertSuccessKilled方法近哟,因?yàn)閕d和phone能唯一確定一個(gè)用戶,所以當(dāng)有重復(fù)出現(xiàn)時(shí)鲫寄,不滿足條件吉执,insert語句不執(zhí)行,返回0
** 如何設(shè)置條件塔拳,體現(xiàn)在SQL語句的書寫鼠证,SQL語句寫在下面要用到的MyBatis的xml文件中 **
至此,數(shù)據(jù)庫對應(yīng)的實(shí)體類以及DAO層的接口完成了靠抑,而且不用寫接口的實(shí)現(xiàn)類,因?yàn)镸yBatis把這些工作都承擔(dān)了
那么這里就可以對DAO層有個(gè)初步的了解:
** DAO層提供了一些接口适掰,這些接口是數(shù)據(jù)庫對應(yīng)的實(shí)體類(即Seckill類和SuccessKilled類)對數(shù)據(jù)庫各種操作(例如:減庫存颂碧、記錄用戶信息等)而封裝的接口 **
三荠列、基于MyBatis實(shí)現(xiàn)DAO層接口
數(shù)據(jù)庫與項(xiàng)目之間的映射之前已經(jīng)實(shí)現(xiàn)了,數(shù)據(jù)庫中的表對應(yīng)org.seckill.entity包下的實(shí)體類载城,數(shù)據(jù)庫中的列對應(yīng)這些類中的屬性肌似,而這些對象要操作數(shù)據(jù)庫,需要中間的映射過程诉瓦,jdbc川队、MyBatis、Hibernate等都是工作在這一層睬澡,把數(shù)據(jù)庫中的數(shù)據(jù)映射到對象中固额,并通過方法,操作數(shù)據(jù)庫
在DAO層煞聪,我們已經(jīng)寫好了接口和方法斗躏,但是沒有實(shí)現(xiàn)類,如果使用jdbc昔脯,就要手動(dòng)的拿到數(shù)據(jù)庫的連接啄糙,也要有實(shí)現(xiàn)接口的實(shí)現(xiàn)類,所以使用成熟的框架可以減少工作量云稚,后期容易維護(hù)等許多好處
這里使用MyBatis隧饼,MyBatis對實(shí)現(xiàn)DAO層接口提供了兩種方法:
- MyBatis內(nèi)部有一個(gè)Mapper機(jī)制來自動(dòng)實(shí)現(xiàn)DAO層接口
- 通過API編程的方式,MyBatis提供了很多API
顯而易見静陈,大部分都是選擇自動(dòng)實(shí)現(xiàn)DAO層接口燕雁,這種方法只需設(shè)計(jì)接口,不需要寫實(shí)現(xiàn)類窿给,通過配置MyBatis的xml文件贵白,寫好SQL語句,其他的工作MyBatis都會(huì)自動(dòng)完成
1.MyBatis全局配置
先在src\main\resources下建立一個(gè)MyBatis全局的配置文件mybatis-conf.xml崩泡,再新建一個(gè)mapper目錄禁荒,用于存放MyBatis的SQL映射
打開MyBatis全局配置文件mybatis-conf.xml
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
將這些內(nèi)容復(fù)制到xml文件中,這些示例都可以在MyBatis官網(wǎng)上的參考文檔中找到
然后配置一些屬性
<configuration>
<!-- 配置全局屬性 -->
<settings>
<!-- 使用jdbc的getGenerateKeys獲取數(shù)據(jù)庫自增主鍵值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列別名替換列名 默認(rèn)為true -->
<setting name="useColumnLabel" value="true"/>
<!-- 開啟駝峰命名轉(zhuǎn)換:Table(create_time) -> Entity(createTime) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
使用列別名替換列名角撞,MyBatis默認(rèn)為true呛伴,MyBatis會(huì)自動(dòng)的識(shí)別出列別名對應(yīng)哪個(gè)列名,并賦值到entity實(shí)體屬性中
前面提到谒所,要實(shí)現(xiàn)DAO層的接口可以使用MyBatis的mapper機(jī)制热康,為DAO接口方法提供SQL語句配置,所以在mapper文件夾下創(chuàng)建相應(yīng)接口的配置文件SeckillDao.xml和SuccessKilledDao.xml
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
同樣劣领,這些內(nèi)容都要添加到xml文件中
2.SeckillDao接口SQL語句配置
打開SeckillDao.xml
<!-- 目的:為DAO接口方法提供SQL語句配置 -->
<mapper namespace="org.seckill.dao.SeckillDao">
<update id="reduceNumber" >
update
seckill
set
number = number - 1
where seckill_id = #{seckillId}
and start_time <![CDATA[ <= ]]> #{startTime}
and end_time >= #{endTime}
and number > 0;
</update>
<select id="queryById" parameterType="long" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
where seckill_id = #{seckillId}
</select>
<select id="queryAll" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
</mapper>
首先是mapper標(biāo)簽中的屬性姐军,namespace是對這個(gè)mapper的命名,也就是對這個(gè)xml文件的命名,這個(gè)命名必須在mapper目錄下唯一奕锌,因?yàn)檎嬲捻?xiàng)目中著觉,mapper下的xml文件有很多,如果命名不唯一惊暴,MyBatis就不知道要調(diào)用哪個(gè)xml文件了饼丘,一般都是包名.接口名
接著逐個(gè)分析SQL語句
<update id="reduceNumber" >
update
seckill
set
number = number - 1
where seckill_id = #{seckillId}
and start_time <![CDATA[ <= ]]> #{killTime}
and end_time >= #{killTime}
and number > 0;
</update>
因?yàn)橐獙?shí)現(xiàn)SeckillDao接口中的減庫存的方法,所以使用update語句辽话,id必須在該xml文件下唯一肄鸽,一般為方法名
int reduceNumber(long seckillId, Date killTime);//SeckillDao接口中定義的方法
update標(biāo)簽中還有parameterType屬性,這里可以不用寫油啤,MyBatis可以自動(dòng)識(shí)別
where后面有些限制條件典徘,秒殺成功的時(shí)間要在規(guī)定時(shí)間內(nèi),要晚于開始時(shí)間村砂,早于結(jié)束時(shí)間烂斋,否則update語句不會(huì)執(zhí)行,當(dāng)庫存小于等于0時(shí)础废,也不執(zhí)行update語句汛骂,數(shù)據(jù)返回類型為int,表示受影響的行數(shù)
至于下面這句
and start_time <![CDATA[ <= ]]> #{killTime}
在w3school上有詳細(xì)介紹:
術(shù)語 CDATA 指的是不應(yīng)由 XML 解析器進(jìn)行解析的文本數(shù)據(jù)(Unparsed Character Data)评腺。
在 XML 元素中帘瞭,"<" 和 "&" 是非法的。
"<" 會(huì)產(chǎn)生錯(cuò)誤蒿讥,因?yàn)榻馕銎鲿?huì)把該字符解釋為新元素的開始蝶念。
"&" 也會(huì)產(chǎn)生錯(cuò)誤,因?yàn)榻馕銎鲿?huì)把該字符解釋為字符實(shí)體的開始芋绸。
某些文本媒殉,比如 JavaScript 代碼,包含大量 "<" 或 "&" 字符摔敛。為了避免錯(cuò)誤廷蓉,可以將腳本代碼定義為 CDATA。
CDATA 部分中的所有內(nèi)容都會(huì)被解析器忽略马昙。
CDATA 部分由 "<![CDATA[" 開始桃犬,由 "]]>" 結(jié)束:
** 如果xml文件中僅有"<"和"&",還是建議把它們替換為實(shí)體引用 **
接著寫完實(shí)現(xiàn)其他方法的SQL語句
<select id="queryById" parameterType="long" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
where seckill_id = #{seckillId}
</select>
queryById方法實(shí)質(zhì)上是select查詢語句行楞,resultType返回的類型是Seckill類攒暇,因?yàn)樽远x的類不在java.lang包下,所以一般是包名.類名子房,但是后面有方法可以省略包名形用,這里就只寫類名
Seckill queryById(long seckillId);//SeckillDao接口中定義的方法
parameterType為long類型就轧,因?yàn)橐呀?jīng)開啟了駝峰轉(zhuǎn)換,所以可以不適用as進(jìn)行列名轉(zhuǎn)換
最后是queryAll方法
<select id="queryAll" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
多個(gè)參數(shù)的話尾序,可以不用給parameterType钓丰,結(jié)果按降序排列
List<Seckill> queryAll(int offset, int limit);//SeckillDao接口中定義的方法
對于resultType躯砰,無論返回的是List還是Map每币,只要給出里面的類型就可以
3.SuccessKilledDao接口SQL語句配置
打開SuccessKilledDao.xml
<mapper namespace="org.seckill.dao.SuccessKilledDao">
<insert id="insertSuccessKilled">
<!-- 主鍵沖突:使用ignore忽略報(bào)錯(cuò) insert不執(zhí)行 返回0 -->
insert ignore into success_killed(seckill_id,user_phone)
values (#{seckillId},#{userPhone})
</insert>
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!-- 根據(jù)id查詢SuccessKilled并攜帶Seckill實(shí)體 -->
<!-- 如何告訴Mybatis把結(jié)果映射到SuccessKilled同時(shí)映射Seckill屬性 -->
select
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",
s.name "seckill.name",
s.start_time "seckill.start_time",
s.end_time "seckill.end_time",
s.create_time "seckill.create_time"
from success_killed sk
inner join seckill s on sk.seckill_id = s.seckill_id
where sk.seckill_id = #{seckillId}
</select>
</mapper>
簡單說下insertSuccessKilled方法,在src\main\sql目錄下有個(gè)schema.sql文件琢歇,里面是建表語句兰怠,在建立success_killed表的時(shí)候設(shè)置了一個(gè)聯(lián)合主鍵,是防止用戶重復(fù)秒殺的
PRIMARY KEY(seckill_id,user_phone)
所以id和phone只要有一個(gè)重復(fù)李茫,insert語句就會(huì)報(bào)錯(cuò)揭保,對于這種錯(cuò)誤,其實(shí)只要不執(zhí)行insert即可魄宏,不需要每次都報(bào)錯(cuò)秸侣,所以使用ignore關(guān)鍵字,當(dāng)有主鍵沖突時(shí)宠互,忽略報(bào)錯(cuò)味榛,insert語句不會(huì)執(zhí)行,結(jié)果返回0予跌,說明沒有插入數(shù)據(jù)
對于queryByIdWithSeckill方法
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!-- 根據(jù)id查詢SuccessKilled并攜帶Seckill實(shí)體 -->
<!-- 如何告訴Mybatis把結(jié)果映射到SuccessKilled同時(shí)映射Seckill屬性 -->
select
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",
s.name "seckill.name",
s.start_time "seckill.start_time",
s.end_time "seckill.end_time",
s.create_time "seckill.create_time"
from success_killed sk
inner join seckill s on sk.seckill_id = s.seckill_id
where sk.seckill_id = #{seckillId}
</select>
首先要明確的是這個(gè)方法的作用搏色,是根據(jù)id查詢SuccessKilled并攜帶Seckill實(shí)體
SuccessKilled queryByIdWithSeckill(long seckillId);//SuccessKilledDao接口中定義的方法
返回SuccessKilled類型,在這個(gè)類中券册,實(shí)例化了Seckill類
from success_killed sk
inner join seckill s on sk.seckill_id = s.seckill_id
where sk.seckill_id = #{seckillId}
from success_killed表频轿,再使用內(nèi)連接的方式使seckill表加入進(jìn)來,on后面表示兩個(gè)表通過相同的id進(jìn)行連接烁焙,id的值為傳進(jìn)來的參數(shù)seckillId的值
在MyBatis中可以忽略as關(guān)鍵字
那么如何告訴Mybatis把結(jié)果映射到SuccessKilled同時(shí)映射Seckill屬性航邢,首先可以得到sk表即success_killed表中的內(nèi)容
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
sk.seckill_id雖然使用了別名,** 但是MyBatis會(huì)忽略別名 骄蝇,所以MyBatis視為從sk表中的seckill_id列取數(shù)據(jù)膳殷,再返回?cái)?shù)據(jù)到Java, 因?yàn)樵贛yBatis全局配置文件中開啟了駝峰命名轉(zhuǎn)換 **乞榨,所以seckill_id就變成了seckillId秽之,賦值給相應(yīng)的變量,這就是使用框架的好處
取到了數(shù)據(jù)吃既,映射到了SuccessKilled中考榨,又怎么同時(shí)映射Seckill屬性呢?
在SuccessKilled類中鹦倚,** 直接實(shí)例化了Seckill類 **河质,并生成了getter和setter方法
success_killed和seckill兩個(gè)表又通過內(nèi)連接的方式進(jìn)行了連接,所以可以直接在select后面這樣寫
s.seckill_id "seckill.seckill_id",
s.name "seckill.name",
s.start_time "seckill.start_time",
s.end_time "seckill.end_time",
s.create_time "seckill.create_time"
前面說過,MyBatis會(huì)忽略別名掀鹅,所以這里要在后面表明散休,這些列是來自哪個(gè)表的,這種寫法實(shí)際是OGNL表達(dá)式乐尊,據(jù)說在Struts上很常見戚丸,但是在MyBatis的xml文件中也經(jīng)常用到,所以還是要多了解下
到這里扔嵌,MyBatis實(shí)現(xiàn)DAO層接口完成了
四限府、MyBatis與Spring整合
在src\main\resources\spring\下新建一個(gè)xml文件spring-dao.xml,所有的DAO層配置都放在該文件中痢缎,關(guān)于配置文件的一些信息 在Spring官網(wǎng)上可以找
在Spring Projects下面可以找到Spring Framework胁勺,選擇版本,我在pom.xml文件中配置的MyBatis是4.3.5独旷, 點(diǎn)擊Reference署穗,使用Ctrl+F搜索容器相關(guān)的
點(diǎn)擊7.2.Container overview,找到相關(guān)配置文件的示例嵌洼,把beans標(biāo)簽內(nèi)的所有內(nèi)容復(fù)制到項(xiàng)目的spring-dao.xml中
然后開始配置整合Mybatis
1.配置數(shù)據(jù)庫相關(guān)參數(shù)
在src\main\resources\新建一個(gè)jdbc的配置文件jdbc.properties
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql:///seckill?useUnicode=true&characterEncoding=utf8
db.user=root
db.password=
在練習(xí)的項(xiàng)目中可以使用數(shù)據(jù)庫的root用戶案疲,實(shí)際工作中不建議使用,我的數(shù)據(jù)庫沒有設(shè)置密碼咱台,所以password為空
這是獲取數(shù)據(jù)庫的一些配置络拌,在url中
jdbc:mysql:///seckill 等價(jià)于 jdbc:mysql://127.0.0.1:3306/seckill
數(shù)據(jù)庫默認(rèn)的端口是3306,可寫可不寫回溺,最后跟的是數(shù)據(jù)庫的名字
至于后面的一些參數(shù)
useUnicode=true&characterEncoding=utf8
使用Unicode編碼春贸,編碼方式為utf8
有些版本的MySQL需要加密數(shù)據(jù)通道,同時(shí)需要檢查服務(wù)器認(rèn)證證書遗遵,在實(shí)際的工作中萍恕,這些根據(jù)實(shí)際情況配置,為了數(shù)據(jù)安全應(yīng)該是盡可能的開啟车要,作為練習(xí)的項(xiàng)目允粤,就可以不用了
Establishing SSL connection without server's identity verification is not recommended.
According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set.
For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'.
You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
如果有關(guān)于數(shù)據(jù)通道的加密和認(rèn)證證書的問題,可以把下面的參數(shù)添加到j(luò)dbc的url后面
useSSL=true&verifyServerCertificate=false
在xml配置文件中配置數(shù)據(jù)庫url時(shí)翼岁,要使用&的轉(zhuǎn)義字符也就是&
然后打開spring-dao.xml文件类垫,添加下面一行
<!-- 配置數(shù)據(jù)庫相關(guān)參數(shù) -->
<context:property-placeholder location="classpath:jdbc.properties"/>
這時(shí),如果你的IDE跟我的Eclipse一樣不靠譜的話琅坡,還要自己手動(dòng)添加幾行內(nèi)容悉患,從Spring上找的xml配置只是最基本的,這次用到了context標(biāo)簽的內(nèi)容榆俺,就要把下面的內(nèi)容添加到beans的標(biāo)簽內(nèi)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
最終的內(nèi)容就是這些售躁,跟從Spring官網(wǎng)上復(fù)制的相比坞淮,這次多了三條關(guān)于context的配置
2.配置數(shù)據(jù)庫連接池
<!-- 配置數(shù)據(jù)庫連接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置連接池屬性 -->
<property name="driverClass" value="${db.driver}"/>
<property name="jdbcUrl" value="${db.url}"/>
<property name="user" value="${db.user}"/>
<property name="password" value="${db.password}"/>
<!-- 配置c3p0連接池的私有屬性 -->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!-- 關(guān)閉連接后不自動(dòng)commit -->
<property name="autoCommitOnClose" value="false"/>
<!-- 獲取連接超時(shí)時(shí)間 -->
<property name="checkoutTimeout" value="1000"/>
<!-- 獲取連接失敗重試次數(shù) -->
<property name="acquireRetryAttempts" value="2"/>
</bean>
配置連接池的屬性,結(jié)合jdbc.properties來寫
關(guān)于#{}與${}的區(qū)別,#{}在MyBatis的SQL語句配置中有著預(yù)編譯的效果MyBatis會(huì)先把#{}視為“?”帜羊,等到執(zhí)行預(yù)編譯語句的時(shí)候就會(huì)換成對應(yīng)的參數(shù),這些MyBatis都自動(dòng)實(shí)現(xiàn)了啡直,而${}是沒有預(yù)編譯效果,在spring-dao的配置中參數(shù)要拿來就能用凌盯,不需要預(yù)編譯付枫,所以這里用${}
關(guān)于c3p0的私有屬性,這就是根據(jù)實(shí)際情況設(shè)置的驰怎,還有很多,這里就簡單的設(shè)置幾條
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
這是設(shè)置連接池中連接個(gè)數(shù)的最大值和最小值二打,默認(rèn)最大值為15县忌、最小值為3
<!-- 關(guān)閉連接后不自動(dòng)commit -->
<property name="autoCommitOnClose" value="false"/>
對于autoCommitOnClose這個(gè)屬性,就是當(dāng)連接池的connection變?yōu)閏lose的時(shí)候继效,實(shí)際是把連接對象放到池子當(dāng)中症杏,這個(gè)過程當(dāng)中連接池會(huì)做相應(yīng)的清理工作,如果把a(bǔ)utoCommitOnClose設(shè)置為true瑞信,當(dāng)我們調(diào)用close的時(shí)候會(huì)連接池會(huì)自動(dòng)commit厉颤,不過本來這個(gè)屬性c3p0默認(rèn)為false,這里只是強(qiáng)調(diào)一下
<!-- 獲取連接超時(shí)時(shí)間 -->
<property name="checkoutTimeout" value="1000"/>
<!-- 獲取連接失敗重試次數(shù) -->
<property name="acquireRetryAttempts" value="2"/>
對于連接超時(shí)的設(shè)置凡简,在實(shí)際項(xiàng)目中很有必要逼友,但是自己練習(xí)的時(shí)候可有可無,后面單元測試的時(shí)候秤涩,如果長時(shí)間都拿不到數(shù)據(jù)帜乞,每次都超時(shí)的時(shí)候,可以把這個(gè)屬性注釋掉筐眷,先測試程序能否正常運(yùn)行
3.配置SqlSessionFactory對象
<!-- 配置SqlSessionFactory對象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入數(shù)據(jù)庫連接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置Mybatis全局配置文件 即mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 掃描entity包 使用別名 省略包名 -->
<property name="typeAliasesPackage" value="org.seckill.entity"/>
<!-- 掃描SQL配置文件 即mapper目錄下的xml文件 -->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
前面兩步黎烈,基本上每個(gè)項(xiàng)目都一樣,從這開始匀谣,是MyBatis的配置照棋,或者使用別的框架,對框架相應(yīng)的配置
使用typeAliasesPackage可以掃描指定的包武翎,之前說到的resultType可以直接使用類名烈炭,就是因?yàn)檫@個(gè)屬性,如果有多個(gè)包要掃描的話后频,使用分號(hào)隔開
對于使用classpath引入配置文件
在java和resources目錄下都是classpath的范圍
4.配置掃描DAO接口包
<!-- 配置掃描DAO接口包 動(dòng)態(tài)實(shí)現(xiàn)DAO接口并注入到Spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 掃描DAO層下的接口 -->
<property name="basePackage" value="org.seckill.dao"/>
</bean>
在這個(gè)bean中梳庆,沒有id暖途,因?yàn)槠渌渲貌粫?huì)調(diào)用這個(gè)bean
對于注入sqlSessionFactory
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
為什么使用BeanName的方法?
當(dāng)MapperScannerConfigurer啟動(dòng)的時(shí)候膏执,如果還沒有加載jdbc.properties配置文件驻售,這樣拿到的dataSource就是錯(cuò)誤的,因?yàn)?{}中的屬性值還沒有被替換更米,所以通過BeanName后處理的方式欺栗,當(dāng)使用MyBatis的時(shí)候,才回去找對應(yīng)的SQLSessionFactory對象征峦,為了防止MapperScannerConfigurer提前初始化SQLSessionFactory
至此迟几,所有的Mybatis和Spring整合的過程完成了
五、DAO層單元測試
1.SeckillDao接口測試
不同的IDE建立測試類的方式大同小異栏笆,下面是Eclipse的過程
在項(xiàng)目列表中类腮,右鍵SeckillDao.java文件,選擇New->Other蛉加,搜索junit蚜枢,選擇JUnit Test Case,點(diǎn)擊Next
最上面可以選擇junit版本针饥,這里使用junit4
緊接著改動(dòng)的是Source folder厂抽,點(diǎn)擊右邊的按鈕
默認(rèn)的是在sec/main/java目錄下,應(yīng)該改為src/test/java目錄下丁眼,之前說過筷凤,單元測試的內(nèi)容都在test目錄下,點(diǎn)擊Ok
先不要著急點(diǎn)Finish苞七,點(diǎn)擊Next藐守,要測試所有的方法,點(diǎn)擊Select All->Finish
點(diǎn)擊OK
此時(shí)可以看到莽鸭,單元測試已經(jīng)添加成功
測試類建好后吗伤,先要配置Spring和junit整合,為了是junit啟動(dòng)時(shí)加載SpringIOC容器
//Spring與junit整合
@RunWith(SpringJUnit4ClassRunner.class)
//告訴junit Spring配置文件的位置
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
在SeckillDaoTest方法上添加兩個(gè)注解硫眨,Spring提供了一個(gè)RunWith接口 是在runner下面的足淆,使用RunWith就實(shí)現(xiàn)了junit啟動(dòng)時(shí)加載SpringIOC容器
還要告訴junit Spring配置文件的位置,使用ContextConfiguration注解礁阁,在加載SpringIOC容器的時(shí)候同時(shí)加載spring-dao.xml文件巧号,驗(yàn)證Spring與MyBatis整合,數(shù)據(jù)庫連接池是否OK等配置
要測試SeckillDao接口姥闭,就要先注入SeckillDao丹鸿,直接實(shí)例化
//注入DAO實(shí)現(xiàn)類依賴
@Autowiredprivate SeckillDao seckillDao;
視頻上使用的是@Resource注解,會(huì)報(bào)錯(cuò)棚品,找不到這個(gè)類靠欢,我也折騰了半天廊敌,索性直接用@Autowired注解
先測試queryById方法
//Spring與junit整合
@RunWith(SpringJUnit4ClassRunner.class)
//告訴junit Spring配置文件的位置
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SeckillDaoTest {
//注入DAO實(shí)現(xiàn)類依賴
@Autowired
private SeckillDao seckillDao;
@Test
public void testReduceNumber() throws Exception {
fail("Not yet implemented");
}
@Test
public void testQueryById() throws Exception {
long id = 1000;
Seckill seckill = seckillDao.queryById(id);
System.out.println(seckill.getName());
System.out.println(seckill);
}
@Test
public void testQueryAll() throws Exception {
fail("Not yet implemented");
}
}
剛開始因?yàn)锧Resource注解的問題一直找不到解決的方法,同時(shí)還有別的報(bào)錯(cuò)信息
Class not found org.seckill.dao.SeckillDaoTest
java.lang.ClassNotFoundException: org.seckill.dao.SeckillDaoTest
一時(shí)間找不到頭緒门怪,看到有人說可能是maven的配置問題骡澈,有些依賴沒配置上,我就按照給出的信息
顯示哪個(gè)jar包有問題掷空,就刪哪個(gè)肋殴,然后讓maven自己下載,但是刪了一個(gè)又報(bào)錯(cuò)另一個(gè)坦弟,加上下載速度慢护锤,又是大半天浪費(fèi)了
然后腦子一抽,索性把a(bǔ)pache-maven-3.3.9.m2\repository目錄下的依賴全刪了酿傍,就這樣刪了又下烙懦,改版本,下了又刪拧粪,兩天時(shí)間就這樣過去了
最后快崩潰了修陡,決定還是按照視頻中的版本來,畢竟對新版本的特性不熟悉可霎,萬一再出些幺蛾子,就該摔電腦了
然后一切又恢復(fù)到兩天前的樣子宴杀,@Resource依舊報(bào)錯(cuò)癣朗,把@Resource替換成@Autowired就沒有報(bào)錯(cuò),然后開始測試
嚴(yán)重: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@105fece7] to prepare test instance [org.seckill.dao.SeckillDaoTest@52045dbe]
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [spring/spring-dao.xml]: Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'dataSource' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy
Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'dataSource' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy
眼看著文件中沒有紅叉旺罢,但就是測試不通過旷余,打斷點(diǎn)都不行,顯然是加載的時(shí)候就有問題扁达,這里面不斷提到找不到一個(gè)類
java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy
上網(wǎng)找了半天正卧,都說在pom.xml中沒有引入spring-jdbc的依賴,要是錯(cuò)誤都這么顯而易見跪解,都皆大歡喜了炉旷,于是針對spring-jdbc,又是循環(huán)上面的過程叉讥,刪了又下窘行,下了又刪,因?yàn)橐推渌鸖pring配置版本相同图仓,就沒改版本罐盔,查了半天,下載了半天救崔,依舊是找不到這個(gè)類
然后腦子又一抽惶看,既然這個(gè)版本找不到捏顺,換個(gè)版本試試,也不能一下子就跳到新版本纬黎,萬一與其他依賴不兼容就崩潰了幅骄,所以選擇了4.1.7.RELEASE下個(gè)版本的最新版本4.2.9.RELEASE
結(jié)果就看到
快速的點(diǎn)開控制臺(tái)
看到id為1000的數(shù)據(jù)輸出了,兩天半的時(shí)間莹桅,快被玩的就要砸電腦了...
接下來測試queryAll方法
@Test
public void testQueryAll() throws Exception {
List<Seckill> seckills = seckillDao.queryAll(0, 100);
for(Seckill seckill : seckills){
System.out.println(seckill);
System.out.println();
}
然后就看到熟悉的junit紅色進(jìn)度條和錯(cuò)誤信息
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [0, 1, param1, param2]
參數(shù)到SQL語句綁定的時(shí)候出了問題昌执,找不到參數(shù)offset,可以回顧下在mapper目錄下的SQL語句配置文件SeckillDao.xml中的SQL是怎么寫的
<select id="queryAll" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
對比著接口中的方法定義
List<Seckill> queryAll(int offset, int limit);//SeckillDao接口中方法的定義
既然接口中和SQL語句中都寫的和明確诈泼,但是為什么綁定不了參數(shù)懂拾?
原因就是** Java沒有保存形參的記錄 **,意味著在Java運(yùn)行過程中
queryAll(int offset, int limit);等價(jià)于queryAll(arg0, arg1);
如果方法只有一個(gè)參數(shù)的話铐达,就沒關(guān)系岖赋,比如上面的queryById方法,所以當(dāng)有多個(gè)參數(shù)的時(shí)候瓮孙,就要告訴MyBatis唐断,哪個(gè)參數(shù)對應(yīng)在哪個(gè)位置,這時(shí)就要對接口中的方法做些改動(dòng)杭抠,MyBatis提供了一個(gè)注解@Param
List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
使用注解的方式脸甘,告訴MyBatis,第一個(gè)參數(shù)叫offset偏灿,對應(yīng)SQL語句中#{offset}丹诀,然后再測試queryAll方法
接著測試最后一個(gè)方法reduceNumber,先看一下接口中方法的定義
int reduceNumber(long seckillId, Date killTime);
傳遞兩個(gè)參數(shù)翁垂,一個(gè)是long類型铆遭,一個(gè)是Date類型,返回int
@Test
public void testReduceNumber() throws Exception {
Date killTime = new Date();
int updateCount = seckillDao.reduceNumber(1000L, killTime);
System.out.println("updateCount = " + updateCount);
}
右鍵測試
依然是上面的錯(cuò)誤沿猜,修改接口中的方法即可
可以看看控制臺(tái)的輸出枚荣,有利于理解整個(gè)運(yùn)行過程
DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@75201592] will not be managed by Spring
DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Preparing: update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0;
DEBUG o.s.dao.SeckillDao.reduceNumber - ==> Parameters: 1000(Long), 2017-01-06 21:12:04.444(Timestamp), 2017-01-06 21:12:04.444(Timestamp)
DEBUG o.s.dao.SeckillDao.reduceNumber - <== Updates: 0
首先是jdbc通過c3p0連接池拿到了數(shù)據(jù)庫的連接,但是這個(gè)jdbc連接沒有被Spring所托管啼肩,是從c3p0拿到的
然后控制臺(tái)還輸出了SQL語句
update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0;
之前寫的#{}都被MyBatis視為占位符“橄妆?”,這就是因?yàn)?{}具有預(yù)編譯的功能
接著顯示的是傳遞過去的參數(shù)疟游,最后輸出結(jié)果是0呼畸,為什么沒有進(jìn)行減庫存的操作呢?
因?yàn)樵缭趧?chuàng)建數(shù)據(jù)庫颁虐,插入數(shù)據(jù)的時(shí)候蛮原,就已經(jīng)設(shè)置了秒殺時(shí)間段
-- 初始化數(shù)據(jù)
INSERT INTO
seckill(name,number,start_time,end_time)
VALUES
('1000秒殺iPhone6S',100,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('500秒殺MBP',200,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('300秒殺iPad',100,'2017-01-01 00:00:00','2017-01-02 00:00:00'),
('200秒殺小米MIX',300,'2017-01-01 00:00:00','2017-01-02 00:00:00');
秒殺活動(dòng)從1號(hào)開始,2號(hào)結(jié)束另绩,被maven的依賴折騰后儒陨,已經(jīng)是6號(hào)了花嘶,所以不在秒殺時(shí)間段內(nèi),沒有執(zhí)行update語句
SeckillDao接口的測試就完成了
2.SuccessKilledDao接口測試
使用Eclipse蹦漠,根據(jù)上面的步驟椭员,建立SuccessKilledDao接口的測試類
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SuccessKilledDaoTest
同樣在類上面添加兩個(gè)注解,Spring和junit整合的注解笛园,然后是告訴Spring配置文件的位置
給這個(gè)測試類注入SuccessKilledDao
@Autowired
private SuccessKilledDao successKilledDao;
首先是insertSuccessKilled方法隘击,先看看接口中方法的定義
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
傳遞的是多個(gè)參數(shù),所以依舊需要改動(dòng)研铆,使用MyBatis的@Param注解
根據(jù)方法的定義埋同,就可以寫測試類了
@Test
public void testInsertSuccessKilled() {
long id = 1000L;
long phone = 13512345678L;
int insertCount = successKilledDao.insertSuccessKilled(id, phone);
System.out.println("insertCount = " + insertCount);
}
返回1,說明成功插入信息
之前說過棵红,這個(gè)方法可以防止用戶重復(fù)秒殺凶赁,所以可以不改變參數(shù),再執(zhí)行一次
可以看到返回值是0逆甜,說明沒有執(zhí)行insert語句
這里還有點(diǎn)小問題
`state` tinyint NOT NULL DEFAULT -1 COMMENT '狀態(tài)標(biāo)識(shí): -1:無效 0:成功 1:已付款 2:已發(fā)貨',
在開始的建表語句的時(shí)候虱肄,定義了state屬性,是狀態(tài)標(biāo)識(shí)交煞,既然能成功執(zhí)行insertSuccessKilled方法咏窿,說明可以插入數(shù)據(jù),那么state應(yīng)該是0素征,所以要改動(dòng)一下SQL語句
<insert id="insertSuccessKilled">
<!-- 主鍵沖突:使用ignore忽略報(bào)錯(cuò) insert不執(zhí)行 返回0 -->
insert ignore into success_killed(seckill_id,user_phone,state)
values (#{seckillId},#{userPhone},0)
</insert>
這樣翰灾,再插入的數(shù)據(jù)的state就是0了
然后是queryByIdWithSeckill方法,先看方法的定義
SuccessKilled queryByIdWithSeckill(long seckillId);
由于之前考慮的不周到稚茅,這條語句還要有些改動(dòng)
因?yàn)镾eckill與SuccessKilled是一對多的關(guān)系,一個(gè)秒殺商品對應(yīng)多個(gè)成功秒殺記錄平斩,那么想要查詢某個(gè)人的秒殺記錄的時(shí)候亚享,上面的語句就行不通了,所以要添加一個(gè)參數(shù)
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
多了一個(gè)參數(shù)绘面,所以還要加上@Param注解欺税,同時(shí),還要改動(dòng)的地方是mapper目錄下SuccessKilledDao.xml文件揭璃,找到與方法名相同的id
where sk.seckill_id = #{seckillId} and sk.user_phone = #{userPhone}
前面已經(jīng)插入過一條成功秒殺的信息晚凿,所以還是用前面的數(shù)據(jù)
@Test
public void testQueryByIdWithSeckill() {
long id = 1000L;
long phone = 13512345678L;
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone);
System.out.println(successKilled);
System.out.println(successKilled.getSeckillId());
}
因?yàn)樵赟uccessKilled類中已經(jīng)實(shí)例化了Seckill類,并生成了getter和setter方法瘦馍,所以這里也可以取到Seckill對象
終于歼秽,所有的DAO層的工作已經(jīng)完成了
六、DAO層編碼后的一些思考
回顧從最初的創(chuàng)建數(shù)據(jù)庫開始情组,到設(shè)計(jì)接口燥筷、編寫SQL語句箩祥、各種配置文件,中間沒有寫一行邏輯代碼肆氓,DAO層的工作實(shí)際上演變?yōu)榱?* 接口設(shè)計(jì)+SQL編寫+配置文件 **袍祖,好處就是源代碼和SQL進(jìn)行了分離,方便Review谢揪,而DAO拼接等邏輯在Service層完成