MyBatis 是一个优秀的 ORM(对象关系映射) 持久层框架.
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具,特点就是很灵活.
我们学习 MyBatis 主要学习两部分:
1. 配置 MyBatis 开发环境
2. 使用 MyBatis 模式和语法操作数据库
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()
因为我们还没有连接数据库,他是找不到路径的
这里一般我们会用多个配置文件,以便之后在多个平台运行(最少一般都会有两个)
像这种常用的,自己可以把它保存在能够经常拿到的地方,下次直接粘贴就好了.
# 配置数据库连接
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
# 配置 MyBatis xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
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 实现上面的接口
固定的格式,自己也可以先保存在能经常拿到的地方:
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 是基于 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);
}
}
左侧出现的小鸟是我添加的插件,点击小鸟他会自动帮我们找到对应的方法,非常的方便,可查
我们可以添加日志文件以便于我们操作之后排查错误
# 开启 MyBatis SQL 打印
logging:
level:
com:
example:
demo: debug
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
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);
}
1. 在 mapper(interface) 里面添加删除的代码声明
// 删除方法
public int del(@Param("id") Integer id);
2. 在 xml 中添加 < delete >标签和删除 sql 编写
<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);
}
1. 在 mapper(interface) 里面添加修改的代码声明
// 修改方法【根据id修改名称】
public int update(@Param("id") Integer id,
@Param("username") String username);
2. 在 xml 中添加 < update >标签和修改 sql 编写
<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);
}
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);
}
我们发现了执行这些操作以后他是会"污染"
我们的数据库的,那么如果我们只是想单纯的测试代码的正确性,这个时候就可以使用SpringBoot单元测试
2.添加单元测试的业务代码
断⾔:如果断⾔失败,则后⾯的代码都不会执⾏。
相关代码:
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);
}
}
我们不仅想看到受影响的行数,还想看到修改后的 id
1. mapper层接口:
// 添加用户,返回受影响的行数和自增 id
public int addGetId(UserInfo userInfo);
2. XML 文件:
<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);
}
先看 #{} 操作根据 id 查找用户
接下来${},其他代码不变
执行的 JDBC 代码:(针对 int 类型)
前者体现的预处理(预查询),后者体现的即时查询
我们在看看针对 String 类型参数:
先使用 #{} 根据用户名查找 用户
执行的 JDBC 代码:(针对 String 类型)
接下来${},其他代码不变
并没有加引号
像一些商品基本都要有这个排序功能(按价格从高到低,从低到高)我这里就按 id 来排序
当传递的是一个 SQL 关键字(SQL 命令)的时候,只能使用 ${},此时如果使用 #{} 就会认为传递的一个普通的值,而非 SQL 命令,所以执行就会报错
注意
${} 会有 SQL 注入问题,我们不得不使用 ${} 时,那么一定要在业务代码中,对传递的值进行安全验证
再看 ${}
他的 SQL 是这样的(已经恒等了):
他对应的 SQL:
这里他是不能直接使用 ${}.
可以使用mysql内置函数 concat()
来处理:
#{}
预处理,而 ${}
是直接替换#{}
适用于所有类型的参数匹配,但 ${}
只适用于数值类型#{}
性能高,并且没有安全问题: 但 ${}
存在 SQL 注入的安全问题在 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>
<select id="getUserById" resultMap="BaseMap">
select * from userinfo where id=#{id}
select>
⼀对⼀映射要使⽤
标签,具体实现如下(⼀篇⽂章只对应⼀个作者):
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层:
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>
用 @Slf4j
打印:
以上使⽤
标签,表示⼀对⼀的结果映射:
property 属性:
指定 Article 中对应的属性,即⽤户。
resultMap 属性:
指定关联的结果集映射,将基于该映射配置来组织⽤户数据。
columnPrefix 属性:
绑定⼀对⼀对象时,是通过columnPrefix+association.resultMap.column 来映射结果集字段。
association.resultMap.column是指 标签中 resultMap属性,对应的结果集映射中,column字段。
注意事项:column不能省略
跟一对一差不多,⼀对多需要使⽤
标签,⽤法和
相同,
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>
判断一个参数是否有值的,如果没值,那么就会隐藏 if 中 的 SQL
就像我们在填写一些注册信息的时候,有些项可以填也可以不填,有些就是必须要填.
举例:添加用户
1. mapper层:
// 添加用户,添加用户时 photo是非必传参数
public int add2(UserInfo userInfo);
2. XML:
<insert id="add2">
insert into userinfo(username,password
<if test="photo!=null">
,photo
if>
) values(#{name},#{password}
<if test="photo!=null">
,#{photo}
if>
)
insert>
4. 传值 photo 时:
最主要的作用就是 去除 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>
主要作用就是实现查询中的 where sql 替换的,他可以实现如果没有任何的查询条件,那么他可以因此查询中的 where sql,但是如果存在查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动的去除最前面
一个 and 字符
举例:根据 id 查找用户
1. mapper层:
// 根据用户 id 查询用户
public UserInfo getUserById(@Param("id") Integer id);
2. XML层:
<select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
if>
where>
select>
主要作用就是在进行修改操作时,配合 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>
作用主要就是对集合进行循环的
标签有如下属性:
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>