1. MyBatis 是什么?
MyBatis 是一个优秀的 ORM(对象关系映射) 持久层框架.
- 它⽀持⾃定义 SQL、存储过程以及⾼级映射。
- MyBatis 去除了⼏乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。
- MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具,特点就是很灵活.
我们学习 MyBatis 主要学习两部分:
1. 配置 MyBatis 开发环境
2. 使用 MyBatis 模式和语法操作数据库
2. 创建 MyBatis 项目
2.1 准备工作,创建数据库和数据表
mybatis 测试数据库:
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);
注: MySQL 5.7 以下版本 需要去掉 default now()
2.2 添加 MyBatis 相关依赖
添加完后不要急着启动项目测试:
因为我们还没有连接数据库,他是找不到路径的
2.3 配置数据库连接字符串 和 MyBatis(保存的 XML 的目录)
2.3.1 配置数据库的连接信息
这里一般我们会用多个配置文件,以便之后在多个平台运行(最少一般都会有两个)
像这种常用的,自己可以把它保存在能够经常拿到的地方,下次直接粘贴就好了.
# 配置数据库连接
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
username: 你的用户名
password: 你的密码
driver-class-name: com.mysql.cj.jdbc.Driver
# 当前运行的环境 (配置文件)
spring:
profiles:
active: dev
2.3.2 配置 MyBatis 的 XML 保存路径
# 配置 MyBatis xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
2.4 使用 MyBatis 的操作模式操作数据库
我们可以先看一下 MyBatis 整体流程:
2.4.1 添加代码
提前创好对应的包:
1. model 包下添加实体类
对应数据库添加:
import lombok.Data;
/**
* 用户实体类
*/
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
}
2. 在 mapper 包下定义接口(普通的接口)
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper //添加后才能跟 xml 文件对应起来
public interface UserMapper {
//根据 id 查询用户
public UserInfo getUserById(@Param("id") Integer id);
}
3. 创建 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="com.example.demo.mapper.UserMapper">
</mapper>
在 resources 的 mybatis 底下新建 UserMapper.xml:(xml 里的名字建议跟 mapper 包下的名字对应一样,这样容易我们观察)
MyBatis 执行过程:
MyBatis 是基于 JDBC 的
4. 添加 Service 层代码
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public UserInfo getUserById(Integer id) {
return userMapper.getUserById(id);
}
}
5. 添加 Controller 层代码
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getuserbyid")
public UserInfo getUserById(Integer id){
if(id == null) return null;
return userService.getUserById(id);
}
}
2.5 测试数据
2.6 添加插件 MyBatisX
左侧出现的小鸟是我添加的插件,点击小鸟他会自动帮我们找到对应的方法,非常的方便,可查
2.7 添加日志文件
我们可以添加日志文件以便于我们操作之后排查错误
# 开启 MyBatis SQL 打印
logging:
level:
com:
example:
demo: debug
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. MyBatis 增删改查
3.1 增添操作
1. 在 mapper (interface) 添加增添方法声明
//添加用户,返回受影响的行数
public int add(UserInfo userInfo);
2. 在 xml 中添加 < insert >标签和增添 sql 编写
<!-- 添加用户,返回受影响的行数 -->
<insert id="add">
insert into userinfo(username,password,photo) values(#{username},#{password},#{photo})
</insert>
3. service 层
public int add(UserInfo userInfo){
return userMapper.add(userInfo);
}
4. controller 层
@RequestMapping("/add")
public int add(UserInfo userInfo){
userInfo.setUsername("张三");
userInfo.setPassword("123");
userInfo.setPhoto("default.png");
return userService.add(userInfo);
}
5. 测试
3.2 删除操作
1. 在 mapper(interface) 里面添加删除的代码声明
// 删除方法
public int del(@Param("id") Integer id);
2. 在 xml 中添加 < delete >标签和删除 sql 编写
<!-- 根据用户 id 删除用户 -->
<delete id="del">
delete from userinfo where id=#{id}
</delete>
3. service 层
public int del(Integer id){
return userMapper.del(id);
}
4. controller 层
@RequestMapping("/del")
public int del(Integer id){
return userService.del(id);
}
5. 测试
3.3 修改操作
1. 在 mapper(interface) 里面添加修改的代码声明
// 修改方法【根据id修改名称】
public int update(@Param("id") Integer id,
@Param("username") String username);
2. 在 xml 中添加 < update >标签和修改 sql 编写
<!-- 根据用户 id 修改用户名称 -->
<update id="update">
update userinfo set username=#{username} where id=#{id}
</update>
3. service 层
public int update(Integer id,String username){
return userMapper.update(id,username);
}
4. controller 层
@RequestMapping("/update")
public int update(Integer id,String username){
return userService.update(id,username);
}
5. 测试
3.4 查找操作
1. 在 mapper(interface) 里面添加查找的代码声明
// 根据用户名来查询用户
public UserInfo getUserByUsername(@Param("username") String username);
一般是根据 id 来查找,因为用户名也可能重复
2. 在 xml 中添加 < select >标签和查找 sql 编写
<!-- 根据用户名查询用户 -->
<select id="getUserByUsername" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username}
</select>
3. service 层
public UserInfo getUserByUsername(String username){
return userMapper.getUserByUsername(username);
}
4. controller 层
@RequestMapping("/getuserbyusername")
public UserInfo getUserById(String username){
if(username == null) return null;
return userService.getUserByUsername(username);
}
5. 测试
3.5 汇总
1. controller 层
2. mapper 层
3. model 层
4. service 层
5. XML 文件
我们发现了执行这些操作以后他是会"污染"
我们的数据库的,那么如果我们只是想单纯的测试代码的正确性,这个时候就可以使用SpringBoot单元测试
4. SpringBoot单元测试
4.1 单元测试优点
- 可以非常简单,直观,快速的测试某一个功能是否正确
- 使用单元测试可以帮我们在打包的时候,发现一些问题,因为在打包之前,所有的单元测试必须通过,否则不能打包成功
- 使用单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改变的情况下,进行测试功能
4.2 单元测试的实现
4.2.1 准备工作: 确认项目中已经内置了测试框架
4.2.2 生成单元测试的类
在 mapper 包下:
4.2.3 配置单元测试的类添加 @SpringBootTest 注解,添加单元测试的业务代码
1. 添加注解
2.添加单元测试的业务代码
4.3 简单断言说明
断⾔:如果断⾔失败,则后⾯的代码都不会执⾏。
4.4 添加 @Transactional 注解执行回滚事务
我现在的数据库
执行修改操作:
修改成功:
在观察原数据库并没发生改变:
执行删除操作:
删除成功:
观察原数据库也没发生改变:
相关代码:
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
void getUserById() {
UserInfo userInfo = userMapper.getUserById(1);
System.out.println(userInfo);
}
// 在单元测试中添加此注解,表示在方法执行完之后回滚事务
@Transactional
@Test
void update() {
int result = userMapper.update(2, "张三");
Assertions.assertEquals(1, result);
}
@Transactional
@Test
void del() {
int result = userMapper.del(2);
System.out.println("受影响的行数:" + result);
Assertions.assertEquals(1, result);
}
@Transactional
@Test
void add() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("王五");
userInfo.setPassword("123");
userInfo.setPhoto("default.png");
int result = userMapper.add(userInfo);
System.out.println("添加的结果:" + result);
Assertions.assertEquals(1, result);
}
}
4.5 返回自增 id
我们不仅想看到受影响的行数,还想看到修改后的 id
1. mapper层接口:
// 添加用户,返回受影响的行数和自增 id
public int addGetId(UserInfo userInfo);
2. XML 文件:
<!-- 添加用户,返回受影响的行数和自增 id -->
<insert id="addGetId" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into userinfo(username,password,photo) values(#{username},#{password},#{photo})
</insert>
useGeneratedKeys:
这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据库内部⽣成的主键(⽐如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动递增字段),默认值:false。
keyColumn:
设置⽣成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称。
keyProperty:
指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回值或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称
3. 单元测试代码:
// @Transactional
@Test
void addGetId() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("老六");
userInfo.setPassword("123");
userInfo.setPhoto("default.png");
System.out.println("添加之前的 user id: "+userInfo.getId());
int result = userMapper.addGetId(userInfo);
System.out.println("受影响的行数: "+result);
System.out.println("添加之后的 user id: "+userInfo.getId());
Assertions.assertEquals(1,result);
}
注释掉了回滚:
4.6 一些查询操作
4.6.1 ${} 和 #{} 的区别演示
① 根据 id 查找用户
先看 #{} 操作根据 id 查找用户
执行的 JDBC 代码:(针对 int 类型的参数)
接下来${},其他代码不变
执行的 JDBC 代码:(针对 int 类型)
前者体现的预处理(预查询),后者体现的即时查询
② 根据用户名查找用户
我们在看看针对 String 类型参数:
先使用 #{} 根据用户名查找 用户
执行的 JDBC 代码:(针对 String 类型)
接下来${},其他代码不变
执行的 JDBC 代码:(针对 String 类型)
报错了,他的sql语句相当于是:
并没有加引号
③ 排序功能
像一些商品基本都要有这个排序功能(按价格从高到低,从低到高)我这里就按 id 来排序
先看 #{}
报错了:
他的 sql 相当于:
再看 ${}
当传递的是一个 SQL 关键字(SQL 命令)的时候,只能使用 ${},此时如果使用 #{} 就会认为传递的一个普通的值,而非 SQL 命令,所以执行就会报错
注意
${} 会有 SQL 注入问题,我们不得不使用 ${} 时,那么一定要在业务代码中,对传递的值进行安全验证
④ SQL 注入演示(${})
假设我们有一个登录功能:
先看 #{}
再看 ${}
他的 SQL 是这样的(已经恒等了):
⑤ 特殊查询: like 查询
根据名称进行模糊查询
先看 #{}
报错了:
他对应的 SQL:
这里他是不能直接使用 ${}.
可以使用mysql内置函数 concat()
来处理:
4.6.2 总结区别
- 定义不同:
#{}
预处理,而${}
是直接替换 - 使用不同:
#{}
适用于所有类型的参数匹配,但${}
只适用于数值类型 - 安全性不同:
#{}
性能高,并且没有安全问题: 但${}
存在 SQL 注入的安全问题
5. resultType VS resultMap
在 resultType中,如果我们在实体类中的字段和mysql中的字段不匹配
这样我们是查不到 对应的值的,这时候我们就可以使用 resultMap 来手动进行映射的处理
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<!-- 主键映射 -->
<id column="id" property="id"></id>
<!-- 普通属性映射 -->
<result column="username" property="name"></result>
</resultMap>
<!-- 根据用户 id 查询用户 -->
<select id="getUserById" resultMap="BaseMap">
select * from userinfo where id=#{id}
</select>
6. 多表查询
6.1 一对一查询
⼀对⼀映射要使⽤
<association>
标签,具体实现如下(⼀篇⽂章只对应⼀个作者):
我们在建一个文章实体类:
1. model层:
import lombok.Data;
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private String createtime;
private String updatetime;
private int uid;
private int rcount;
private int state;
private UserInfo userInfo;
}
多加了一个 UserInfo 类
2. mapper层:
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//根据文章 id 获取文章
public ArticleInfo getArticleById(@Param("id") Integer id);
}
3. 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">
<!-- namespace 要设置是实现接口的具体包名加类名 -->
<mapper namespace="com.example.demo.mapper.ArticleMapper">
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id column="id" property="id"></id>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="uid" property="uid"></result>
<result column="rcount" property="rcount"></result>
<result column="state" property="state"></result>
<association property="userInfo"
resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_"></association>
</resultMap>
<select id="getArticleById" resultMap="BaseMap">
select a.*,u.id u_id,u.username u_username,u.password u_password from articleinfo a left join userinfo u on
a.uid=u.id where a.id=#{id}
</select>
</mapper>
4. 单元测试代码:
用 @Slf4j
打印:
以上使⽤
<association>
标签,表示⼀对⼀的结果映射:
property 属性:
指定 Article 中对应的属性,即⽤户。
resultMap 属性:
指定关联的结果集映射,将基于该映射配置来组织⽤户数据。
columnPrefix 属性:
绑定⼀对⼀对象时,是通过columnPrefix+association.resultMap.column 来映射结果集字段。
association.resultMap.column是指 标签中 resultMap属性,对应的结果集映射中,column字段。
注意事项:column不能省略
6.2 一对多实现
跟一对一差不多,⼀对多需要使⽤ <collection>
标签,⽤法和 <association>
相同,
1. model层:
import lombok.Data;
import java.util.List;
/**
* 用户实体类
*/
@Data
public class UserInfo {
private Integer id;
private String name;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
private List<ArticleInfo> artlist;
}
2. mapper层:
// 查询用户及用户发表的所有文章,根据用户id
public UserInfo getUserAndArticleByUid(@Param("uid") Integer uid);
3. XML层:
<mapper namespace="com.example.demo.mapper.UserMapper">
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<!-- 主键映射 -->
<id column="id" property="id"></id>
<!-- 普通属性映射 -->
<result column="username" property="name"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="state" property="state"></result>
<collection property="artlist"
resultMap="com.example.demo.mapper.ArticleMapper.BaseMap"
columnPrefix="a_">
</collection>
</resultMap>
<select id="getUserAndArticleByUid" resultMap="BaseMap">
select u.*,a.id a_id,a.title a_title,a.content a_content,
a.createtime a_createtime,
a.updatetime a_updatetime from userinfo u left join articleinfo a
on u.id=a.uid where u.id=#{uid}
</select>
4. 单元测试代码:
7. 动态 SQL
7.1 if 标签
判断一个参数是否有值的,如果没值,那么就会隐藏 if 中 的 SQL
就像我们在填写一些注册信息的时候,有些项可以填也可以不填,有些就是必须要填.
简单语法:
举例:添加用户
1. mapper层:
// 添加用户,添加用户时 photo是非必传参数
public int add2(UserInfo userInfo);
2. XML:
<!-- 添加用户,添加用户时 photo是非必传参数 -->
<insert id="add2">
insert into userinfo(username,password
<if test="photo!=null">
,photo
</if>
) values(#{name},#{password}
<if test="photo!=null">
,#{photo}
</if>
)
</insert>
3. 不加 photo 的测试代码:
4. 传值 photo 时:
对应数据库值:
7.2 trim 标签
最主要的作用就是 去除 SQL 语句前后多余的某个字符,向我们经常会在最后添加的值多加一个逗号啥的
主要语法:
1. mapper层:
// 添加用户,其中 username、password、photo 都是非必传参数,
// 但至少会传递一个参数
public int add3(UserInfo userInfo);
2. XML层:
<insert id="add3">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name!=null">
username,
</if>
<if test="password!=null">
password,
</if>
<if test="photo!=null">
photo
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name!=null">
#{name},
</if>
<if test="password!=null">
#{password},
</if>
<if test="photo!=null">
#{photo}
</if>
</trim>
</insert>
3. 单元测试代码:
7.3 where 标签
主要作用就是实现查询中的 where sql 替换的,他可以实现如果没有任何的查询条件,那么他可以因此查询中的 where sql,但是如果存在查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动的去除最前面
一个 and 字符
举例:根据 id 查找用户
1. mapper层:
// 根据用户 id 查询用户
public UserInfo getUserById(@Param("id") Integer id);
注意:
这里只返回一个,我就把数据库保留了一份
2. XML层:
<!-- 根据 id 查询用户 -->
<select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
</if>
</where>
</select>
3. 单元测试代码:
4. 加上 and:
测试代码:
注意:
and 在前面不能在后面
7.4 set 标签
主要作用就是在进行修改操作时,配合 if 来处理非必传输的,它的特点就是会自动去除最后一个英文逗号
举例,修改数据:
1. mapper层:
int update2(UserInfo userInfo);
2. XML层:
<update id="update2">
update userinfo
<set>
<if test="name!=null">
username=#{name},
</if>
<if test="password!=null">
password=#{password},
</if>
<if test="photo!=null">
photo=#{photo}
</if>
</set>
where id=#{id}
</update>
3. 测试代码:
7.5 foreach 标签
作用主要就是对集合进行循环的
<foreach>
标签有如下属性:
collection:
绑定⽅法参数中的集合,如 List,Set,Map或数组对象
item:
遍历时的每⼀个对象
open:
语句块开头的字符串
close:
语句块结束的字符串
separator:
每次遍历之间间隔的字符串
举例:根据多个用户 id 删除用户:
1. mapper层:
int delIds(List<Integer> ids);
2. XML层:
<delete id="delIds">
delete from userinfo where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
方便观察,我们可以加几条数据:
3. 单元测试代码: