文章目录
5. 基于 MyBatis 实现 DAO
5.1 回顾 MyBatis 实现 DAO 理论
MyBatis 特点:
参数:我们提供, 对象、实体等类型
SQL:我们自己写,灵活性高, 这样可以充分发挥我们 SQL 的技巧
Entity/List:底层通过 JDBC 把最终的结果(即 Entity/List)做封装,返回
MyBatis 怎么用:
- SQL 写在哪
XML 提供 SQL(推荐)
注解提供 SQL(复杂 SQL 拼接逻辑时,注解处理起来比较繁琐) - 如何实现 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. 小技巧
- 将错误复制到上方 注释 (便于查看)
2. 错误解析
-
一般 spring 报出的错误 上面是 spring 初始化错误
-
下面是告诉我们 出现哪些错误 -> 所以我们一般从下往上看错误信息 即
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
- 所有接口对应的单元测试