MyBatis 查询数据库入门
一、MyBatis 是什么?
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 去除了几乎 所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和 映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记 录。
简单来说 MyBatis 是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库工具。
针对 “MyBatis 是一款优秀的持久层框架” 进行分析和补充:
MyBatis也是一个 ORM (Object Relational Mapping,即对象关系映射)框架。
在面向对象编程语言中,将关系型数据库中的数据与对象建立起映射关系,进而自动的完成数据与对象的相互转换:
- 将输入数据(即传入对象)+ SQL 映射成原生 SQL。
- 将结果集映射为返回对象,即输出对象。\
ORM 把数据库映射为对象:
- 数据库表(table) --> 类(class)
- 记录(record,行数据) --> 对象(object)
- 字段(field) --> 对象的属性(attribute)
一般的 ORM 框架,会将数据库模型的每张表都映射为一个 Java 类。
即,使用 MyBatis 可以像操作对象一样来操作数据库中的表,可以实现对象和数据库表之间的转换。
即:MyBatis 可以当作是一座 “桥梁”:
将数据库 和 程序,映射起来。 MySQL 和 MyBatis 是不一样的:
MySQL 提供了一个 数据存取(数据管理)的软件。
而 MyBatis 是一个 “中间桥梁”,用于连接程序和数据库,建立映射关系,进行 数据操作 的中间层(持久层)。
二、为什么要学习 MyBatis
对于后端开发来说,程序是由以下两个重要的部分组成的:
- 后端程序
- 数据库
而这两个重要的组成部分要通讯,就要依靠数据库连接工具,那数据库连接工具有哪些?比如之前我们 学习的 JDBC,还有今天我们将要介绍的 MyBatis,那已经有了 JDBC 了,为什么还要学习 MyBatis?
这是因为 JDBC 的操作太繁琐了,我们回顾一下 JDBC 的操作流程:
- 创建数据库连接池
DataSource
- 通过
DataSource
获取数据库连接Connection
- 编写要执行带
?
占位符的SQL
语句 - 通过
Connection
及SQL
创建操作命令对象Statement
- 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
- 使用
Statement
执行SQL
语句 - 查询操作:返回结果集
ResultSet
,更新操作:返回更新的数量 - 处理结果集
- 释放资源
JDBC 操作示例回顾
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Scanner;
public class TestJDBC {
public static void main(String[] args) throws SQLException {
//让用户手动输入数据到数据库中
Scanner scanner = new Scanner(System.in);
//1. 创建数据源
DataSource dataSource = new MysqlDataSource();
//设置数据库所在的地址
((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false");
//设置登录数据库的用户名
((MysqlDataSource)dataSource).setUser("root");
//设置登录数据库的密码
((MysqlDataSource)dataSource).setPassword("1234");
//2. 让代码和数据库服务器之间建立连接
Connection connection = dataSource.getConnection();
//3. 操作数据库,以插入数据为例
// 关键所在:构造一个 SQL 语句
// 在 JDBC 中构造 SQL 语句,不必带上 ;
// ; 只是在命令行中用来区分不同的语句,现在是直接在代码中操作
String sql = "insert into JDBC values(1,'张三')";
// 此处光是一个 String 类型的 sql 还不行,需要把这个 String 包装成一个 “语句对象”
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//用户手动输入数据
System.out.println("请输入 ID:");
int id = scanner.nextInt();
System.out.println("请输入 姓名:");
String name = scanner.next();
//? 相当于告诉 java程序,这两个字段的值 还不确定,此时就使用 ? 先占个位置
// 再使用 PreparedStatement 的 setXXX 方法 进行替换,这里的setXXX方法很多,需要让这里的方法和数据库的列的类型匹配
String sql2 = "insert into JDBC values(?,?)";
PreparedStatement statement = connection.prepareStatement(sql2);
//进行替换操作
statement.setInt(1,id); //下标从 1 开始计算,把第一个 ? 替换成 id 这样的值
statement.setString(2,name);
System.out.println("statement: " + statement); //通过这个打印操作,可以看到拼装好之后的 SQL 长啥样
//4. 执行 SQL
// SQL 里面如果是 insert, update, delete ,都使用 executeUpdate 方法,
// SQL 里面如果是 select,则使用 executeQuery 方法
// 返回值就表示这个操作,影响到了几行,就相当于在控制台里输入 sql 之后,得到的数字
int ret = preparedStatement.executeUpdate();
int ret2 = statement.executeUpdate();
System.out.println(ret);
System.out.println(ret2);
//5. 此时 SQL 已经执行完毕,然后还需要释放资源
preparedStatement.close();
statement.close();
connection.close();
}
}
从上述代码和操作流程可以看出,对于 JDBC 来说,整个操作非常的繁琐,我们不但要拼接每一个参 数,而且还要按照模板代码的方式,一步步的操作数据库,并且在每次操作完,还要手动关闭连接等, 而所有的这些操作步骤都需要在每个方法中重复书写。于是我们就想,那有没有一种方法,可以更简单、更方便的操作数据库呢?
答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库。
三、怎么学 MyBatis
MyBatis 学习只分为两部分:
- 配置 MyBatis 开发环境。
- 使用 MyBatis 模式和语法操作数据库。
1. 创建 MyBatis 项目
准备工作:创建数据库 和 数据表
-- 创建数据库
drop database if exists Mybatis;
create database Mybatis DEFAULT CHARACTER SET utf8;
-- 使⽤数据数据
use Mybatis;
-- 创建表[⽤户表]
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
);
-- 创建⽂章表
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
);
-- 创建视频表
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
);
-- 添加⼀个⽤户信息
INSERT INTO `Mybatis`.`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。
1.1 添加 MyBatis 相关依赖
这里会涉及两个场景:
- 项目创建的时候,引入 MyBatis 相关依赖。
- 老项目添加 MyBatis。
1. 新建一个 MyBatis 项目
- 创建新项目:
\
- MyBatis 项目是建立在 Spring MVC 项目上的,因此,下面三个框架依赖必不可少:
\
- 添加 MyBatis 项目:
\
- 引入可驱动的数据库:
- 点击
finish
即可完成。
2. 老项目中引入 MyBatis 相关依赖
使用 Edit Starter
插件进行添加。
在 pom.xml
文件中的 dependencies
标签中,找到一个空行(人造一个也行),鼠标右击,选择 generate
:
下面为获取依赖的官方网址:
选择要添加的依赖:
\
\
1.2 配置数据库连接字符串
不要立即启动项目,否则就会报错:
\
- 在
resources
下创建三个.yml
文件
(现在的学习还不会涉及到生产环境)
- 配置开发环境配置文件:
注意:关于 driver-class-name
中所写的驱动名称
- 驱动名称:我们使用的是 MySQL,因此我们填写的是 MySQL 的驱动名称。
- 如果使用的 MySQL 是
5.x
之前的版本,则用com.mysql.jdbc.Driver
。 如果版本大于5.x
,则用com.mysql.cj.jdbc.Driver
。
\
- 在主配置文件中激活开发环境
\
此时启动项目后就不会报错了:
\
1.3 配置 MyBatis 保存的 xml 的目录
MyBatis 有两种操作方法:
- 使用
xml
的形式。 - 注解。(MyBatis 3.1 版本之后提供)
但是,注解的方式并不好用,我们主要还是关注 xml
形式的操作。
- 一般我们在
resources
目录下,创建一个子目录,来存放xml
文件:
\
- 由于此配置是公共的,所以我们在主配置文件配置 MyBatis 的 xml 保存路径:
\
四、使用 MyBatis 的操作模式操作数据库
MyBatis 的操作模式
MyBatis 的操作模式,包含两个部分:
- Interface(接口,里面是方法的定义)
- xml 文件(对 方法 的实现,使用 SQL 语句)
在 Interface 接口中会加一个注解,这个注解是MyBatis里的注解 @Mapper
,将普通的接口变为 MyBatis 里面的接口,将接口里面的方法映射进 xml
文件。
\
MyBatis 查询:实现一个根据用户id来查询用户信息的操作
数据库中已经创建好的表:
\
1. 定义接口
现在根目录底下创建几个包:
- model 与数据库交互
- controller 与前端交互
- server 决定调用哪些映射方法
- mapper 操作数据,实现映射
\
为了能与数据进行交互,需要创建实体类,来存储在数据库中得到的结果。在 model
目录底下创建实体类,(实体类名称最好与数据库的表名一致,这样更好处理):
在实体类里面的字段,也要和数据库中表里面的字段名称相同:
\
下面就可以在 mapper
包底下写数据了:
注意:一定要在接口上加上 注解@Mapper。
在我们给接口加上 @Mapper 注解后,它就不是一个单纯的接口了。此时,UserMapper
接口变成 MyBatis 的一部分了。这个 接口 里面的方法,就可以使用 xml
去实现了。
但是 Java 的接口是依靠类来实现的。这确实和传统的开发是不一样的,传统开发直接写业务代码就行了。
现在是要写 SQL 了,SQL 和 业务代码,是两个不同的体系。
普通接口变成 MyBatis 接口,有什么好处呢?
它将接口的生命周期,交给容器来托管,它会实现 interface 和 xml 之间的关联关系。
\
下面来写 User Mapper
接口里面方法的定义:
package com.example.MyBatisDemo.mapper;
import com.example.MyBatisDemo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper // 变成 mybatis interface
public interface UserMapper {
// 根据用户 id 查询用户
public UserInfo getUserById(@Param("id")Integer id); // @Param 代表参数在xml中名字为 id
}
\
2. 创建 xml,实现接口里面定义的方法
xml 文件,不能随意创建。
因为我们在之前的配置文件中,指定了 xml 文件的保存路径,并且命名规则也指定了。
按照规则创建 xml 文件,xml 文件的名称建议和 接口 名称一样,比较好联系:
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="">
</mapper>
其中里面的 namespace
设置成实现接口的位置:包名 + 接口名称:
\
接着,在 xml 文件中,实现 接口UserMapper
里面的方法了:
查询标签里面的 resultType 是不能省略的,否则访问网页会报错:
\
select
标签里面的内容你就按照 SQL 语句写就行了,此处我们写的是select * from userinfo where id=#{id}
。
\
- 关于接口
UserMapper
中中的@Param:
如果不写这个注解,默认在 xml 文件中,获取方法参数的名称,就是方法参数原本的名称。
但是,在有一些 jdk 版本中,不写这个注解就会报错。所以为了避免麻烦,还是加上这个注解比较好。\
-
这里面要注意一点,
#{id}
里面的 id 是@Param("id")
里的 id 名称。(换句话说,如果@Param("userId")
,你 SQL 里面要变为#{userId}
。\ -
还有一点,SQL 语句末尾可以不要分号。
\
- 关于
获取方法参数,使用
${}
和#{}
的区别,后面会详细讲。
运行结果展示
在 service
包底下创建一个 UserService
类。
在 UserService
类里面写一个方法,去调用 接口UserMapper
中的方法。
然后,在 controller
包下,创建 UserContronller
类,去调用 类UserService
中的方法。
UserMapper 中方法
service 层
controller 层
\
启动项目,打开浏览器去访问:
\
\
开启 MyBatis 日志打印功能
一般在开发环境中开启。
目录结构:
# 开启 Mybatis SQL 打印
logging:
level:
com:
example:
MyBatisDemo: debug
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
开启后可以在idea的控制台上看见日志:
当我们开启 MyBatis SQL 日志打印的时候,我们能清楚看到最终生成的SQL语句以及执行结果。
Spring Boot 单元测试简单使用
五、增、删、改 操作
接下来实现以下用户信息的增加、删除和修改操作,对应使用MyBatis 的标签如下:
<insert>
标签:插入语句。<update>
标签:修改语句。<delete>
标签:删除语句。
@Transactional 注解
有一点要注意:在进行增、删、改的时候,如果不想污染数据库,可以在Test中加注解:@Transactional(表示在方法执行完之后回滚事务)
\
MyBatis 修改操作
- interface 里面添加修改方法的声明:
注意:@Param 建议要添加上,不然有些电脑上运行会报错,会提示找不到参数。
它和 xml 里面的参数是匹配的。
- 在 xml 中添加
<update>
标签和具体的执行 sql
名称要对应:
\
MyBatis 删除操作
- 在 interface 里面添加 删除 的代码声明
- 在 xml 中添加
<delete>
标签和删除的 sql 编写
\
MyBatis 添加操作
- 在 interface 添加方法声明
- 在 xml 实现添加业务
\
注意:这里的参数传入的是一个对象啊,我们怎么才能获取到里面的属性呢?可以直接通过
#{属性名}
去获取就行:
\
如果不想将影响的行数,作为返回值了。想要 插入成功的用户信息的ID 作为返回值!虽然实现的步骤没有发生变化,但是想要达到预期的效果,还需要添加些东西:
在 第二个步骤中(方法名称设为 addGetId):想要返回用户信息的ID,还需要添加两个属性:useGeneratedKeys
和 keyProperty
- useGeneratedKeys: 是否自动生成主键(true 为是,false 为否)。这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据 库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false
- keyProperty: 返回的主键赋值到那个属性上,这个时候可以指定 id 属性了。指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值 或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
注意:最好加一个属性 keyColumn
: 设置生成键值在表中的列名,在某些数据库(如 PostgreSQL)中,当主键列不是表中第一列的时候,是必须设置的。如果生成列不止一个,就可以用逗号分隔多个属性名称。
即,加入 keyColumn 的原因就是为了确保获取对应字段。
\
\
六、查询操作
单表查询
单表查询即为上面所介绍的:
\
参数占位符 #{} 和 ${}
1. 功能不同
#{}
预编译处理。${}
字符直接替换。
预编译处理 : MyBatis 在处理 #{}
时,会将 SQL 中的 #{}
替换为 ?
。使用 PreparedStatement 的 set 方法来赋值
直接替换 : MyBatis 在处理 ${}
时,把 ${}
替换成变量的值。
可以在之前写过的查询代码中,演示下 #{}
和 ${}
的区别:
测试类
使用 #{}
占位符:
测试类运行结果:
使用 ${}
占位符:
测试类运行结果:
\
${} 的问题
当参数为数值类型时(在不考虑安全问题的前提下),${}
和 #{} 的执行效果都是一样的,然而当参数类型为 字符时,再使用 ${}
就有问题了,如下代码所示:
<select id="getUserById" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username=${name}
</select>
以上程序执行时,生成的 SQL 语句如下:
这会导致程序报错,因为传递的参数是字符类型的,而在 SQL 语法中,如果是字符类型需要给值添加单引号,否则就会报错,而 ${}
是直接替换的,不会自动添加单引号,所以执行就会报错。而使用 #{}
采用的是 占位符 预执行的,所以不存在任何问题,它的实现代码如下:
<select id="getUserById" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username=#{name}
</select>
以上程序的最终生成的执行 SQL 如下:
2. 使用场景不同
虽然使用 #{} 的方式可以处理任意类型的参数,然而当传递的参数是一个 SQL 命令或 SQL 关键字时 #{} 就会出问题了。比如,当我们要根据价格从高到低(倒序)、或从低到高(正序)查询时,如下图所示:
此时我们要传递的排序的关键字,desc 倒序(价格从高到低)或者是 asc 正序(价格从低到高),此时我们使用${}
的实现代码如下:
<select id="getOrderList" resultType="com.example.MyBatisDemo.model.Goods">
select * from goods order by price ${order}
</select>
以上代码生成的执行 SQL 和 运行结果如下:
但是,如果将代码中的 ${}
改为 #{}
,那么程序就会报错,#{}
的实现代码如下:
<select id="getOrderList" resultType="com.example.MyBatisDemo.model.Goods">
select * from goods order by price #{order}
</select>
以上代码生成的执行 SQL 和 运行结果如下:
从上述的执行结果可以看出,当传递的是普通参数时,需要使用 #{}
的方式,而当传递的是 SQL 命令或 SQL 关键字时,需要使用 ${}
来对 SQL 中的参数进行直接替换并执行。
3. 安全性不同
${}
和 #{}
最主要的区别体现在安全方面,当使用 ${}
会出现安全问题,也就是 SQL 注入的问题,而使用 #{}
因为是预处理,所以不会出现安全问题, 下面通过登录功能来观察二者的区别:
3.1 使用 ${} 实现用户登录
信息:
UserMapper.xml 中的实现代码如下:
<!-- 用户登录 -->
<select id="login" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username='${username}' and password='${password}'
</select>
单元测试代码如下:
@Test
void login() {
UserInfo userInfo = userMapper.login("admin","admin");
System.out.println(userInfo);
}
以上代码生成的执行 SQL 和运行结果如下:
从结果可以看出,当我们传入了正确的用户名和密码时,能成功查询数据。但是,在我们使用 ${}
时,当我们在不知道正确密码的情况下,使用 SQL 注入语句也能得到用户的私人信息,SQL 注入的实现代码如下:
@Test
void login() {
List<UserInfo> userInfo = userMapper.login("admin","' or 1='1");
System.out.println("用户信息:"+userInfo);
}
以上代码生成的执行 SQL 和 运行结果如下:
从上述结果可以看出,当使用 ${} 时,在不知道正确密码的情况下也能得到用户的私人数据,这就像一个小偷在没有你们家钥匙的情况下,也能轻松的打开你们家大门一样,这是何其恐怖的事情。那使用 #{} 有没有安全问题呢?接下来我们来测试一下。
3.2 使用 #{} 实现用户登录
首先将 UserMapper.xml 中的代码改成以下内容:
<select id="login" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username=#{username} and password=#{password}
</select>
接着我们使用上面的 SQL 注入来测试登录功能:
@Test
void login() {
List<UserInfo> userInfo = userMapper.login("admin","' or 1='1");
System.out.println("用户信息:"+userInfo);
}
最终生成的 SQL 和 执行结果如下:
从上述代码可以看出,使用 SQL 注入是无法攻破 #{} 的“大门”的,所以可以放心使用。
总结:
${}
和 #{}
都是 MyBatis 中用来替换参数的,它们二者的区别主要体现在:
- 功能不同:
${}
是直接替换,而#{}
是预处理。 - 使用场景不同:普通参数使用
#{}
,如果传递的是 SQL 命令或 SQL 关键字,需要使用${}
,但在使用前一定要做好安全验证。 - 安全性不同:使用
${}
存在安全问题,而#{}
则不存在安全问题。
like 查询 - 特殊情况
在前面的问题中,由于关键字就那么几个,可以直接穷举,所以很容易在 Controller 层里面判断数据的正确性。
但是!模糊匹配的结果是不能穷举的!如果数据有几百万个,我们验证数据的正确性得累死!
下面演示:
like 使用 #{}
报错:
<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username like '%#{username}%'
</select>
报错信息如下:
相当于:select * from userinfo where username like '%'username'%'
。
所以这里就不能使用 #{}
了,可以使用 ${}
进行直接的替换,就不会出现单双引号了:
<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username like '%${username}%'
</select>
测试类运行结果如下:
但是!使用了 ${}
,就需要在 Controller 里进行验证数据,这一点很重要!!
但是模糊匹配的结果,是海量的!我们不可能全部穷举出来。
\
此时就可以考虑 concat
拼接方法:
可以考虑使用 mysql 的内置函数 concat()
来处理,此函数的效果为:
实现代码如下:
<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%')
</select>
结果如下:
\
多表查询
前置知识
返回类型:resultType
这个我们很熟悉,这是用于指定 SQL 查询结果映射的对象类型。
但是这里有个细节!!
虽然我们只是使用了 id 属性,但是有一个问题:当我们指定了返回的类型之后,这个实体类(UserInfo)里面的属性必须和数据库中的字段是一致的!哪怕这个属性在 SQL 语句中没有被使用,也是一样的,必须相同!
在名字相同的情况下,运行测试类,看看情况:
\
如果我们将 UserInfo 中的 username 属性 改为 name:
此时再来运行测试类:
虽然测试通过了,但是 name 属性是为 null 的,而不是 admin。
出现这个问题的原因就是:数据库表中的 字段名称 和 实体类属性名称,不相同!!
因此无法完成匹配赋值!
除了修改 实体类属性名称,还有一个办法,使用下面介绍的 resultMap
:
返回字典映射:resultMap
resultMap 使用场景:
- 字段名称和程序中属性名不同的情况,可以使用 resultMap 配置映射:
下面的写法只适合 单表查询
<resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
<!-- 主键映射 -->
<id column="id" property="id"></id>
<!-- 表示普通属性映射 -->
<result column="username" property="name"></result>
</resultMap>
- resultMap里,为此 resultMap 命名为
id
,type
为你要映射的类的地址 - 普通属性映射里面,
column
代表的是数据库中的字段名,property
代表的是程序中属性名。即 column 映射到 property。
此时就可把 resultType 删除,改用 resultMap了,其值为我们给 resultMap 起的别名 BaseMap:
<select id="getUserById" resultMap="BaseMap">
select * from userinfo where id=${id}
</select>
此时再运行测试单元,得到结果:
\
- 一对一和多对多关系可以使用 resultMap 映射并查询数据。
多表查询:一对一关系
下面来模拟实现一对一的关系。文章和作者就是一对一的关系,因为一篇文章只能对应一个作者。
我们先创建文章表的实体类:由于 文章表 和 作者表 是一对一关系,所以可以在文章表的实体类中加入 UserInfo
的属性:
package com.example.MyBatisDemo.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ArticleInfo {
private Integer id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private Integer uid;
private Integer rcount;
private Integer state;
private UserInfo userInfo;
}
先来观察下 articleInfo 表中的信息,方便后面写代码:
\
在 mapper 包中创建 MyBatis 接口,实现根据文章 id 查询到文章的详细信息:
@Mapper
public interface ArticleMapper {
public List<ArticleInfo> getAll(@Param("id")Integer id);
}
创建一个新的 xml 用来编写 SQL:
<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<select id="getAll" resultType="com.example.MyBatisDemo.model.ArticleInfo">
select * from articleinfo where id=#{id}
</select>
</mapper>
编写测试类:
@SpringBootTest
@Slf4j
class ArticleMapperTest {
@Resource
private ArticleMapper articleMapper;
@Test
void getAll() {
List<ArticleInfo> articleInfo = articleMapper.getAll(1);
log.info("作者信息: " + articleInfo);
}
}
测试类执行结果:
咋一看上面的结果没啥问题,但是观察 属性 UserInfo 时却发现是空的:
为什么会出现上面的情况呢?
原因就是,实体类 ArticleInfo
中有 UserInfo
属性,但是 文章表中却没有这个字段:
此时我们就需要 resultMap
了。
设置 resultMap
:
<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.model.ArticleInfo">
</resultMap>
<select id="getAll" resultMap="BaseMap">
select * from articleinfo where id=#{id}
</select>
</mapper>
BaseMap
为此标签的别名,与其他 xml 中的 resultMap 的别名重复的话,不会有影响。
在 resultMap 中设置映射关系:将文章表中所有字段都映射出来:
<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.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>
</resultMap>
<select id="getAll" resultMap="BaseMap">
select * from articleinfo where id=#{id}
</select>
</mapper>
此时我们发现,resultMap 中还没有映射对象,这时候就需要用到标签 association
:
<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.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.MyBatisDemo.mapper.UserMapper.BaseMap">
</association>
</resultMap>
<select id="getAll" resultMap="BaseMap">
select * from articleinfo where id=#{id}
</select>
</mapper>
association
标签的意思为:通过 resultMap 将目前的 resultMap 与另一个 resultMap 进行关联!就是通过 UserMapper 的 BaseMap 配置的字段信息,打包赋值给 userinfo。
将 SQL 语句改为多表的查询:
<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<select id="getAll" resultMap="BaseMap">
select a.*,u.* from articleinfo a left join userinfo u on a.uid=u.id where a.id=#{id}
</select>
</mapper>
但是如果 UserMapper 的 BaseMap 中只配置了部分字段,就会出现一些问题:
<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
<!-- 主键映射 -->
<id column="id" property="id"></id>
<!-- 表示普通属性映射 -->
<result column="username" property="name"></result>
</resultMap>
</mapper>
执行 ArticleMapperTest 测试单元,结果如下:
可以看到 UserInfo 里面只显示了在 UserMapper.xml 中映射的字段,其余没有映射的字段都是为空(status 默认为 0)
这里可以得出一个小结论:
- 在本身 xml 文件中,是可以不用映射所有属性的信息,因为它是自己调用自己,所以不需要 resultMap 将属性全部映射,都能自动完成所有属性的映射。
- 而想要在一个 resultMap 中调用另一个 resultMap 中的信息,只能是它映射了的信息,否则无法获取。
这时候我们将 UserMapper.xml 中的属性映射补全:
<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.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>
</resultMap>
</mapper>
测试类 ArticleMapperTest 运行结果:
可以看到UserInfo的所有字段都出来了。
但是上面获得的部分信息却是有误的:
但是有误的信息却和 ArticleInfo 中的信息相符合:
\
得出一个结论:
当一个字段在两张表中都存在时,默认读取的是 association 标签的 property 参数所在的数据表的字段值。
当前 property 是在 ArticleInfo 表中的:
下面来看下重名的字段:
下面来解决重名的问题。
解决 不同数据表中字段重名的情况
解决方法很简单,添加一个前缀,让 SQL 中的字段都带有 “身份标记”,这样可以避免重名的情况。
这种添加前缀的方式,是通过在 association
中添加一个属性 columnPrefix
,翻译成中文就是 属性前缀 的意思。
<association property="userInfo"
resultMap="com.example.MyBatisDemo.mapper.UserMapper.BaseMap"
columnPrefix="u_">
</association>
设置好前缀后,修改 SQL 语句:把那些字段名称重复的,前面加 u_
前缀,并且我们获取 userinfo 中全部的字段信息:
<select id="getAll" resultMap="BaseMap">
select a.*,u.id u_id,u.updatetime u_updatetime,u.createtime u_createtime,u.state u_state,
u.username u_username,u.password u_password,u.photo u_photo
from articleinfo a left join userinfo u on a.uid=u.id where a.id=#{id}
</select>
测试类 ArticleMapperTest 运行结果:
\
这才真正是实现了 一对一 关系的多表查询!!
多表查询:一对多关系
一对多关系,比如,一个用户可以是多篇文章的作者。
一对多关系,需要使用 <collection>
标签,用法和 是一样的。
下面来演示:
首先将 用户表 的实体类进行处理:
@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> articleInfoList;
}
使用线性表的属性,来接收 多篇文章的信息。
在 UserMapper 中定义方法:
@Mapper
public interface UserMapper {
// 根据用户 id 查询用户信息,和 所对应的文章信息
public UserInfo getUserAndArticleById(@Param("id")Integer id);
}
\
在 UserMapper,xml 中映射属性:
<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
<resultMap id="BaseMap" type="com.example.MyBatisDemo.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>
</resultMap>
</mapper>
映射 中 articleList 对象,使用
collection
标签:
<collection property="articleInfoList"
resultMap="com.example.MyBatisDemo.mapper.ArticleMapper.BaseMap"
columnPrefix="a_">
</collection>
property
:需要映射的属性。resultMap
:映射对象。columnPrefix
:将文章表中 带有a_
前缀的字段值,打包映射到 articleList 对象中。
修改 SQL 语句:
<!-- 根据用户 id 查询用户信息,和 所对应的文章信息 -->
<select id="getUserAndArticleById" resultMap="BaseMap">
select u.*,a.id a_id,a.title a_title,a.content a_content,a.createtime a_createtime,a.updatetime a_updatetime,a.uid a_uid,a.rcount a_rcount,a.state a_state
from userinfo u left join articleinfo a on u.id=a.uid where u.id=#{id}
</select>
\
生成并编写测试单元:
@SpringBootTest
@Slf4j
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
void getUserAndArticleById() {
UserInfo userInfo = userMapper.getUserAndArticleById(1);
log.info("用户信息:" + userInfo);
}
}
运行结果:
\
至此一对多查询完结!
七、复杂情况:动态 SQL 使用
动态 sql 是Mybatis的强大特性之一,能够完成不同条件下不同的 sql 拼接。
可以参考官方文档:mybatis – MyBatis 3 | 动态 SQL
1. if 标签
在注册用户的时候,可能会有这样一个问题,如下图所示:
注册分为两种字段:必填字段和非必填字段,那如果在添加用户的时候有不确定的字段传入,程序应该 如何实现呢?
这个时候就需要使用动态标签 来判断了。
if 标签判断一个参数是否有值,如果没值,那么就会隐藏 if 中的 sql。
但是 必须有一个值是必传的。
语法:
<if test="username!=null">
username=#{username}
</if>
- 在 test 中,判断表达式是否为空。
- 如果test 的结果为 true,那么拼接里面的 SQL 语句,加上
username=#{username}
。 - 如果test 的结果为 false,那么 if 标签中的 sql 就会被忽略。
这两个 username 是对应的。名字是 实体类中的属性名。
演示:
我们先给 userinfo 表中的 photo 设置一个默认值,默认为 default.png
\
在 UserMapper 中添加方法 add2:
@Mapper // 变成 mybatis interface
public interface UserMapper {
// 添加用户,photo 是非必传参数
public int add2(UserInfo userInfo);
}
\
在 UserMapper.xml 中编写 SQL:
<!-- 添加用户,photo 是非必传参数 -->
<insert id="add2">
insert into userinfo(username,password
<if test="photo!=null">
,photo
</if>
) value(#{name},#{password}
<if test="photo!=null">
,#{photo}
</if>
)
</insert>
test 里面的 photo 为对象的属性名。
编写测试类,当传入 photo 属性时:
@SpringBootTest
@Slf4j
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
void add2() {
UserInfo userInfo = new UserInfo();
userInfo.setName("张三");
userInfo.setPassword("123");
userInfo.setPhoto("123.png");
int result = userMapper.add2(userInfo);
log.info("用户信息:"+userInfo);
}
}
运行结果:
当不传入 photo 时:
2. trim 标签
最主要的作用:去除 SQL 语句前后多余的某个字符。
标签中有如下属性:
- prefix:表示整个语句块,以prefix的值作为前缀
- suffix:表示整个语句块,以suffix的值作为后缀
- prefixOverrides:表示整个语句块要去除掉的前缀
- suffixOverrides:表示整个语句块要去除掉的后缀
\
语法:
<trim prefix="(" suffix")" prefixOverrides="," suffixOverrides=",">
<if test="xxx">
...
</if>
...
</trim>
- 基于
prefix
配置,开始部分加上(
- 基于
suffix
配置,结束部分加上)
- 多个 组织的语句都以
,
结尾,在最后拼接好的字符串还会以,
结尾,会基于suffixOverrides
配置去掉最后一个,
演示:
在 UserMapper 添加方法 add3.
编写 SQL 语句:
<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
<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>
</mapper>
测试类:不传入 photo 时
@SpringBootTest
@Slf4j
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
void add3() {
UserInfo userInfo = new UserInfo();
userInfo.setName("王五");
userInfo.setPassword("123");
//userInfo.setPhoto("123.png");
int result = userMapper.add3(userInfo);
log.info("用户信息:"+userInfo);
}
}
运行结果:
当传入 photo 时:
\
3. where 标签
主要作用:实现查询中的 where 替换。它可以实现如果没有任何的查询条件,那么它可以隐藏查询中的 where sql,但如果存在查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动的去除最前面一个 and 字符。
演示:
SQL 语句编写:
<!-- 根据 id 查询用户 -->
<select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
</if>
</where>
</select>
当传入id 时:
结果:
当不传入 id 时:
结果:
如果 里面的都为空了,则不会添加 where 语句。
可以去除最前一个 and:
但是无法去除最后一个 and:
以上标签也可以使用 <trim prefix="where" prefixOverrides="and">
替换。
4. set 标签
根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容。
可以去除最后一个,
语法:
update table_name
<set>
<if test="xxx">
...
</if>
...
</set>
演示:
<update id="updateById">
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>
测试:
@Test
void updateById() {
UserInfo userInfo = new UserInfo();
userInfo.setId(5);
userInfo.setName("拉普拉斯");
int result = userMapper.updateById(userInfo);
}
结果:
以上标签也可以使用 <trim prefix="set" suffixOverrides=",">
替换。
5. foreach 标签
对集合进行遍历时可以使用该标签。标签有如下属性:
- collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
- item:遍历时的每一个对象
- open:语句块开头的字符串
- close:语句块结束的字符串
- separator:每次遍历之间间隔的字符串
\
演示:
在 UserMapper 中添加方法 delIds
@Mapper // 变成 mybatis interface
public interface UserMapper {
public int delIds(List<Integer> ids);
}
编写 sql 语句:
<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
<delete id="delIds">
delete from userinfo where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
</mapper>
\
编写测试类:
@SpringBootTest
@Slf4j
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
void delIds() {
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
int result = userMapper.delIds(list);
}
}
执行结果:
删除前:
删除后:
\
转载自:https://juejin.cn/post/7146758104741314574