Java Webjava专题

高并发秒杀API(二)

2017-01-05  本文已影响309人  MOVE1925

前言

本篇将完成DAO层的设计与开发,包括:

一、数据库设计与编码

打开Eclipse,在src\main下建立一个文件夹sql,用于存放建表语句,新建一个SQL文件schema.sql,先创建一个秒杀商品的库存表

-- 数据库初始化脚本

-- 创建数据库
CREATE DATABASE seckill;

-- 使用数据库
USE seckill;

--创建秒杀库存表
CREATE TABLE seckill(
`seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
`name` varchar(120) NOT NULL COMMENT '商品名称',
`number` int NOT NULL COMMENT '库存数量',
`start_time` timestamp NOT NULL COMMENT '秒杀开始时间',
`end_time` timestamp NOT NULL COMMENT '秒杀结束时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
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,再单独对start_time、end_time、create_time三列单独建立索引,最后显式的设置MySQL引擎为InnoDB、自增主键初始值设置为1000、编码方式为utf8,并添加注释

** MySQL默认的有很多引擎,只有InnoDB支持事务 **

可以插入几条数据

-- 初始化数据
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');

建立秒杀成功明细表,记录秒杀成功的用户信息和商品信息

-- 秒杀成功明细表
-- 用户登录认证相关的信息
CREATE TABLE success_killed(
`seckill_id` bigint NOT NULL COMMENT '秒杀商品id',
`user_phone` bigint NOT NULL COMMENT '用户手机号',
`state` tinyint NOT NULL DEFAULT -1 COMMENT '状态标识: -1:无效  0:成功  1:已付款  2:已发货',
`create_time` timestamp NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone),/*联合主键 防止用户重复秒杀*/
key idx_create_time(create_time)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

create_time就是秒杀成功的时间

因为id和phone可以唯一确定一个用户,所以这里要用到联合主键,防止用户重复秒杀一个商品,当然以后也可以为此做过滤

数据库的设计完成了,可以在控制台或者数据库管理工具输入上述SQL语句


创建数据库

二、DAO层相关接口编码

先在java目录下建立两个包:

在org.seckill.entity包下新建实体类Seckill,对应数据库中的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方法,并复写toString方法

同样在org.seckill.entity包下新建实体类SuccessKilled,对应数据库中的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方法,并复写toString方法

private Seckill seckill;

这里实例化了一个Seckill类的对象,因为当用户成功秒杀一个商品时,可能需要完全拿到Seckill的实体

接着在org.seckill.dao包下新建接口SeckillDao,因为在数据库中seckill表记录的是秒杀商品的库存,所以当用户秒杀成功时,应该对数据库进行操作,也就是减库存

    /**
     * 减库存
     * @param seckillId
     * @param killTime
     * @return 返回受影响的行数
     */
    int reduceNumber(long seckillId, Date killTime);

还可以查询秒杀库存表的信息

    /**
     * 根据id查询秒杀对象
     * @param seckillId 秒杀商品id
     * @return
     */
    Seckill queryById(long seckillId);
    
    /**
     * 根据偏移量查询秒杀商品列表
     * @param offset 初始位置
     * @param limit 查询个数
     * @return
     */
    List<Seckill> queryAll(int offset, int limit);

偏移量就是用户可以设置初始位置offset,查询limit个数据

在org.seckill.dao包下新建接口SuccessKilledDao,当有一个用户在规定时间内成功秒杀一个商品时,进行记录,并且可以根据id查询相应的信息

    /**
     * 插入购买明细,可过滤重复
     * @param seckillId
     * @param userPhone
     * @return 返回受影响的行数,返回0表示没有插入数据
     */
    int insertSuccessKilled(long seckillId, long userPhone);
    
    /**
     * 根据id查询SuccessKilled并携带Seckill实体
     * @param seckill
     * @return
     */
    SuccessKilled queryByIdWithSeckill(long seckillId);

对于insertSuccessKilled方法,因为id和phone能唯一确定一个用户,所以当有重复出现时,不满足条件,insert语句不执行,返回0
** 如何设置条件,体现在SQL语句的书写,SQL语句写在下面要用到的MyBatis的xml文件中 **

至此,数据库对应的实体类以及DAO层的接口完成了,而且不用写接口的实现类,因为MyBatis把这些工作都承担了

那么这里就可以对DAO层有个初步的了解:
** DAO层提供了一些接口,这些接口是数据库对应的实体类(即Seckill类和SuccessKilled类)对数据库各种操作(例如:减库存、记录用户信息等)而封装的接口 **

三、基于MyBatis实现DAO层接口

Mybatis框架的作用

数据库与项目之间的映射之前已经实现了,数据库中的表对应org.seckill.entity包下的实体类,数据库中的列对应这些类中的属性,而这些对象要操作数据库,需要中间的映射过程,jdbc、MyBatis、Hibernate等都是工作在这一层,把数据库中的数据映射到对象中,并通过方法,操作数据库

在DAO层,我们已经写好了接口和方法,但是没有实现类,如果使用jdbc,就要手动的拿到数据库的连接,也要有实现接口的实现类,所以使用成熟的框架可以减少工作量,后期容易维护等许多好处

这里使用MyBatis,MyBatis对实现DAO层接口提供了两种方法:

显而易见,大部分都是选择自动实现DAO层接口,这种方法只需设计接口,不需要写实现类,通过配置MyBatis的xml文件,写好SQL语句,其他的工作MyBatis都会自动完成

1.MyBatis全局配置

先在src\main\resources下建立一个MyBatis全局的配置文件mybatis-conf.xml,再新建一个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">

将这些内容复制到xml文件中,这些示例都可以在MyBatis官网上的参考文档中找到

然后配置一些属性

<configuration>
    <!-- 配置全局属性 --> 
    <settings>
    
        <!-- 使用jdbc的getGenerateKeys获取数据库自增主键值 -->
        <setting name="useGeneratedKeys" value="true"/>
        
        <!-- 使用列别名替换列名 默认为true -->
        <setting name="useColumnLabel" value="true"/>
        
        <!-- 开启驼峰命名转换:Table(create_time) -> Entity(createTime) -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        
    </settings>
</configuration>

使用列别名替换列名,MyBatis默认为true,MyBatis会自动的识别出列别名对应哪个列名,并赋值到entity实体属性中

前面提到,要实现DAO层的接口可以使用MyBatis的mapper机制,为DAO接口方法提供SQL语句配置,所以在mapper文件夹下创建相应接口的配置文件SeckillDao.xml和SuccessKilledDao.xml

<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">

同样,这些内容都要添加到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标签中的属性,namespace是对这个mapper的命名,也就是对这个xml文件的命名,这个命名必须在mapper目录下唯一,因为真正的项目中,mapper下的xml文件有很多,如果命名不唯一,MyBatis就不知道要调用哪个xml文件了,一般都是包名.接口名

接着逐个分析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>

因为要实现SeckillDao接口中的减库存的方法,所以使用update语句,id必须在该xml文件下唯一,一般为方法名

int reduceNumber(long seckillId, Date killTime);//SeckillDao接口中定义的方法

update标签中还有parameterType属性,这里可以不用写,MyBatis可以自动识别
where后面有些限制条件,秒杀成功的时间要在规定时间内,要晚于开始时间,早于结束时间,否则update语句不会执行,当库存小于等于0时,也不执行update语句,数据返回类型为int,表示受影响的行数

至于下面这句

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

w3school上有详细介绍:

术语 CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data)。
在 XML 元素中,"<" 和 "&" 是非法的。
"<" 会产生错误,因为解析器会把该字符解释为新元素的开始。
"&" 也会产生错误,因为解析器会把该字符解释为字符实体的开始。
某些文本,比如 JavaScript 代码,包含大量 "<" 或 "&" 字符。为了避免错误,可以将脚本代码定义为 CDATA。
CDATA 部分中的所有内容都会被解析器忽略。
CDATA 部分由 "<![CDATA[" 开始,由 "]]>" 结束:

** 如果xml文件中仅有"<"和"&",还是建议把它们替换为实体引用 **

接着写完实现其他方法的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方法实质上是select查询语句,resultType返回的类型是Seckill类,因为自定义的类不在java.lang包下,所以一般是包名.类名,但是后面有方法可以省略包名,这里就只写类名

Seckill queryById(long seckillId);//SeckillDao接口中定义的方法

parameterType为long类型,因为已经开启了驼峰转换,所以可以不适用as进行列名转换

最后是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>

多个参数的话,可以不用给parameterType,结果按降序排列

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忽略报错 insert不执行 返回0 -->
        insert ignore into success_killed(seckill_id,user_phone)
        values (#{seckillId},#{userPhone})
    </insert>
    
    <select id="queryByIdWithSeckill" resultType="SuccessKilled">
        <!-- 根据id查询SuccessKilled并携带Seckill实体 -->
        <!-- 如何告诉Mybatis把结果映射到SuccessKilled同时映射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目录下有个schema.sql文件,里面是建表语句,在建立success_killed表的时候设置了一个联合主键,是防止用户重复秒杀的

PRIMARY KEY(seckill_id,user_phone)

所以id和phone只要有一个重复,insert语句就会报错,对于这种错误,其实只要不执行insert即可,不需要每次都报错,所以使用ignore关键字,当有主键冲突时,忽略报错,insert语句不会执行,结果返回0,说明没有插入数据

对于queryByIdWithSeckill方法

    <select id="queryByIdWithSeckill" resultType="SuccessKilled">
        <!-- 根据id查询SuccessKilled并携带Seckill实体 -->
        <!-- 如何告诉Mybatis把结果映射到SuccessKilled同时映射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>

首先要明确的是这个方法的作用,是根据id查询SuccessKilled并携带Seckill实体

SuccessKilled queryByIdWithSeckill(long seckillId);//SuccessKilledDao接口中定义的方法

返回SuccessKilled类型,在这个类中,实例化了Seckill类

from success_killed sk
inner join seckill s on sk.seckill_id = s.seckill_id
where sk.seckill_id = #{seckillId}

from success_killed表,再使用内连接的方式使seckill表加入进来,on后面表示两个表通过相同的id进行连接,id的值为传进来的参数seckillId的值
在MyBatis中可以忽略as关键字

那么如何告诉Mybatis把结果映射到SuccessKilled同时映射Seckill属性,首先可以得到sk表即success_killed表中的内容

  sk.seckill_id,
  sk.user_phone,
  sk.create_time,
  sk.state,

sk.seckill_id虽然使用了别名,** 但是MyBatis会忽略别名 ,所以MyBatis视为从sk表中的seckill_id列取数据,再返回数据到Java, 因为在MyBatis全局配置文件中开启了驼峰命名转换 **,所以seckill_id就变成了seckillId,赋值给相应的变量,这就是使用框架的好处

取到了数据,映射到了SuccessKilled中,又怎么同时映射Seckill属性呢?
在SuccessKilled类中,** 直接实例化了Seckill类 **,并生成了getter和setter方法


SuccessKilled类中的实例化Seckill类

success_killed和seckill两个表又通过内连接的方式进行了连接,所以可以直接在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会忽略别名,所以这里要在后面表明,这些列是来自哪个表的,这种写法实际是OGNL表达式,据说在Struts上很常见,但是在MyBatis的xml文件中也经常用到,所以还是要多了解下

到这里,MyBatis实现DAO层接口完成了

四、MyBatis与Spring整合

在src\main\resources\spring\下新建一个xml文件spring-dao.xml,所有的DAO层配置都放在该文件中,关于配置文件的一些信息 在Spring官网上可以找

Spring官网
在Spring Projects下面可以找到Spring Framework,选择版本,我在pom.xml文件中配置的MyBatis是4.3.5, 点击Reference,使用Ctrl+F搜索容器相关的
Spring官方文档
点击7.2.Container overview,找到相关配置文件的示例,把beans标签内的所有内容复制到项目的spring-dao.xml中 Spring官方文档

然后开始配置整合Mybatis

1.配置数据库相关参数

在src\main\resources\新建一个jdbc的配置文件jdbc.properties

db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql:///seckill?useUnicode=true&characterEncoding=utf8
db.user=root
db.password=

在练习的项目中可以使用数据库的root用户,实际工作中不建议使用,我的数据库没有设置密码,所以password为空

这是获取数据库的一些配置,在url中

jdbc:mysql:///seckill 等价于 jdbc:mysql://127.0.0.1:3306/seckill

数据库默认的端口是3306,可写可不写,最后跟的是数据库的名字

至于后面的一些参数

useUnicode=true&characterEncoding=utf8

使用Unicode编码,编码方式为utf8

有些版本的MySQL需要加密数据通道,同时需要检查服务器认证证书,在实际的工作中,这些根据实际情况配置,为了数据安全应该是尽可能的开启,作为练习的项目,就可以不用了

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.

如果有关于数据通道的加密和认证证书的问题,可以把下面的参数添加到jdbc的url后面

useSSL=true&verifyServerCertificate=false
在xml配置文件中配置数据库url时,要使用&的转义字符也就是& 

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

<!-- 配置数据库相关参数 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

这时,如果你的IDE跟我的Eclipse一样不靠谱的话,还要自己手动添加几行内容,从Spring上找的xml配置只是最基本的,这次用到了context标签的内容,就要把下面的内容添加到beans的标签内

<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">

最终的内容就是这些,跟从Spring官网上复制的相比,这次多了三条关于context的配置

2.配置数据库连接池

<!-- 配置数据库连接池 -->
<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"/>
    <!-- 关闭连接后不自动commit -->
    <property name="autoCommitOnClose" value="false"/>
    <!-- 获取连接超时时间 -->
    <property name="checkoutTimeout" value="1000"/> 
    <!-- 获取连接失败重试次数 -->
    <property name="acquireRetryAttempts" value="2"/>
</bean>

配置连接池的属性,结合jdbc.properties来写
关于#{}与${}的区别,#{}在MyBatis的SQL语句配置中有着预编译的效果MyBatis会先把#{}视为“?”,等到执行预编译语句的时候就会换成对应的参数,这些MyBatis都自动实现了,而${}是没有预编译效果,在spring-dao的配置中参数要拿来就能用,不需要预编译,所以这里用${}

关于c3p0的私有属性,这就是根据实际情况设置的,还有很多,这里就简单的设置几条

  <property name="maxPoolSize" value="30"/>
  <property name="minPoolSize" value="10"/>

这是设置连接池中连接个数的最大值和最小值,默认最大值为15、最小值为3

<!-- 关闭连接后不自动commit --> 
<property name="autoCommitOnClose" value="false"/>

对于autoCommitOnClose这个属性,就是当连接池的connection变为close的时候,实际是把连接对象放到池子当中,这个过程当中连接池会做相应的清理工作,如果把autoCommitOnClose设置为true,当我们调用close的时候会连接池会自动commit,不过本来这个属性c3p0默认为false,这里只是强调一下

<!-- 获取连接超时时间 --> 
<property name="checkoutTimeout" value="1000"/> 
<!-- 获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2"/>

对于连接超时的设置,在实际项目中很有必要,但是自己练习的时候可有可无,后面单元测试的时候,如果长时间都拿不到数据,每次都超时的时候,可以把这个属性注释掉,先测试程序能否正常运行

3.配置SqlSessionFactory对象

<!-- 配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 注入数据库连接池 -->
    <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>

前面两步,基本上每个项目都一样,从这开始,是MyBatis的配置,或者使用别的框架,对框架相应的配置
使用typeAliasesPackage可以扫描指定的包,之前说到的resultType可以直接使用类名,就是因为这个属性,如果有多个包要扫描的话,使用分号隔开

对于使用classpath引入配置文件


项目目录

在java和resources目录下都是classpath的范围

4.配置扫描DAO接口包

<!-- 配置扫描DAO接口包 动态实现DAO接口并注入到Spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 注入sqlSessionFactory -->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    <!-- 扫描DAO层下的接口 -->
    <property name="basePackage" value="org.seckill.dao"/>
</bean>

在这个bean中,没有id,因为其他配置不会调用这个bean

对于注入sqlSessionFactory

<!-- 注入sqlSessionFactory --> 
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

为什么使用BeanName的方法?
当MapperScannerConfigurer启动的时候,如果还没有加载jdbc.properties配置文件,这样拿到的dataSource就是错误的,因为#{}中的属性值还没有被替换,所以通过BeanName后处理的方式,当使用MyBatis的时候,才回去找对应的SQLSessionFactory对象,为了防止MapperScannerConfigurer提前初始化SQLSessionFactory

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

五、DAO层单元测试

1.SeckillDao接口测试

不同的IDE建立测试类的方式大同小异,下面是Eclipse的过程
在项目列表中,右键SeckillDao.java文件,选择New->Other,搜索junit,选择JUnit Test Case,点击Next


Eclipse中创建junit测试类 Eclipse中创建junit测试类

最上面可以选择junit版本,这里使用junit4

紧接着改动的是Source folder,点击右边的按钮


Source Folder Selection

默认的是在sec/main/java目录下,应该改为src/test/java目录下,之前说过,单元测试的内容都在test目录下,点击Ok

先不要着急点Finish,点击Next,要测试所有的方法,点击Select All->Finish


junit提示信息

点击OK


项目目录
此时可以看到,单元测试已经添加成功

测试类建好后,先要配置Spring和junit整合,为了是junit启动时加载SpringIOC容器

//Spring与junit整合
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit Spring配置文件的位置
@ContextConfiguration({"classpath:spring/spring-dao.xml"})

在SeckillDaoTest方法上添加两个注解,Spring提供了一个RunWith接口 是在runner下面的,使用RunWith就实现了junit启动时加载SpringIOC容器
还要告诉junit Spring配置文件的位置,使用ContextConfiguration注解,在加载SpringIOC容器的时候同时加载spring-dao.xml文件,验证Spring与MyBatis整合,数据库连接池是否OK等配置

要测试SeckillDao接口,就要先注入SeckillDao,直接实例化

//注入DAO实现类依赖
@Autowiredprivate SeckillDao seckillDao;

视频上使用的是@Resource注解,会报错,找不到这个类,我也折腾了半天,索性直接用@Autowired注解

先测试queryById方法

//Spring与junit整合
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit Spring配置文件的位置
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SeckillDaoTest {
    
    //注入DAO实现类依赖
    @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");
    }

}

刚开始因为@Resource注解的问题一直找不到解决的方法,同时还有别的报错信息

context标签错误
Class not found org.seckill.dao.SeckillDaoTest
java.lang.ClassNotFoundException: org.seckill.dao.SeckillDaoTest

一时间找不到头绪,看到有人说可能是maven的配置问题,有些依赖没配置上,我就按照给出的信息


jar包问题

显示哪个jar包有问题,就删哪个,然后让maven自己下载,但是删了一个又报错另一个,加上下载速度慢,又是大半天浪费了

然后脑子一抽,索性把apache-maven-3.3.9.m2\repository目录下的依赖全删了,就这样删了又下,改版本,下了又删,两天时间就这样过去了

最后快崩溃了,决定还是按照视频中的版本来,毕竟对新版本的特性不熟悉,万一再出些幺蛾子,就该摔电脑了

然后一切又恢复到两天前的样子,@Resource依旧报错,把@Resource替换成@Autowired就没有报错,然后开始测试

严重: 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

眼看着文件中没有红叉,但就是测试不通过,打断点都不行,显然是加载的时候就有问题,这里面不断提到找不到一个类

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

上网找了半天,都说在pom.xml中没有引入spring-jdbc的依赖,要是错误都这么显而易见,都皆大欢喜了,于是针对spring-jdbc,又是循环上面的过程,删了又下,下了又删,因为要和其他Spring配置版本相同,就没改版本,查了半天,下载了半天,依旧是找不到这个类

然后脑子又一抽,既然这个版本找不到,换个版本试试,也不能一下子就跳到新版本,万一与其他依赖不兼容就崩溃了,所以选择了4.1.7.RELEASE下个版本的最新版本4.2.9.RELEASE

结果就看到


junit测试通过

快速的点开控制台


控制台输出信息

看到id为1000的数据输出了,两天半的时间,快被玩的就要砸电脑了...

接下来测试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报错信息

然后就看到熟悉的junit红色进度条和错误信息

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

参数到SQL语句绑定的时候出了问题,找不到参数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语句中都写的和明确,但是为什么绑定不了参数?
原因就是** Java没有保存形参的记录 **,意味着在Java运行过程中

queryAll(int offset, int limit);等价于queryAll(arg0, arg1);

如果方法只有一个参数的话,就没关系,比如上面的queryById方法,所以当有多个参数的时候,就要告诉MyBatis,哪个参数对应在哪个位置,这时就要对接口中的方法做些改动,MyBatis提供了一个注解@Param

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

使用注解的方式,告诉MyBatis,第一个参数叫offset,对应SQL语句中#{offset},然后再测试queryAll方法


queryAll方法输出结果

接着测试最后一个方法reduceNumber,先看一下接口中方法的定义

  int reduceNumber(long seckillId, Date killTime);

传递两个参数,一个是long类型,一个是Date类型,返回int

    @Test
    public void testReduceNumber() throws Exception {
        Date killTime = new Date();
        int updateCount = seckillDao.reduceNumber(1000L, killTime);
        System.out.println("updateCount = " + updateCount);
    }

右键测试


reduceNumber方法错误信息

依然是上面的错误,修改接口中的方法即可


reduceNumber方法输出结果
可以看看控制台的输出,有利于理解整个运行过程
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连接池拿到了数据库的连接,但是这个jdbc连接没有被Spring所托管,是从c3p0拿到的

然后控制台还输出了SQL语句

update seckill set number = number - 1 where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0;

之前写的#{}都被MyBatis视为占位符“?”,这就是因为#{}具有预编译的功能
接着显示的是传递过去的参数,最后输出结果是0,为什么没有进行减库存的操作呢?
因为早在创建数据库,插入数据的时候,就已经设置了秒杀时间段

-- 初始化数据
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');

秒杀活动从1号开始,2号结束,被maven的依赖折腾后,已经是6号了,所以不在秒杀时间段内,没有执行update语句

SeckillDao接口的测试就完成了

2.SuccessKilledDao接口测试

使用Eclipse,根据上面的步骤,建立SuccessKilledDao接口的测试类

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

同样在类上面添加两个注解,Spring和junit整合的注解,然后是告诉Spring配置文件的位置

给这个测试类注入SuccessKilledDao

    @Autowired
    private SuccessKilledDao successKilledDao;

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

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

传递的是多个参数,所以依旧需要改动,使用MyBatis的@Param注解

根据方法的定义,就可以写测试类了

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

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


success_killed表中的信息

之前说过,这个方法可以防止用户重复秒杀,所以可以不改变参数,再执行一次


insertSuccessKilled方法输出结果
可以看到返回值是0,说明没有执行insert语句

这里还有点小问题

`state` tinyint NOT NULL DEFAULT -1 COMMENT '状态标识: -1:无效  0:成功  1:已付款  2:已发货',

在开始的建表语句的时候,定义了state属性,是状态标识,既然能成功执行insertSuccessKilled方法,说明可以插入数据,那么state应该是0,所以要改动一下SQL语句

    <insert id="insertSuccessKilled">
        <!-- 主键冲突:使用ignore忽略报错 insert不执行 返回0 -->
        insert ignore into success_killed(seckill_id,user_phone,state)
        values (#{seckillId},#{userPhone},0)
    </insert>

这样,再插入的数据的state就是0了

然后是queryByIdWithSeckill方法,先看方法的定义

SuccessKilled queryByIdWithSeckill(long seckillId);

由于之前考虑的不周到,这条语句还要有些改动

因为Seckill与SuccessKilled是一对多的关系,一个秒杀商品对应多个成功秒杀记录,那么想要查询某个人的秒杀记录的时候,上面的语句就行不通了,所以要添加一个参数

SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);

多了一个参数,所以还要加上@Param注解,同时,还要改动的地方是mapper目录下SuccessKilledDao.xml文件,找到与方法名相同的id

where sk.seckill_id = #{seckillId} and sk.user_phone = #{userPhone}

前面已经插入过一条成功秒杀的信息,所以还是用前面的数据

    @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());
    }

因为在SuccessKilled类中已经实例化了Seckill类,并生成了getter和setter方法,所以这里也可以取到Seckill对象


queryByIdWithSeckill方法输出结果

终于,所有的DAO层的工作已经完成了

六、DAO层编码后的一些思考

回顾从最初的创建数据库开始,到设计接口、编写SQL语句、各种配置文件,中间没有写一行逻辑代码,DAO层的工作实际上演变为了** 接口设计+SQL编写+配置文件 **,好处就是源代码和SQL进行了分离,方便Review,而DAO拼接等逻辑在Service层完成

上一篇下一篇

猜你喜欢

热点阅读