高并發(fā)秒殺API(二)

前言

本篇將完成DAO層的設(shè)計(jì)與開發(fā),包括:

  • 數(shù)據(jù)庫(kù)、DAO實(shí)體與接口設(shè)計(jì)與編碼
  • 基于MyBatis實(shí)現(xiàn)DAO編程
  • MyBatis與Spring整合
  • DAO層單元測(cè)試

一、數(shù)據(jù)庫(kù)設(shè)計(jì)與編碼

打開Eclipse,在src\main下建立一個(gè)文件夾sql,用于存放建表語(yǔ)句,新建一個(gè)SQL文件schema.sql,先創(chuàng)建一個(gè)秒殺商品的庫(kù)存表

-- 數(shù)據(jù)庫(kù)初始化腳本

-- 創(chuàng)建數(shù)據(jù)庫(kù)
CREATE DATABASE seckill;

-- 使用數(shù)據(jù)庫(kù)
USE seckill;

--創(chuàng)建秒殺庫(kù)存表
CREATE TABLE seckill(
`seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品庫(kù)存id',
`name` varchar(120) NOT NULL COMMENT '商品名稱',
`number` int NOT NULL COMMENT '庫(kù)存數(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='秒殺庫(kù)存表';

主鍵為seckill_id,再單獨(dú)對(duì)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ù)庫(kù)的設(shè)計(jì)完成了,可以在控制臺(tái)或者數(shù)據(jù)庫(kù)管理工具輸入上述SQL語(yǔ)句


創(chuàng)建數(shù)據(jù)庫(kù)

二、DAO層相關(guān)接口編碼

先在java目錄下建立兩個(gè)包:

  • org.seckill.entity:數(shù)據(jù)庫(kù)對(duì)應(yīng)的實(shí)體包
  • org.seckill.dao:DAO層接口包

在org.seckill.entity包下新建實(shí)體類Seckill,對(duì)應(yīng)數(shù)據(jù)庫(kù)中的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,對(duì)應(yīng)數(shù)據(jù)庫(kù)中的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類的對(duì)象,因?yàn)楫?dāng)用戶成功秒殺一個(gè)商品時(shí),可能需要完全拿到Seckill的實(shí)體

接著在org.seckill.dao包下新建接口SeckillDao,因?yàn)樵跀?shù)據(jù)庫(kù)中seckill表記錄的是秒殺商品的庫(kù)存,所以當(dāng)用戶秒殺成功時(shí),應(yīng)該對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作,也就是減庫(kù)存

    /**
     * 減庫(kù)存
     * @param seckillId
     * @param killTime
     * @return 返回受影響的行數(shù)
     */
    int reduceNumber(long seckillId, Date killTime);

還可以查詢秒殺庫(kù)存表的信息

    /**
     * 根據(jù)id查詢秒殺對(duì)象
     * @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)的信息

    /**
     * 插入購(gòu)買明細(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);

對(duì)于insertSuccessKilled方法,因?yàn)閕d和phone能唯一確定一個(gè)用戶,所以當(dāng)有重復(fù)出現(xiàn)時(shí),不滿足條件,insert語(yǔ)句不執(zhí)行,返回0
** 如何設(shè)置條件,體現(xiàn)在SQL語(yǔ)句的書寫,SQL語(yǔ)句寫在下面要用到的MyBatis的xml文件中 **

至此,數(shù)據(jù)庫(kù)對(duì)應(yīng)的實(shí)體類以及DAO層的接口完成了,而且不用寫接口的實(shí)現(xiàn)類,因?yàn)镸yBatis把這些工作都承擔(dān)了

那么這里就可以對(duì)DAO層有個(gè)初步的了解:
** DAO層提供了一些接口,這些接口是數(shù)據(jù)庫(kù)對(duì)應(yīng)的實(shí)體類(即Seckill類和SuccessKilled類)對(duì)數(shù)據(jù)庫(kù)各種操作(例如:減庫(kù)存、記錄用戶信息等)而封裝的接口 **

三、基于MyBatis實(shí)現(xiàn)DAO層接口

Mybatis框架的作用

數(shù)據(jù)庫(kù)與項(xiàng)目之間的映射之前已經(jīng)實(shí)現(xiàn)了,數(shù)據(jù)庫(kù)中的表對(duì)應(yīng)org.seckill.entity包下的實(shí)體類,數(shù)據(jù)庫(kù)中的列對(duì)應(yīng)這些類中的屬性,而這些對(duì)象要操作數(shù)據(jù)庫(kù),需要中間的映射過程,jdbc、MyBatis、Hibernate等都是工作在這一層,把數(shù)據(jù)庫(kù)中的數(shù)據(jù)映射到對(duì)象中,并通過方法,操作數(shù)據(jù)庫(kù)

在DAO層,我們已經(jīng)寫好了接口和方法,但是沒有實(shí)現(xiàn)類,如果使用jdbc,就要手動(dòng)的拿到數(shù)據(jù)庫(kù)的連接,也要有實(shí)現(xiàn)接口的實(shí)現(xiàn)類,所以使用成熟的框架可以減少工作量,后期容易維護(hù)等許多好處

這里使用MyBatis,MyBatis對(duì)實(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語(yǔ)句,其他的工作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ù)庫(kù)自增主鍵值 -->
        <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í)別出列別名對(duì)應(yīng)哪個(gè)列名,并賦值到entity實(shí)體屬性中

前面提到,要實(shí)現(xiàn)DAO層的接口可以使用MyBatis的mapper機(jī)制,為DAO接口方法提供SQL語(yǔ)句配置,所以在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語(yǔ)句配置

打開SeckillDao.xml

<!-- 目的:為DAO接口方法提供SQL語(yǔ)句配置 -->
<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是對(duì)這個(gè)mapper的命名,也就是對(duì)這個(gè)xml文件的命名,這個(gè)命名必須在mapper目錄下唯一,因?yàn)檎嬲捻?xiàng)目中,mapper下的xml文件有很多,如果命名不唯一,MyBatis就不知道要調(diào)用哪個(gè)xml文件了,一般都是包名.接口名

接著逐個(gè)分析SQL語(yǔ)句

    <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接口中的減庫(kù)存的方法,所以使用update語(yǔ)句,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語(yǔ)句不會(huì)執(zhí)行,當(dāng)庫(kù)存小于等于0時(shí),也不執(zhí)行update語(yǔ)句,數(shù)據(jù)返回類型為int,表示受影響的行數(shù)

至于下面這句

and start_time <![CDATA[ <= ]]> #{killTime}

w3school上有詳細(xì)介紹:

術(shù)語(yǔ) 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語(yǔ)句

    <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查詢語(yǔ)句,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接口中定義的方法

對(duì)于resultType,無論返回的是List還是Map,只要給出里面的類型就可以

3.SuccessKilledDao接口SQL語(yǔ)句配置

打開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> 

簡(jiǎn)單說下insertSuccessKilled方法,在src\main\sql目錄下有個(gè)schema.sql文件,里面是建表語(yǔ)句,在建立success_killed表的時(shí)候設(shè)置了一個(gè)聯(lián)合主鍵,是防止用戶重復(fù)秒殺的

PRIMARY KEY(seckill_id,user_phone)

所以id和phone只要有一個(gè)重復(fù),insert語(yǔ)句就會(huì)報(bào)錯(cuò),對(duì)于這種錯(cuò)誤,其實(shí)只要不執(zhí)行insert即可,不需要每次都報(bào)錯(cuò),所以使用ignore關(guān)鍵字,當(dāng)有主鍵沖突時(shí),忽略報(bào)錯(cuò),insert語(yǔ)句不會(huì)執(zhí)行,結(jié)果返回0,說明沒有插入數(shù)據(jù)

對(duì)于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方法


SuccessKilled類中的實(shí)例化Seckill類

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官網(wǎng)

在Spring Projects下面可以找到Spring Framework,選擇版本,我在pom.xml文件中配置的MyBatis是4.3.5, 點(diǎn)擊Reference,使用Ctrl+F搜索容器相關(guān)的
Spring官方文檔

點(diǎn)擊7.2.Container overview,找到相關(guān)配置文件的示例,把beans標(biāo)簽內(nèi)的所有內(nèi)容復(fù)制到項(xiàng)目的spring-dao.xml中

Spring官方文檔

然后開始配置整合Mybatis

1.配置數(shù)據(jù)庫(kù)相關(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ù)庫(kù)的root用戶,實(shí)際工作中不建議使用,我的數(shù)據(jù)庫(kù)沒有設(shè)置密碼,所以password為空

這是獲取數(shù)據(jù)庫(kù)的一些配置,在url中

jdbc:mysql:///seckill 等價(jià)于 jdbc:mysql://127.0.0.1:3306/seckill

數(shù)據(jù)庫(kù)默認(rèn)的端口是3306,可寫可不寫,最后跟的是數(shù)據(jù)庫(kù)的名字

至于后面的一些參數(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ù)庫(kù)url時(shí),要使用&的轉(zhuǎn)義字符也就是& 

然后打開spring-dao.xml文件,添加下面一行

<!-- 配置數(shù)據(jù)庫(kù)相關(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ù)庫(kù)連接池

<!-- 配置數(shù)據(jù)庫(kù)連接池 -->
<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ǔ)句配置中有著預(yù)編譯的效果MyBatis會(huì)先把#{}視為“?”,等到執(zhí)行預(yù)編譯語(yǔ)句的時(shí)候就會(huì)換成對(duì)應(yīng)的參數(shù),這些MyBatis都自動(dòng)實(shí)現(xiàn)了,而${}是沒有預(yù)編譯效果,在spring-dao的配置中參數(shù)要拿來就能用,不需要預(yù)編譯,所以這里用${}

關(guān)于c3p0的私有屬性,這就是根據(jù)實(shí)際情況設(shè)置的,還有很多,這里就簡(jiǎn)單的設(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"/>

對(duì)于autoCommitOnClose這個(gè)屬性,就是當(dāng)連接池的connection變?yōu)閏lose的時(shí)候,實(shí)際是把連接對(duì)象放到池子當(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"/>

對(duì)于連接超時(shí)的設(shè)置,在實(shí)際項(xiàng)目中很有必要,但是自己練習(xí)的時(shí)候可有可無,后面單元測(cè)試的時(shí)候,如果長(zhǎng)時(shí)間都拿不到數(shù)據(jù),每次都超時(shí)的時(shí)候,可以把這個(gè)屬性注釋掉,先測(cè)試程序能否正常運(yùn)行

3.配置SqlSessionFactory對(duì)象

<!-- 配置SqlSessionFactory對(duì)象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 注入數(shù)據(jù)庫(kù)連接池 -->
    <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的配置,或者使用別的框架,對(duì)框架相應(yīng)的配置
使用typeAliasesPackage可以掃描指定的包,之前說到的resultType可以直接使用類名,就是因?yàn)檫@個(gè)屬性,如果有多個(gè)包要掃描的話,使用分號(hào)隔開

對(duì)于使用classpath引入配置文件


項(xiàng)目目錄

在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

對(duì)于注入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í)候,才回去找對(duì)應(yīng)的SQLSessionFactory對(duì)象,為了防止MapperScannerConfigurer提前初始化SQLSessionFactory

至此,所有的Mybatis和Spring整合的過程完成了

五、DAO層單元測(cè)試

1.SeckillDao接口測(cè)試

不同的IDE建立測(cè)試類的方式大同小異,下面是Eclipse的過程
在項(xiàng)目列表中,右鍵SeckillDao.java文件,選擇New->Other,搜索junit,選擇JUnit Test Case,點(diǎn)擊Next


Eclipse中創(chuàng)建junit測(cè)試類
Eclipse中創(chuàng)建junit測(cè)試類

最上面可以選擇junit版本,這里使用junit4

緊接著改動(dòng)的是Source folder,點(diǎn)擊右邊的按鈕


Source Folder Selection

默認(rèn)的是在sec/main/java目錄下,應(yīng)該改為src/test/java目錄下,之前說過,單元測(cè)試的內(nèi)容都在test目錄下,點(diǎn)擊Ok

先不要著急點(diǎn)Finish,點(diǎn)擊Next,要測(cè)試所有的方法,點(diǎn)擊Select All->Finish


junit提示信息

點(diǎn)擊OK


項(xiàng)目目錄

此時(shí)可以看到,單元測(cè)試已經(jīng)添加成功

測(cè)試類建好后,先要配置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ù)庫(kù)連接池是否OK等配置

要測(cè)試SeckillDao接口,就要先注入SeckillDao,直接實(shí)例化

//注入DAO實(shí)現(xiàn)類依賴
@Autowiredprivate SeckillDao seckillDao;

視頻上使用的是@Resource注解,會(huì)報(bào)錯(cuò),找不到這個(gè)類,我也折騰了半天,索性直接用@Autowired注解

先測(cè)試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ò)信息

context標(biāo)簽錯(cuò)誤
Class not found org.seckill.dao.SeckillDaoTest
java.lang.ClassNotFoundException: org.seckill.dao.SeckillDaoTest

一時(shí)間找不到頭緒,看到有人說可能是maven的配置問題,有些依賴沒配置上,我就按照給出的信息


jar包問題

顯示哪個(gè)jar包有問題,就刪哪個(gè),然后讓maven自己下載,但是刪了一個(gè)又報(bào)錯(cuò)另一個(gè),加上下載速度慢,又是大半天浪費(fèi)了

然后腦子一抽,索性把a(bǔ)pache-maven-3.3.9.m2\repository目錄下的依賴全刪了,就這樣刪了又下,改版本,下了又刪,兩天時(shí)間就這樣過去了

最后快崩潰了,決定還是按照視頻中的版本來,畢竟對(duì)新版本的特性不熟悉,萬一再出些幺蛾子,就該摔電腦了

然后一切又恢復(fù)到兩天前的樣子,@Resource依舊報(bào)錯(cuò),把@Resource替換成@Autowired就沒有報(bào)錯(cuò),然后開始測(cè)試

嚴(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

眼看著文件中沒有紅叉,但就是測(cè)試不通過,打斷點(diǎn)都不行,顯然是加載的時(shí)候就有問題,這里面不斷提到找不到一個(gè)類

java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy

上網(wǎng)找了半天,都說在pom.xml中沒有引入spring-jdbc的依賴,要是錯(cuò)誤都這么顯而易見,都皆大歡喜了,于是針對(duì)spring-jdbc,又是循環(huán)上面的過程,刪了又下,下了又刪,因?yàn)橐推渌鸖pring配置版本相同,就沒改版本,查了半天,下載了半天,依舊是找不到這個(gè)類

然后腦子又一抽,既然這個(gè)版本找不到,換個(gè)版本試試,也不能一下子就跳到新版本,萬一與其他依賴不兼容就崩潰了,所以選擇了4.1.7.RELEASE下個(gè)版本的最新版本4.2.9.RELEASE

結(jié)果就看到


junit測(cè)試通過

快速的點(diǎn)開控制臺(tái)


控制臺(tái)輸出信息

看到id為1000的數(shù)據(jù)輸出了,兩天半的時(shí)間,快被玩的就要砸電腦了...

接下來測(cè)試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報(bào)錯(cuò)信息

然后就看到熟悉的junit紅色進(jìn)度條和錯(cuò)誤信息

Caused by: org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [0, 1, param1, param2]

參數(shù)到SQL語(yǔ)句綁定的時(shí)候出了問題,找不到參數(shù)offset,可以回顧下在mapper目錄下的SQL語(yǔ)句配置文件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>

對(duì)比著接口中的方法定義

List<Seckill> queryAll(int offset, int limit);//SeckillDao接口中方法的定義

既然接口中和SQL語(yǔ)句中都寫的和明確,但是為什么綁定不了參數(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ù)對(duì)應(yīng)在哪個(gè)位置,這時(shí)就要對(duì)接口中的方法做些改動(dòng),MyBatis提供了一個(gè)注解@Param

List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

使用注解的方式,告訴MyBatis,第一個(gè)參數(shù)叫offset,對(duì)應(yīng)SQL語(yǔ)句中#{offset},然后再測(cè)試queryAll方法


queryAll方法輸出結(jié)果

接著測(cè)試最后一個(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);
    }

右鍵測(cè)試


reduceNumber方法錯(cuò)誤信息

依然是上面的錯(cuò)誤,修改接口中的方法即可


reduceNumber方法輸出結(jié)果

可以看看控制臺(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ù)庫(kù)的連接,但是這個(gè)jdbc連接沒有被Spring所托管,是從c3p0拿到的

然后控制臺(tái)還輸出了SQL語(yǔ)句

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)行減庫(kù)存的操作呢?
因?yàn)樵缭趧?chuàng)建數(shù)據(jù)庫(kù),插入數(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語(yǔ)句

SeckillDao接口的測(cè)試就完成了

2.SuccessKilledDao接口測(cè)試

使用Eclipse,根據(jù)上面的步驟,建立SuccessKilledDao接口的測(cè)試類

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SuccessKilledDaoTest

同樣在類上面添加兩個(gè)注解,Spring和junit整合的注解,然后是告訴Spring配置文件的位置

給這個(gè)測(cè)試類注入SuccessKilledDao

    @Autowired
    private SuccessKilledDao successKilledDao;

首先是insertSuccessKilled方法,先看看接口中方法的定義

int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);

傳遞的是多個(gè)參數(shù),所以依舊需要改動(dòng),使用MyBatis的@Param注解

根據(jù)方法的定義,就可以寫測(cè)試類了

@Test
    public void testInsertSuccessKilled() {
        long id = 1000L;
        long phone = 13512345678L;
        int insertCount = successKilledDao.insertSuccessKilled(id, phone);
        System.out.println("insertCount = " + insertCount);
    }
insertSuccessKilled方法輸出結(jié)果

返回1,說明成功插入信息


success_killed表中的信息

之前說過,這個(gè)方法可以防止用戶重復(fù)秒殺,所以可以不改變參數(shù),再執(zhí)行一次


insertSuccessKilled方法輸出結(jié)果

可以看到返回值是0,說明沒有執(zhí)行insert語(yǔ)句

這里還有點(diǎn)小問題

`state` tinyint NOT NULL DEFAULT -1 COMMENT '狀態(tài)標(biāo)識(shí): -1:無效  0:成功  1:已付款  2:已發(fā)貨',

在開始的建表語(yǔ)句的時(shí)候,定義了state屬性,是狀態(tài)標(biāo)識(shí),既然能成功執(zhí)行insertSuccessKilled方法,說明可以插入數(shù)據(jù),那么state應(yīng)該是0,所以要改動(dòng)一下SQL語(yǔ)句

    <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);

由于之前考慮的不周到,這條語(yǔ)句還要有些改動(dòng)

因?yàn)镾eckill與SuccessKilled是一對(duì)多的關(guān)系,一個(gè)秒殺商品對(duì)應(yīng)多個(gè)成功秒殺記錄,那么想要查詢某個(gè)人的秒殺記錄的時(shí)候,上面的語(yǔ)句就行不通了,所以要添加一個(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對(duì)象


queryByIdWithSeckill方法輸出結(jié)果

終于,所有的DAO層的工作已經(jīng)完成了

六、DAO層編碼后的一些思考

回顧從最初的創(chuàng)建數(shù)據(jù)庫(kù)開始,到設(shè)計(jì)接口、編寫SQL語(yǔ)句、各種配置文件,中間沒有寫一行邏輯代碼,DAO層的工作實(shí)際上演變?yōu)榱?* 接口設(shè)計(jì)+SQL編寫+配置文件 **,好處就是源代碼和SQL進(jìn)行了分離,方便Review,而DAO拼接等邏輯在Service層完成

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容