目录
1.MyBatis 框架的搭建
1.1 创建数据库和表
1.2 添加 MyBatis 依赖
1.3 设置 MyBatis 配置
1.3.1 设置数据库的连接信息
1.3.2 设置 XML 保存路径和命名格式
1.4 根据 MyBatis 写法完成数据库得操作
1.4.1 定义接口
1.4.2 使用 XML 实现接口
2.MyBatis查询操作
2.1 单表查询
2.1.1 参数占位符 #{ } 和 ${ }(常见面试问题)
从上边的事例可以看出 ${ } 可以实现的功能 #{ } 都能实现,并且 ${ } 还有 SQL 注入的问题,那么为什么 ${ } 的写法还存在?
2.1.2 ${ } 的优点
2.2 类中的属性和数据库表中的字段名不一致时,那么查询结果为 null,解决方案
2.2.1 将类中的属性和表中的字段名保持一致(最简单的解决方案)
2.2.2 使用 sql 语句中的 as 进行列名(字段名)重命名,让列名(字段名)等于属性名
2.2.3 返回字典映射:resultMap
2.3 like 查询操作
2.3.1 解决方案一:去掉单引号
2.3.2 解决方案二:mysql 内置函数 concat
3. 增、删、改操作
3.1 删除操作 + @Transactional 注解
3.2 修改用户操作
3.3 添加用户操作
3.3.1 MyBatis 添加操作,返回受影响的行数
3.3.2 返回自增 ID
4. 多表查询
4.1 一对一映射:使用注解
4.2 一对多映射:文章案例
5.动态 SQL
5.1 if 标签
5.2 trim 标签
5.3 where 标签
5.4 set 标签
5.5 foreach 标签
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及高级映射。MyBatis 去除了几乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
MyBatis 是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库工具
MyBatis 学习只分为两部分:
-- 创建数据库
drop database if exists mycnblog2023;
create database mycnblog2023 DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog2023;
-- 创建表[用户表]
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 timestamp default current_timestamp,
updatetime timestamp default current_timestamp,
`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 timestamp default current_timestamp,
updatetime timestamp default current_timestamp,
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 timestamp default current_timestamp,
updatetime timestamp default current_timestamp,
uid int
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog2023`.`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);
新创建的 Mybatis 启动时报错是正常的,因为未设置要连接的具体 MySQL 的详细信息
application.properties:
#设置数据库的相关连接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog2023?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
MyBatis 的 XML 中保存是查询数据库的具体操作 SQL:
# 设置 MyBatis XML 存放路径和命名格式
mybatis.mapper-locations=classpath:mybatis/*Mapper.xml
常规的写法,包含了两个文件:
添加实体类:
package com.example.demo.model;
import lombok.Data;
import java.time.LocalDateTime;
//添加实体类
@Data
public class Userinfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
使用 @Mapper 注解,即数据持久层标志,相当于五大类注解中的 @Repository
package com.example.demo.dao;
import com.example.demo.model.Userinfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper //数据持久层的标志
public interface UserMapper {
List getAll();
}
MyBatis 固定的 xml 格式:
注意:namespace 是表明当前 xml 实现的是哪个接口的(不是使用 xml 文件名和接口进行匹配的)
UserMapper.xml 查询所有⽤户的具体实现 SQL:
其中:
标签:需要指定 namespace 属性,表示命名空间,值为 mapper 接⼝的全限定名,包括全包名.类名。- id:方法名(是和 Interface(接⼝)中定义的方法名称⼀样的),表示对接⼝的具体实现方法
- id 中需要实现一个返回类型 resultType:是返回的数据类型,也就是开头我们定义的实体类
- 写具体的 SQL 时,注意没有分号
使用单元测试验证是否生效:在需要生成单元测试类(UserMapper)中右键 generate,生成一个 Test
package com.example.demo.dao;
import com.example.demo.model.Userinfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
//Spring 容器就是 IoC 容器,实现 DI,就可以把测试类(UserMapper)注入进去,使用 @Autowired 注解
@Autowired
private UserMapper userMapper;
@Test
void getAll() {
List list = userMapper.getAll();
System.out.println(list);
}
}
其中:
- 需要添加一个 @SpringBootTest 注解:告诉当前的测试程序,目前项目时运行在 Spring Boot 容器中
- Spring 容器就是 IoC 容器,实现 DI,就可以把测试类(UserMapper)注入进去,使用 @Autowired 注解
mysql 中:
运行测试类,实现功能:
在这里,我介绍一种 MyBatis 插件:为了方便开发 MyBatis 实现 XML 和对应的接口之技安的快速跳转,可以安装一个 MyBatisX 插件
实现快速跳转:
根据 id 查询一条信息
UserMapper 类:
@Mapper //数据持久层的标志
public interface UserMapper {
//传递单个参数
Userinfo getUserById(@Param("id") Integer id);
}
xml:
测试类:
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
@Test
void getUserById() {
Userinfo userinfo = userMapper.getUserById(1);
System.out.println(userinfo.toString());
}
}
传参有两种方式:1️⃣使用 ${ 参数名 }(使用 @value 注解读取配置文件的写法)
执行语句:
2️⃣使用 #{ 参数名 }
预编译和直接替换区别:
SQL 注入:使用特殊的 SQL 语句完成非法操作
例如,进行用户名和密码的判断:
假设这是一个登陆的功能 ,必须要求用户名和密码都输入正确,才能返回一条 SQL 语句;
则 SQL 注入指的是密码输入错误也能查到数据
UserMapper 类:
@Mapper //数据持久层的标志
public interface UserMapper {
//登陆功能
Userinfo login(@Param("username") String username, @Param("password") String password);
}
xml(使用 ${ }):
测试类:
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
@Test
void login() {
String username = "admin";
String password = "' or 1='1";
Userinfo userinfo = userMapper.login(username, password);
System.out.println(userinfo.toString());
}
}
在这里还有一个区别:
运行测试:
我们发现竟然可以查询出数据:输入不是正确密码,竟然能查询处结果(这就是最简单的 SQL 注入);则 使用 ${ } ,那么可以通过 ' or 1='1 这个命令登陆各种系统—— SQL 注入
使用 #{ }:
MyBatis 在处理 #{ } 时,会将 SQL 中的 #{ } 替换为 ? 号,使用 PreparedStatement 的 set 方法来赋值
例如,SQL 语句:
select * from userinfo order by id asc;
select * from userinfo order by id desc;
- ${ } 适用场景:当业务需要传递 SQL 命令,只能使用 ${ },不能使用 #{ }
- ${ } 注意事项:如果要使用 ${ },那么传递的参数一定要能被穷举,否则不能使用(上述只能是 desc 或者 asc)
已知数据库:
类中属性(Userinfo 类):
//添加实体类
@Data
public class Userinfo {
private int id;
private String name;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
@Test
void getAllByOrder() {
List list = userMapper.getAllByOrder("desc");
System.out.println(list);
}
UserMapper 类:
List getAllByOrder(@Param("myOrder") String myOrder);
XML 配置:
存在的问题: 定义的 name 已经在多个类中使用,那么把定义的 name 改为 username,随之其他类也需要改动,工作量有可能会增大
xml 配置修改 sql 语句:
定义一个 resultMap,将属性名和字段名进行手动映射
UserMap.xml:
id 设置到查询结果中:
UserMapper 类:
//like 查询
List getLikeList(@Param("username") String username);
UserMapper.xml:
生成单元测试:
@Test
void getLikeList() {
String username = "三";
List list = userMapper.getLikeList(username);
System.out.println(list);
}
- 这个时候发现使用 like 的时候报错了,因为 #{username} 是预执行,会变成 ?,则在设置 问号 的时候会加单引号 ,相当于:select * from userinfo where username like '%'username'%';
- 换成 ${ } 可以吗?可以,但是不能去用;因为使用 ${ }的场景:数值或者能被穷举或 过滤(风险也很大,关键东西多,过滤不彻底)
String username = "%三%";
这个时候就不存在单引号中在嵌套一个单引号
例如:使用 concat 进行拼接
UserMapper.xml:
单元测试:
String username = "三";
在 interface 中定义一个方法接口:
@Mapper //数据持久层的标志
public interface UserMapper {
//删除操作
int delById(@Param("id") Integer id);
}
Mapper.xml 实现标签:
delete from userinfo where id = #{id}
删除操作不需要返回类型,可以不写 resultType
已知数据库:
生成测试类:
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
@Transactional //事务,单元测试添加 @Transactional,执行完成之后会进行回滚操作:既能实现测试功能,同时不会污染或影响数据库
//已经构建了数据库,不能说测试的时候删除就没了
@Test
void delById() {
int id = 2;
int result = userMapper.delById(id);
System.out.println("受影响的行数:" + result);
}
}
添加一个注解 @Transactional,这是一个事务注解,执行完成之后会进行回滚操作,这样既能实现测试功能,同时不会污染或影响数据库(已经构建看数据库,不能说测试的时候删除就没了)
这个时候我们执行数据库操作发现这条语句依然存在:
没有 注解 @Transactional
UserMapper 类:
//修改操作
int update(Userinfo userinfo);
传递对象不需要用 @Param
UserMapper.xml(修改操作也不需要返回类型,不写 resultType):
update userinfo set username = #{username} where id = #{id}
传递对象中的#{ }:括号中直接使用对象的属性名,不需要写 userinfo.username;Mybatis 框架已经默认做了这个过程
生成单元测试:
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
@Test
void update() {
Userinfo userinfo = new Userinfo();
userinfo.setId(1);
userinfo.setUsername("超级管理员");
int result = userMapper.update(userinfo);
System.out.println("受影响的行数:" + result);
}
}
数据库:
当然如果不想影响数据库,和删除操作一样添加 @Transactional 注解
UserMapper 类:
//修改操作
int update(Userinfo userinfo);
UserMapper.xml:
insert into userinfo(username, password, photo) values(#{username}, #{password}, #{photo})
同理——传递对象中的#{ }:括号中直接使用对象的属性名,不需要写 userinfo.username;Mybatis 框架已经默认做了这个过程
生成单元测试:
//添加操作
@Test
void add() {
Userinfo userinfo = new Userinfo();
userinfo.setUsername("张三");
userinfo.setPassword("123");
userinfo.setPhoto("/image/default.png");
int result = userMapper.add(userinfo);
System.out.println("受影响的行数:" + result);
}
数据库:
这个时候发现 id 好像不是很符合,接下来我们来设置 自增id
UserMapper 类:
//返回自增 ID
int insert(Userinfo userinfo);
UserMapper.xml:
insert into userinfo(username, password, photo) values(#{username}, #{password}, #{photo})
生成单元测试:
@Test
void insert() {
Userinfo userinfo = new Userinfo();
userinfo.setUsername("李四");
userinfo.setPassword("123456");
userinfo.setPhoto(" ");
int result = userMapper.insert(userinfo);
System.out.println("受影响的行数:" + result + " | ID:" + userinfo.getId());
}
添加实体类:
package com.example.demo.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Articleinfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int uid;
private int rcount;
private int state;
//联表字段
private String username;
}
注解实现(查询文章的所有信息和用户的username),在这里使用 @Select 注解写 sql 相当于 xml 中的 sql:
package com.example.demo.dao;
import com.example.demo.model.Articleinfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface ArticleMapper {
//查询注解
//查询文章所有信息和用户的username
@Select("select a.*,u.username from articleinfo a left join userinfo u on a.uid=u.id")
List getAll();
}
生成测试类:
package com.example.demo.dao;
import com.example.demo.model.Articleinfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void getAll() {
List list = articleMapper.getAll();
System.out.println(list);
}
}
文章案例:查询一个用户的多篇文章
把查询分成两个查询:在业务类中首先查询到用户信息,再用用户的 id 去文章表里查询文章的 list,把 list 设置到文章用户的文章列表中
面试问项目有没有使用到多线程?上边案例就是如此,启动线程池,同时查询两张表(一张用户表,一张文章表),查询完成之后拼接实现查询
查询用户多篇文章,实体类 Userinfo 中添加字段:
@Data
public class Userinfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
private List alist;
}
UserMapper 类:
@Select("select * from userinfo where id=#{id}")
Userinfo getUserById2(@Param("id") Integer id);
ArticleMapper 类:
@Select("select * from articleinfo where uid=#{uid}")
List getListByUid(@Param("uid") Integer uid);
单元测试(单线程实现):
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
//Spring 容器就是 IoC 容器,实现 DI,就可以把测试类(UserMapper)注入进去,使用 @Autowired 注解
@Autowired
private UserMapper userMapper;
@Autowired
private ArticleMapper articleMapper;
@Test
void getUserList() {
int uid = 1;
//1.根据 uid 查询 userinfo
Userinfo userinfo = userMapper.getUserById2(uid);
System.out.println(userinfo);
//2.根据 uid 查询文章列表
List list = articleMapper.getListByUid(uid);
//组装数据
userinfo.setAlist(list);
System.out.println(userinfo);
}
}
多线程:
@SpringBootTest //告诉当前的测试程序,目前项目时运行在 Spring Boot容器中
class UserMapperTest {
@Test
void getUserList() {
int uid = 1;
//定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100));
final Object[] resultArray = new Object[2];
threadPool.submit(new Runnable() {
@Override
public void run() {
//1.根据 uid 查询 userinfo
resultArray[0] = userMapper.getUserById2(uid);
}
});
threadPool.submit(new Runnable() {
@Override
public void run() {
//2.根据 uid 查询文章列表
resultArray[1] = articleMapper.getListByUid(uid);
}
});
//组装数据(等线程池执行完成之后)
while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {
}
Userinfo userinfo = (Userinfo) resultArray[0];
userinfo.setAlist((List) resultArray[1]);
System.out.println(userinfo);
}
}
语法:
....
insert into userinfo(username, password
,photo
) values(#{username},#{password}
,#{photo}
)
用来去掉不必要的信息;如果所有字段都是非必填项,就考虑使用
insert into userinfo
username,
password,
photo,
values
#{username},
#{password},
#{photo},
测试类:
@Test
void add3() {
Userinfo userinfo = new Userinfo();
userinfo.setUsername("王五");
userinfo.setPassword("123");
int result = userMapper.add2(userinfo);
System.out.println("执行结果:" + result);
}
UserMapper 接⼝中新增条件查询方法:
//where 标签
List getListByWhere(Userinfo userinfo);
UserMapper.xml 中新增条件查询 sql:
测试类1(什么都不传,相当于没有 where 语句):
@Test
void getListByWhere() {
Userinfo userinfo = new Userinfo();
List list = userMapper.getListByWhere(userinfo);
System.out.println(list);
}
测试类2(传入id,正常添加 where语句):
@Test
void getListByWhere() {
Userinfo userinfo = new Userinfo();
userinfo.setId(1);
List list = userMapper.getListByWhere(userinfo);
System.out.println(list);
}
测试类3(where 语句可以去掉前面的 and):
@Test
void getListByWhere() {
Userinfo userinfo = new Userinfo();
//userinfo.setId(1);
userinfo.setUsername("王五");
//userinfo.setPassword("123");
List list = userMapper.getListByWhere(userinfo);
System.out.println(list);
}
上述 where标签也可以使用 trim 标签:
trim 里边执行到了加 where,执行不到不加 where
UserMapper 接⼝中新增条件查询方法:
//set 标签
int update2(Userinfo userinfo);
UserMapper.xml 中新增条件查询 sql:
update userinfo
username=#{username},
password=#{password},
where id=#{id}
测试类:
@Test
void update2() {
Userinfo userinfo = new Userinfo();
userinfo.setId(3);
userinfo.setUsername("王五");
int result = userMapper.update2(userinfo);
System.out.println("执行结果:" + result);
}
根据多个⽂章 id 来删除⽂章数据。
ArticleMapper 中新增接口方法:
int deleteByIds(List ids);
ArticleMapper.xml 中新增删除 sql:
delete from article
where id in
#{item}