【SSM 项目:Java 高并发秒杀 API (一)DAO 层】2 基于 MyBatis 实现 DAO & MyBatis 整合 Spring & 单元测试

5. 基于 MyBatis 实现 DAO

5.1 回顾 MyBatis 实现 DAO 理论

在这里插入图片描述
MyBatis 特点:
参数:我们提供, 对象、实体等类型
SQL:我们自己写,灵活性高, 这样可以充分发挥我们 SQL 的技巧
Entity/List:底层通过 JDBC 把最终的结果(即 Entity/List)做封装,返回
在这里插入图片描述
MyBatis 怎么用:

  1. SQL 写在哪

    XML 提供 SQL(推荐)
    注解提供 SQL(复杂 SQL 拼接逻辑时,注解处理起来比较繁琐)

  2. 如何实现 DAO 接口

    Mapper 自动实现 DAO 接口(推荐, 这样我们只需关注 SQL 语句的编写, 如何设计 DAO 接口)
    API 编程方式实现 DAO 接口

5.2 配置 Mybatis

创建 MyBatis 全局配置文件 mybatis-config.xml
resources 下新建
mybatis-config.xml
xml 需要文档配置 dtd

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置全局属性 -->
    <!-- 此处写出 MyBatis 三个强调的配置,
     其他配置如数据库连接池,事务交给 spring 配置管理,整合 mybatis 和 spring 时再讲解-->
    <settings>
        <!-- 使用 jdbc 的 getGenetatedKeys 获取数据库自增主键值-->
        <setting name="useGenerateKeys" value="true"/>
        <!-- 使用列别名替换列名 默认:true
         select name as title from table
         -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 开启驼峰命名转化:Table(create_time) -> Entity(createTime)-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

5.3 新建 mapper 目录

resources 下新建
存放 mybatis SQL 的映射 mapper

mybatis 实现 DAO
命名规范: DAO 名是什么,mapper 映射里的 xml 就是什么

5.3.1 SeckillDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SeckillDao">
    <!-- 目的:为 DAO 接口方法提供 sql 语句配置-->
    <!-- id 就是方法名-->
    <!-- 传参数时 用 #{}-->
    <!-- parameterType:参数类型,多个参数时不用写-->
    <!-- resultType:返回值类型,List<Seckill> 写泛型里的即可-->
    <!-- XML 中不允许有 <= ,因为关键字冲突,应写成 <![CDATA[ <= ]]>-->
    
    <update id="reduceNumber">
        <!-- 具体 sql -->
        update
            seckill
        set
            number = number - 1
        where seckill_id = #{seckillId}
        and start_time <![CDATA[ <= ]]> #{killTime}
        and end_time >= #{killTime}
        and number > 0
    </update>
    <select id="queryById" resultType="Seckill" parameterType="long">
        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>

5.3.2 SuccessKilledDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SuccessKilledDao">
    <insert id="insertSuccessKilled">
        <!-- SQL 技巧:防止主键冲突报错 ignore-->
        insert ignore into success_killed(seckill_id, user_phone, state)
        values (#{seckillId}, #{userPhone}, 0)
    </insert>
    <select id="queryByIdWithSeckill" resultType="SuccessKilled" parameterType="long">
        <!-- 根据 id 查询 SuccessKilled 并携带秒杀产品对象实体 -->
        <!-- 如何告诉 MyBatis 把结果映射到 SuccessKilled 同时映射 seckill 属性 -->
        <!-- MyBatis 最大特点:可以自由控制 SQL -->
        select
            sk.seckill_id,
            sk.user_phone,
            sk.create_time,
            sk.state,
            s.seckill_id as "seckill.seckill_id",
            s.name as "seckill.name",
            s.number as "seckill.number",
            s.start_time as "seckill.start_time",
            s.end_time as "seckill.end_time",
            s.create_time as "seckill.create_time"
        from success_killed sk
        inner join seckill s on sk.seckill_id = s.seckill_id
        where sk.seckill_id = #{seckillId} and sk.user_phone = #{userPhone}
    </select>
</mapper>

6. Spring 整合 MyBatis

resources 下 -> 新建 spring 目录 -> 新建 spring-dao.xml 文件

6.1 spring-dao.xml

放所有 DAO 相关的配置

<?xml version="1.0" encoding="UTF-8"?>
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置整合 MyBatis 过程 -->

    <!-- 1. 配置数据库相关参数 properties 的属性:${url} -->
    <!-- jdbc.properties报红:说明没有这个文件,在 resources 下新建, 这是 jdbc 的配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" ignore-unresolvable="true"/>

    <!-- 2. 数据库连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 配置连接池属性 -->
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- c3p0 连接池的私有属性(根据经验与项目场景调整,此处为高并发,防止线程被锁住)-->
        <property name="maxPoolSize" value="30"/>
        <property name="minPoolSize" value="10"/>
        <!-- 关闭连接后不自动 commit,c3p0 autoCommitOnClose 属性默认就是 false,此处写出来强调一下 -->
        <property name="autoCommitOnClose" value="false"/>
        <!-- 获取连接超时时间 -->
        <property name="checkoutTimeout" value="1000"/>
        <!-- 当获取连接失败重试次数 -->
        <property name="acquireRetryAttempts" value="2"/>
    </bean>

    <!-- 此处为真正的 MyBatis 和 Spring  整合的配置 -->

    <!-- 约定大于配置 -->
    <!-- 3. 配置 SqlSessionFactory 对象 (MyBatis 最重要的 API) -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置 MyBatis 全局配置文件: mybatis-config.xml -->
        <!-- classpath 指 java 和 resources 下的文件 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 扫描 entity 包, 使用别名; 多个包可在 org.seckill.entity 后用 ; 隔开 -->
        <property name="typeAliasesPackage" value="org.seckill.entity"/>
        <!-- 扫描 sql 配置文件: mapper 需要的 xml 文件 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 4. 配置扫描 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>


</beans>

6.2 jdbc 配置文件 jdbc.properties

注:URL后面的多个参数没有空格

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=round
jdbc.username=root
jdbc.password=123456

7. 单元测试

验证 Dao 的实现 以及 Dao MyBatis 编写 以及 Spring 整合是否 OK

DAO 层单元测试 :SQL 语句、接口定义、数据绑定

junit 测试
一个快捷导测试类的方法:
DAO 接口里面右键,选择 go to, 再点 test, 然后 creat new test

快捷键 ctrl + shift + t

在这里插入图片描述
在这里插入图片描述

7.1 SeckillDaoTest.java

package org.seckill.dao;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.entity.Seckill;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;

/**
 *  配置 spring 和 junit 整合, junit 启动时加载 springIOC 容器
 *  spring-test, junit
 *
 */

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

public class SeckillDaoTest {

    // 注入 Dao 实现类依赖
    @Resource
    private SeckillDao seckillDao;

    @Test
    public void testQueryById() throws Exception {
        long id = 1000;
        Seckill seckill = seckillDao.queryById(id);
        System.out.println(seckill.getName());
        System.out.println(seckill);
    }
    /**
    * 1000 元秒杀 iphone6
    Seckill
     {seckillId=1000,
     name='1000 元秒杀 iphone6',
     number=100,
     startTime=Sun Nov 01 00:00:00 GMT+08:00 2015,
     endTime=Mon Nov 02 00:00:00 GMT+08:00 2015,
     createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     */
    @Test
    public void testQueryAll() throws Exception {
        List<Seckill> seckills = seckillDao.queryAll(0, 100);
        for(Seckill seckill : seckills) {
            System.out.println(seckill);
        }
    }
    /**
     * Seckill{seckillId=1000, name='1000 元秒杀 iphone6', number=100, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1001, name='500 元秒杀 ipad2', number=200, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1002, name='300 元秒杀小米 4', number=300, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1003, name='200 元秒杀红米 note', number=400, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}*/
    @Test
    public void testReduceNumber() throws Exception {
        /**
         * update seckill set number = number - 1
         * where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0
         * Parameters: 1000(Long), 2021-12-18 19:45:34.369(Timestamp), 2021-12-18 19:45:34.369(Timestamp)
         * Updates: 0(因为此次测试的 killTime 不在秒杀时间内)
         */
        Date killTime = new Date();
        int updateCount = seckillDao.reduceNumber(1000L, killTime);
        System.out.println("updateCount=" + updateCount);
    }
}


7.1.1 测试 testQueryById() 方法

开始测试:
DAO 接口的方法上 右键 -> debug
在这里插入图片描述

7.1.1.1 错误一:java 非法字符

错误一:【IDEA 错误(一)】错误:(1, 1) java: 非法字符: ‘\ufeff‘ & 错误:(1, 10) java: 需要class, interface或enum 解决方案

7.1.1.2 错误二:MySQL 错误 及 JDBC 错误

在这里插入图片描述

1. 小技巧
  1. 将错误复制到上方 注释 (便于查看)
    在这里插入图片描述
2. 错误解析
  1. 一般 spring 报出的错误 上面是 spring 初始化错误

  2. 下面是告诉我们 出现哪些错误 -> 所以我们一般从下往上看错误信息 即 cause by 后的内容

    单元测试出错,首先检查 Caused by ,然后检查配置文件以及代码是否有问题。
    (at 里是线程的运行轨迹)

遇到错误不要怕,不要放弃,复制错误信息 Google 一下 或者 B 站看看

1.【SSM 错误 1】Could not resolve placeholder ‘driver‘ in string value “${driver}“
2.【SSM 错误 2】org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection
3. MySQL 错误【三】 Navicat / IDEA 中文乱码 & Navicat 编码不一致(完美解决)

最终输出:

   @Test
    public void testQueryById() throws Exception {
        long id = 1000;
        Seckill seckill = seckillDao.queryById(id);
        System.out.println(seckill.getName());
        System.out.println(seckill);
    }
    /**
    * 1000 元秒杀 iphone6
    Seckill
     {seckillId=1000,
     name='1000 元秒杀 iphone6',
     number=100,
     startTime=Sun Nov 01 00:00:00 GMT+08:00 2015,
     endTime=Mon Nov 02 00:00:00 GMT+08:00 2015,
     createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     */

7.1.2 测试 testQueryAll() 方法

@Test
    public void testQueryAll() throws Exception {
        List<Seckill> seckills = seckillDao.queryAll(0, 100);
        for(Seckill seckill : seckills) {
            System.out.println(seckill);
        }
    }
7.1.2.1 错误三:SQL 绑定时 参数未找到
Caused by: org.apache.ibatis.binding.BindingException:
Parameter 'offset' not found. Available parameters are [0, 1, param1, param2]

在这里插入图片描述
错因分析:
在这里插入图片描述
解决方法:

    // java 编程语言的问题:
    // java 没有保存形参的记录:queryAll(int offset, int limit) ->java 运行时参数变为 queryAll(arg0,arg1)
    // 一个参数时没有问题,当有多个参数时要告诉 MyBatis 哪个参数叫什么名字 这样在 xml 里通过 #{} 提取参数时 MyBatis 才能找到具体值
    // 具体做法:修改接口 -> 增加 @Param 注解 说明形参是什么
    List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

运行结果:

@Test
    public void testQueryAll() throws Exception {
        List<Seckill> seckills = seckillDao.queryAll(0, 100);
        for(Seckill seckill : seckills) {
            System.out.println(seckill);
        }
    }
    /**
     * Seckill{seckillId=1000, name='1000 元秒杀 iphone6', number=100, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1001, name='500 元秒杀 ipad2', number=200, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1002, name='300 元秒杀小米 4', number=300, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     * Seckill{seckillId=1003, name='200 元秒杀红米 note', number=400, startTime=Sun Nov 01 00:00:00 GMT+08:00 2015, endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}
     */

7.1.3 测试 testReduceNumber() 方法

@Test
    public void testReduceNumber() throws Exception {
        /**
         * update seckill set number = number - 1 
         * where seckill_id = ? and start_time <= ? and end_time >= ? and number > 0 
         * Parameters: 1000(Long), 2021-12-18 19:45:34.369(Timestamp), 2021-12-18 19:45:34.369(Timestamp)
         * Updates: 0(因为此次测试的 killTime 不在秒杀时间内)
         */
        Date killTime = new Date();
        int updateCount = seckillDao.reduceNumber(1000L, killTime);
        System.out.println("updateCount=" + updateCount);
    }

7.2 SuccessKilledDaoTest.java

package org.seckill.dao;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.entity.SuccessKilled;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import static org.junit.Assert.*;

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

    @Resource
    private SuccessKilledDao successKilledDao;
    @Test
    public void testInsertSuccessKilled() throws Exception{
        long id = 1001L;
        long userPhone = 18742519888L;
        int insertCount = successKilledDao.insertSuccessKilled(id, userPhone);
        System.out.println("insertCount=" + insertCount);
    }
    /**
     * 第一次:insertCount=1
     * 第一次:insertCount=0
     * 重复插入不了,因为联合主键为 PRIMARY KEY (`seckill_id`, `user_phone`) 是唯一主键
     */

    @Test
    public void testQueryByIdWithSeckill() throws Exception{
        long id = 1001L;
        long userPhone = 18742519888L;
        SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, userPhone);
        System.out.println(successKilled);
        System.out.println(successKilled.getSeckill());
    }
    /**
     * SuccessKilled{skillId=0,
     * userPhone=18742519888, 
     * state=0, 
     * createTime=Sat Jan 01 00:00:00 GMT+08:00 1}
     * Seckill{seckillId=1001, 
     * name='500 元秒杀 ipad2', 
     * number=200, 
     * startTime=Sun 
     * Nov 01 00:00:00 GMT+08:00 2015, 
     * endTime=Mon Nov 02 00:00:00 GMT+08:00 2015, 
     * createTime=Sat Dec 18 13:48:26 GMT+08:00 2021}*/
}

7.2.1.1 错误四:Value ‘0000-00-00 00:00:00’ can not be represented as java.sql.Timestamp

MySQL 错误【四】Value ‘0000-00-00 00:00:00’ can not be represented as java.sql.Timestamp

慕课源码https://github.com/yoyo185644/seckill

8. 小结

目前 秒杀 API 所有 DAO 层的工作开发已完成

  • 数据库表设计
  • 实体类的编写
  • DAO 接口设计
  • MyBatis 实现接口
  • Spring 整合 MyBatis
  • 所有接口对应的单元测试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心海非海_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值