Java - Mybatis
Mybatis 是个 ORM 框架,ORM 全称是 Object Relational Mapping(对象关系映射),这和前面学的 JDBC 中 DAO 很像,一个 POJO 类对应一个表,它两之间就是映射关系,而 ORM 说的就是这回事。
相比我们自己封装的 JDBC 框架优势在哪?
1.可以不写大量 JDBC 代码
2.不用处理结果集
3.另一个好处是 SQL 和代码解耦
目录
- 目录
- 1 初试单表增删改查
- 2 接口代理执行 SQL
- 3 Mapper 标签参数类型
- 4 动态 SQL
- 5 多表关联查询结果映射
- 6 缓存🔨
- 7 逆向工程🔨
1 初试单表增删改查
Maven 配置依赖。具体版本或者 Jar 包可以到 Gtihub 上看。
<!--MyBatis 框架-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<!--MySQL 驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
还是使用 JDBC 中的表做实例演示。
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户 ID',
`name` varchar(255) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '用户密码',
`realname` varchar(255) COMMENT '真实姓名',
`gender` varchar(1) COMMENT '性别',
`tel` varchar (11) COMMENT '手机号码',
PRIMARY KEY (`id`)
) COMMENT = '这是一张存储用户信息的表'
1.1 Insert
创建 mybatis-config.xml 核心配置文件。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="userMapper.xml"/>
</mappers>
</configuration>
<environments>
配置的数据库链接信息,default 属性是标识默认使用那个数据库配置,里面的 <enviroment>
是具体的配置吗,可以有多个。
<transactionManager>
用来指定事务管理方式,有 JDBC 或 MANAGED 两种,JDBC 是手动管理事务,MANAGED 是交给第三方管理。
<dataSource>
数据源,用来提供 Connection 对象,这个 Connection 对象是实现了 JDK 中 javax.sql.DataSource 接口。常见的数据源有 Druid、C3P、DBCP 这些,这个标签 type 属性只能写 UNPOOLED|POOLED|JNDI 这三个值其中一个,UNPOOLED 不用连接池每次都建立 Connection 对象与数据库连接,POOLED 使用 Mybatis 内置连接池,JNDI 使用第三方连接池。配好了 type 对应还有些扩展的 <property>
标签可以使用,这在官网数据源(dataSource)这部分有说明,比如 poolMaximumActiveConnections 就是配置连接池连接数量 <property name="poolMaximumActiveConnections" value="5"></property>
。
<mappers>
配置的具体 ORM 文件,以后所有要执行的 SQL 都回到这些配置文件中找。这个 mappers 编写时是一个 mapper 对应一个表,文件名命名是驼峰风格,以表名打头 Mapper 结尾,以数据库 t_user 表为例,可以写成 UserMapper.xml。
创建 userMapper.xml。这里 <insert>
是代表要执行 insert 语句,如果要执行其他的语句可以改成 select/update/delete 任意一种,回到 insert 标签还写了属性 id,这个 id 在执行 SQL 时填写,到时候顺着 id 就能找到标签中的语句。<mapper>
namespace 属性作用是防止 insert/select/update/delete 这几个标签中的 ID 冲突,如果有冲突可以通过命名空间来区分到底调的是哪个 id,后面执行 SQL 会引用,一般填写 Mapper 文件全限定名。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
<!--插入用户-->
<insert id="insertUser">
INSERT INTO `t_user` (`name`, `password`, `realname`, `gender`, `tel`)
VALUES
('raingray', 'zhangsan', '123456', 'm', '13412341234')
</insert>
</mapper>
创建一个测试类来执行 SQL。
SqlSession sqlSession = null;
try {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 多个 <environment> 的情况下,可以根据 <environment> 的 id 属性,加载指定环境。
// SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "development");
// 加载默认环境
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
// 执行 SQL
System.out.println("插入了" + sqlSession.insert("insertUser") + "条数据");
// 提交事务
sqlSession.commit();
} catch (IOException e) {
// 出现异常回滚事务
if (sqlSession != null) {
sqlSession.rollback();
}
throw new RuntimeException(e);
} finally {
// 关闭数据库会话连接
if (sqlSession != null) {
sqlSession.close();
}
}
通过 sqlSession 对象执行 SQL 语句。要想获得这个对象得先 SqlSessionFactoryBuilder 加载 mybatis 配置文件得到 SqlSessionFactory 对象,再由 openSession 方法获得 sqlSession 对象。
这里调用 sqlSession.insert("insertUser")
方法会向找 <mappers>
中配置的 Mapper,挨个匹配 insert/select/update/delete 标签 id 属性值,只要是 insertOneUser 就获取里面 SQL 语句并执行。前面说了 Mapper 命名空间,这里要是有好几个 Mapper 里面都有重复的 id 呢?找到多个就不知道执行具体哪个 Mapper 语句,肯定会报错,这时候正确的写法应该指定命令空间避免歧义 sqlSession.insert("test.insertUser")
。
运行后确实成功插入数据,不过 SQL 参数是硬编码不灵活,到底怎么传参呢?这里再看看三种传参方式。
1.传递 Map 集合
Map 集合创建出来。
Map<String, Object> user = new HashMap<>();
user.put("nikeName", "test");
user.put("pass", "123456");
user.put("realName", "test");
user.put("gender", "M");
user.put("phone", "13412333333");
System.out.println("插入了" + sqlSession.insert("test.insertUser", user) + "条数据");
UserMapper.xml 接收的时候要用 #{}
,花括号中是 Key 的名称。#{}
等同于 JDBC 预编译的 PreparedStatement,会自动绑定 ?
占位符,里面可以传递参数名,要注意这个参数名称一定要和 Map 中的 Key 一样避免出错。另一种接收参数的方式是 ${}
这是 JDBC 的 Statement,传递参数采用字符串拼接的方式,有 SQL 注入风险,不建议用。
<!--使用 Map 传参-->
<insert id="insertUser">
INSERT INTO `t_user` (`name`, `password`, `realname`, `gender`, `tel`)
VALUES
(#{nikeName}, #{pass}, #{realName}, #{gender}, #{phone})
</insert>
2.传递 POJO 对象
先创建一个 User 的 POJO 类,对应 t_user 表,里面所有的字段命名使用驼峰风格,字段类型要使用包装类,因为后续在使用查询语句 ORM 把查询结果封装为对象,假如有个字段没有值 ORM 会自动处理成 null,此时赋值 null 给默认类型出现类型错误异常,由于包装类都是继承 Object,对象默认值就是 null,所以不会出现此问题。
package pojo;
public class User {
private Integer id;
private String name;
private String password;
private String realName;
private String gender;
private String tel;
public User() {
}
public User(Integer id, String name, String passowrd, String realName, String gender, String tel) {
this.id = id;
this.name = name;
this.password = passowrd;
this.realName = realName;
this.gender = gender;
this.tel = tel;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
", realName='" + realName + '\'' +
", gender='" + gender + '\'' +
", tel='" + tel + '\'' +
'}';
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setPassword(String password) {
this.password = password;
}
public void setRealName(String realName) {
this.realName = realName;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setTel(String tel) {
this.tel = tel;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public String getRealName() {
return realName;
}
public String getGender() {
return gender;
}
public String getTel() {
return tel;
}
}
UserMapper.xml 接收的时候参数需要注意格式,不能随便写,以 relName 为例,执行时会调 POJO 对象 getter 方法 getRealName()。从结果来看,参数规则是去掉 get 首字母小写。
<insert id="insertUser">
INSERT INTO `t_user` (`name`, `password`, `realname`, `gender`, `tel`)
VALUES
(#{name}, #{password}, #{realName}, #{gender}, #{tel})
</insert>
执行的时候就可以直接传递 POJO 对象进来。
// 3. 传递 POJO 对象
System.out.println("插入了" + sqlSession.insert("test.insertUser",
new pojo.User(null, "reklawjr", "password",
"真实名称", "W", "13412341233")) + "条数据");
3.传递其他类型
如果有些语句只需要传递一个参数,也是没问题的,这通常在 select/delete 操作指定行数据会遇到,比如 where id = ${id}
,中的 id 参数可以随便写,反正只传递一个包装类对象,但是参数命名最好要有含义,别一眼看过去不知道要穿什么参数。
sqlSession.insert("XXX", 1);
1.2 Delete
这里就刚好提到传参也可以穿单个对象,这里是传 Int 参数,没有报错的原因是自动装箱程 Intger。
System.out.println("删除了" + sqlSession.delete("test.deleteUserById", 29) + "行数据");
删除操作就把标签换成 delete。
<!--删除指定 id 的用户-->
<delete id="deleteUserById">
DELETE FROM `mybatis`.`t_user` WHERE `id` = ${id}
</delete>
1.3 Update
和前面 insert 是一样的操作。
// 1.传递 POJO 对象
User user = new User(26, "Jeff", "123qwe", "杰夫", "M", "12341234");
System.out.println("更新了" + sqlSession.update("test.updateUserById", user) + "行数据");
// 2.传 Map 集合
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("id", 27);
stringObjectHashMap.put("name", "test");
stringObjectHashMap.put("password", "123qwe");
stringObjectHashMap.put("realName", "真实姓名");
stringObjectHashMap.put("gender", "W");
stringObjectHashMap.put("tel", "13411111111");
System.out.println("更新了" + sqlSession.update("test.updateUserById", stringObjectHashMap) + "行数据");
只是 Mapper 中需要把标签名称换成 update。
<!--更新指定 id 的用户信息-->
<update id="updateUserById">
UPDATE `t_user` SET
`name` = #{name},
`password` = #{password},
`realname` = #{realName},
`gender` = #{gender},
`tel` = #{tel}
WHERE
`id` = ${id}
</update>
也尝试过不换,就保持 insert 照样能通过 id 找到 SQL 成功执行。
1.4 Select
1.用 selectOne()
查询一行数据
selectOne 执行完 SQL 后会自动调对应的 setter 方法封装成对象。
User o = sqlSession.selectOne("test.selectUserById", 27);
System.out.println(o);
具体封装成什么对象看 Mapper 中的 resultType 属性值,这个值是填 POJO 类的全限定名。
<!--根据用户 id 查询用户所有信息-->
<select id="selectUserById" resultType="pojo.User">
SELECT
*
FROM
`t_user`
WHERE
id = ${id}
</select>
要是发现封装的对象某些值不存在,很有可能是你的 POJO 类和查询结果集中的列名不一致,这需要你手动更正 POJO 类 setter 方法名或者查询列去别名。
2.用 selectList()
查询多行数据
Mapper 还是一样,写好 SQL 指定要封装成什么对象。
<!--查询用户表内所有用户信息-->
<select id="selectAllUser" resultType="pojo.User">
SELECT
*
FROM
`t_user`
</select>
查询出来这么多对象放哪里呢?一般是放集合里,这个集合是通过查询方法指定的,这里用 selectList 方法表示把封装完的对象都塞进 List 中。
List<User> users = sqlSession.selectList("test.selectAllUser");
users.forEach(user -> System.out.println(user));
1.5 开启 SQL 执行日志
可以启用内置的日志。
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
......
<configuration>
这样运行的时候能看到具体 SQL 和对应传递的参数,很方便排错。
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1978504976.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@75ed9710]
==> Preparing: UPDATE `t_user` SET `name` = ?, `password` = ?, `realname` = ?, `gender` = ?, `tel` = ? WHERE `id` = 49
==> Parameters: Jeff(String), 123qwe(String), 杰夫(String), M(String), 010-12341234(String)
<== Updates: 0
更新了0行数据
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@75ed9710]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@75ed9710]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@75ed9710]
Returned connection 1978504976 to pool.
如果要看日志输出更多信息可以配置官网提到的第三方日志工具。
1.6 IDEA 管理数据库
编写 Mapper.xml 中的 SQL 有时候会提示黄色告警,这是因为 IDEA 没有连接数据库,要是连接上了就不会告警,编写时还能直接有 SQL 提示,顺带侧边栏 DataSource 功能还能管理数据库功能也都有跟 Navicat 一样。
IDEA 侧边栏 Database 中点击➕,选择 DataSource 在里面找到你要连接的数据库。
配置数据库信息。
配置 jdbc-driver 才能连接到数据库。
进入 driver 配置有两个路径,第一个。
第二个,切到 Drivers 面板打字搜数据库名。默认情况是没有对应 jar 包的,点击 Download 会自动下载,不过默认下载到 %AppData%\JetBrains\IntelliJIdea2024.1\jdbc-drivers
,不好管理,建议把默认配置全部删掉,手动下载可以到 Maven 仓库找,这里下到单独的目录中。
之后就可以测试连接看是否成功,一切没问题后就可以管理数据库。
1.7 IDEA 建立配置文件模板
每次写 Mapper XML 配置文件都需要手动粘贴毕竟麻烦,打开 Ctrl + Alt + S 打开 IDEA 设置,找到 Editor -> File and Code Templates,新建模板,后续右键新建就自动生成此文件内容。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="填写 Mapper 接口全限定名">
</mapper>
2 接口代理执行 SQL
原来调对应 Mapper 对应 SQL 还需要传递 ID 值来找,这也是硬编码不太灵活。
sqlSession.selectOne("test.selectUserById", 27);
Mybatis 实现了动态代理机制,让你写完 Dao 的接口后,自动根据接口方法生成 DaoImpl 实现类,帮你完善里面所有的方法,后面调对应接口的方法就可以直接找到 Mapper 中标签的 id 获取对应 SQL 执行。这也是实际中的用法。
下面改造下硬编码从 Mapper 调 CRUD 方法的项目。整个编写顺序是先写接口,后写 Mapper,最后注册 Mapper 并运行测试代码。
2.1 编写接口
直接建立 mapper 包里面放接口用,每个接口命名都按照表名开头,Mapper 结尾,比如有个表叫 t_Order 可以写成 OrderMapper.java。
接口里面跟以前的 DAO 一样要写针对这个表的 CRUD 操作各种方法。注意,实现类不需要我们写,Mybatis 执行 SQL 的过程中动态代理帮我们生成并执行。
UserMapper.java。
package com.raingray.mapper;
import com.raingray.pojo.User;
import java.util.List;
public interface UserMapper {
/**
* 插入一个用户
* @param user POJO 对象
* @return 返回插入成功的行数
*/
int insertUser(User user);
/**
* 根据用户传递过来的对象更新数据库信息
* @param user POJO 对象
* @return 返回更新成功的行数
*/
int updateUserById(User user);
/**
* 根据用户 ID 删除这个用户
* @param id 用户ID
* @return 返回删除成功的行数
*/
int deleteUserById(int id);
/**
* 根据用户 ID 查询用户所有信息
* @param id 用户 ID
* @return 查询成功返回一个 POJO 对象
*/
User selectUserById(int id);
/**
* 查询数据库中所有 User 信息
* @return 返回 List 集合里面存放所有 POJO 对象
*/
List<User> selectAllUser();
}
2.2 编写 Mapper
在 mapper 包里创建一个跟接口同名 Mapper。最后 Maven 将这个 Mapper 打包进去。
UserMapper.xml。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.raingray.mapper.UserMapper">
<!--新增一个用户-->
<insert id="insertUser">
INSERT INTO `t_user` (`name`, `password`, `realname`, `gender`, `tel`)
VALUES
(#{name}, #{password}, #{realName}, #{gender}, #{tel})
</insert>
<!--删除指定 id 的用户-->
<delete id="deleteUserById">
DELETE FROM `mybatis`.`t_user` WHERE `id` = #{id}
</delete>
<!--更新指定 id 的用户信息-->
<update id="updateUserById">
UPDATE `t_user`
SET `name` = #{name},
`password` = #{password},
`realname` = #{realName},
`gender` = #{gender},
`tel` = #{tel}
WHERE
`id` = #{id}
</update>
<!--根据用户 id 查询用户所有信息-->
<select id="selectUserById" resultType="com.raingray.pojo.User">
SELECT * FROM `t_user` WHERE id = #{id}
</select>
<!--查询用户表内所有用户信息-->
<select id="selectAllUser" resultType="com.raingray.pojo.User">
SELECT * FROM `t_user`
</select>
</mapper>
这个 Mapper 的文件名要和接口名称 UserMapper 一样,里面 <mapper>
标签 namespace 属性要填接口全限定名,并且 CRUD 对应的标签 id 也要和接口方法名中的一样,这样后面执行 SQL 才能通过接口找到 Mapper 中具体 ID 对应的 SQL。
因为涉及到后面注册 Mapper,这里要强调下 UserMapper.xml 存放位置,这个 UserMapper.xml 文件要么你放到 mapper 包里和 UserMapper.java 接口放一起,设置最后打包的时候把 UserMapper.xml 一起打包进去(如果不想这么麻烦,就在 resources 资源目录创建相同的的包放进去,也可以)。
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
为什么这么做?因为后面注册的时候会指定接口所在包名,如果这个包里面没有 XxxMapper.xml 则会提示绑定异常。
org.apache.ibatis.binding.BindingException: Type interface UserMapper is not known to the MapperRegistry.
at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:47)
at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:940)
at org.apache.ibatis.session.defaults.DefaultSqlSession.getMapper(DefaultSqlSession.java:291)
at UserMapperTest.testInsertUser(UserMapperTest.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
2.2 注册 Mapper
创建 mybatis-config.xml 核心配置文件。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--引入数据库配置文件-->
<properties resource="jdbc.properties"/>
<!--配置日志-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--配置连接池-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--注册 Mapper-->
<mappers>
<!-- <mapper resource="mapper/UserMapper.xml"/>-->
<!-- <mapper class="com.raingray.mapper.UserMapper"/>-->
<!-- <package name="com.raingray.mapper"/>-->
<package name="com/raingray/mapper"/>
</mappers>
</configuration>
注册 Mapper,<mapper>
标签有三个属性,另外两个属性 resource 和 url 虽然可以使用,但是这里就不再用了。选用 class 属性,值填接口全限定名,这样会自动找对应包名下与接口相同文件名的 Mapper XML 文件。
<!--注册 Mapper-->
<mappers>
<!--使用 url 属性填绝对路径-->
<!--<mapper url=file:///D:/project/UserMapper.xml>-->
<!--<mapper url=file:///usr/local/project/UserMapper.xml>-->
<!--使用 resource 属性填类路径-->
<!--<mapper resource="mapper/UserMapper.xml">-->
<!--使用 class 属性,填包名。自动找接口同名的 Mapper-->
<mapper class="com.raingray.mapper.UserMapper"/>
</mappers>
为了防止以后接口越来越多每次都要注册导致文件行数太长,<package>
标签可以注册指定包下所有 Mapper XML 文件,具体配置是指定 name 属性的值,这个值需要配上存放 Mapper 接口目录的路径,或者存放接口的这个包全限定名。
<mappers>
<!--注册 com.raingray.mapper 包下所有 Mapper XML 文件-->
<package name="com.raingray.mapper"/>
<!--注册 com/raingray/mapper 目录下所有 Mapper XML 文件-->
<!--<package name="com/raingray/mapper"/>-->
</mappers>
除了注册 Mapper 这里还新增了 <properties resource="jdbc.properties"/>
,这个代表引入一个外部的属性配置文件,后续可以使用 ${}
引入对应的值,比如 ${driver}
就获取到键为 driver 的值 com.mysql.cj.jdbc.Driver。这么做的目的是防止以后核心配置文件太大了,不好找对应的配置所以单独拎出来。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=
2.3 测试运行
通过 SqlSession 的 getMapper) 方法动态代理的方式实现 UserMapper 接口,通过 UserMapper 调接口中的 deleteUserById 方法。这个删除方法被执行时,会找注册的 Mapper.xml 比对其 namespace 属性来确认是哪个 Mapper 文件,接着通过接口的方法名 deleteUserById 对比 Mapper.xml 中 CRUD 标签的 ID 找到 SQL 语句,最后根据你的语句类型执行 sqlSession 中的 selectOne/selectList/delete 之类的方法,总之这从接口实现到方法调用背后都是动态代理完成。
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
usermapper.deleteUserById(50);
完整的单元测试类如下。
import com.raingray.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;
import com.raingray.pojo.User;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class UserMapperTest {
@Test
public void testInsertUser() {
SqlSession sqlSession = null;
try {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User(null, "reklawjr", "password","真实名称", "W", "13412341234");
mapper.insertUser(user);
sqlSession.commit();
} catch (IOException e) {
if (sqlSession != null) {
sqlSession.rollback();
}
throw new RuntimeException(e);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
@Test
public void testSelectAllUser () {
{
SqlSession sqlSession = null;
try {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
UserMapper usermapper = sqlSession.getMapper(UserMapper.class);
List<User> users = usermapper.selectAllUser();
users.forEach(user -> System.out.println(user));
sqlSession.commit();
} catch (IOException e) {
if (sqlSession != null) {
sqlSession.rollback();
}
throw new RuntimeException(e);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
}
3 Mapper 标签参数类型
3.1 SQL 形参类型
3.1.1 手动指定传参类型
前面说过 SQL 硬编码很不灵活需要传参,按理说 SQL 映射文件 Mapper.xml 需要手动指定一个要接收参数的类型,以前没写是因为 Mybatis 根据传递的参数自动识别出其类型。
将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以根据语句中实际传入的参数计算出应该使用的类型处理器(TypeHandler),默认值为未设置(unset)。
手动指定参数类型是用 parameterType 属性值,下面看看各种情况下如何处理参数。
select,insert,update 和 delete 标签都有 parameterType 属性,值填全限定名,比如下面明确表名传入的参数类型是包装类 Long。
<select parameterType="java.lang.Long">
......
</select>
但是 Mybatis 为这些简单类型都做了别名。
别名 映射的类型 _byte byte _char (since 3.5.10) char _character (since 3.5.10) char _long long _short short _int int _integer int _double double _float float _boolean boolean string String byte Byte char (since 3.5.10) Character character (since 3.5.10) Character long Long short Short int Integer integer Integer double Double float Float boolean Boolean date Date decimal BigDecimal bigdecimal BigDecimal biginteger BigInteger object Object date[] Date[] decimal[] BigDecimal[] bigdecimal[] BigDecimal[] biginteger[] BigInteger[] object[] Object[] map Map hashmap HashMap list List arraylist ArrayList collection Collection iterator Iterator
因此 Long 可以简写成 long。
<select parameterType="long">
......
</select>
如果 Mapper 接口中有两个不同类型的参数,Mapper 映射文件中怎么通过 parameterType 指定这两个参数的类型?实际上不用写参数类型可以交由 Mybatis 自动推断。下面就来看看 Mapper 接口有多个参数,Mapper 映射文件中如何全部引用。
3.1.2 传输多个参数
有时候接口会传输多个参数,对应 Mapper 该怎么获取参数?
/**
* 通过用户 id,手机号和昵称查询用户信息
* @param name 用户名
* @param id 用户id
* @return 查询成功返回一个 POJO 对象
*/
User selectUserByNameAndIdAndTel(String name, Integer id, String tel);
其实 Mybatis 把这些参数都放到 Map 集合中了,通过两个 key 来获取数据,一个是 arg 另一个是 param,这个 key 是按照接口参数位置顺序排序,比如接口的第一个参数是 name 那么对应 key 也是 arg0 和 param1,这个数随着参数数量自动增加。
map.put("arg0", name);
map.put("arg1", id);
map.put("arg0", tel);
......
map.put("param1", name);
map.put("param2", id);
map.put("param3", tel);
......
最后直接在 Mapper 跟正常 Map 集合方式取参数。
<!--根据用户 id 和用户名查询用户所有信息-->
<select id="selectUserByNameAndIdAndTel" resultType="com.raingray.pojo.User">
SELECT
*
FROM
`t_user`
WHERE
id = #{arg1} AND name = #{arg0} AND tel = #{arg2}
<!--id = #{param2} AND name = #{param1} AND tel = #{param3}-->
</select>
但这个 key 的辨识度实在太低,不太好分辨具体 key 对应那个参数,这时候 Mybatis 有提供注解 org.apache.ibatis.annotations.Param 来解决这个问题,它只有一个元素 value,使用的时候直接传值 @Param("别名")
。
直接在接口中定义即可。
/**
* 通过用户 id,手机号和昵称查询用户信息
* @param name 用户名
* @param id 用户id
* @return 查询成功返回一个 POJO 对象
*/
User selectUserByNameAndIdAndTel2(@Param("name") String name, @Param("id") Integer id, @Param("tel") String tel);
用了注解只能使用注解的值和 param 打头的参数。
<!--根据用户 id 和用户名查询用户所有信息-->
<select id="selectUserByNameAndIdAndTel" resultType="com.raingray.pojo.User">
SELECT
*
FROM
`t_user`
WHERE
id = id = #{id} AND name = #{name} AND tel = #{tel}
<!--id = #{param2} AND name = #{param1} AND tel = #{param3}-->
</select>
3.2 SQL 查询结果映射
3.2.1 返回类型
查询结果前面在 1.1 Insert 小节已经接触过,就是指定 resultType 可以返回什么类型的数据。这次再看看 Map 集合和其他的类型怎么使用。
1.返回 Map
Mapper 接口使用 Map 返回值。
Map<String, Object> selectUserById(Integer id);
在 resultType
属性传别名 map。要是忘了别名是什么可以回顾 3.1.1 手动指定传参类型小节内容。
<!--测试返回 map 集合-->
<select id="selectUserById" resultType="map">
SELECT
`id`, `name`, `password`, t_realname as tesaa, `gender`, `tel`
FROM
`t_user`
WHERE
id = #{id}
</select>
执行方法
UserMapper usermapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> stringObjectMap = usermapper.selectUserById(21);
System.out.println(stringObjectMap);
System.out.println(stringObjectMap.get("id"));
System.out.println(stringObjectMap.getClass());
看结果确实封装成 HashMap 集合。
{tesaa=asdasdsad, password=cJDLi6lTb2, gender=F, name=Hayashi Seiko, tel=YbEzm4aRVT, id=21}
21
class java.util.HashMap
如果查询很多条数据也可以把封装的 Map 放到一个 List 集合中
List<Map> selectUserById(Integer id);
2.返回 Map,使用自增主键作为 Key
在 Mapper 接口方法上用 @MapKey 注解,值填结果集中列名,这样 Mapper XML 中 resultType=map,返回 Map 的 key 就会用 SQL 结果集中对应列名的值。
@MapKey("id")
Map<Integer, Object> selectUserById2(Integer id);
Mapper 中也要确保对应 id 这个列名称存在。
<select id="selectUserById2" resultType="map">
SELECT
`id`, `name`, `password`, t_realname as tesaa, `gender`, `tel`
FROM
`t_user`
</select>
最后测试
Map<Long, Object> stringObjectMap = usermapper.selectUserById2();
System.out.println(stringObjectMap);
Object x = stringObjectMap.get(27L); // Map 中就是对数据库中数字处理就是返回 long
System.out.println(stringObjectMap.getClass());
一个大集合中成功使用 id 作为键,值是对象集合。
{44={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=44}, 45={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=45}, 46={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=46}, 47={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=47}, 48={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=48}, 49={tesaa=杰夫, password=123qwe, gender=M, name=Jeff, tel=12341234, id=49}, 51={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=51}, 52={tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=52}, 21={tesaa=asdasdsad, password=cJDLi6lTb2, gender=F, name=Hayashi Seiko, tel=YbEzm4aRVT, id=21}, 22={tesaa=Joanne Hill, password=l5TTQEbCR7, gender=F, name=Joanne Hill, tel=tBSJCzsiju, id=22}, 23={tesaa=Ku Siu Wai, password=VPGbbUWOwl, gender=F, name=Ku Siu Wai, tel=XZqW7HmEpf, id=23}, 24={tesaa=Leslie Wright, password=gaqnfwbloS, gender=F, name=Leslie Wright, tel=2hEpoKYCR8, id=24}, 25={tesaa=Hashimoto Ayano, password=YpHCZBLvn6, gender=F, name=Hashimoto Ayano, tel=5LLVDVuUEd, id=25}, 26={tesaa=Taniguchi Nanami, password=WDFWZfvKqS, gender=M, name=Taniguchi Nanami, tel=IEkrs76WQv, id=26}, 27={tesaa=真实姓名, password=123qwe, gender=W, name=test, tel=13411111111, id=27}}
{tesaa=真实名称, password=password, gender=W, name=reklawjr, tel=13412341234, id=44}
class java.util.HashMap
这个集合 key 只能使用 id?不是这样的,可以使用任何键。只是要小心如果 key 一样可能原来的值会被覆盖导致数据不完整。
3.返回基础数据类型
也可以返回其他包装类,比如 SQL 只查某个行数据中的一个,那么 resultType 就可以设置对应的别名来返回。比如 resultType="long"。
3.2.2 手动映射 POJO 类属性和结果集列名
以前 POJO 类属性和 SQL 查询结果集列名对不上,就调不到属性最终封装成对象对应的属性就是 null。为正确赋值,解决方案有两种,首先是 SQL 查询我们把查询的列取别名,这样结果集列名与 POJO 类对象属性名一样才能够能正确赋值。
Mapper 接口 com.raingray.User。
public class User {
private Integer id;
private String name;
private String password;
private String gender;
private String tel;
private String tesaa;
}
数据库对应列名。
mysql> select * from t_user;
+----+------------------+------------+------------------+--------+-------------+
| id | name | password | t_realname | gender | tel |
+----+------------------+------------+------------------+--------+-------------+
| 21 | Hayashi Seiko | cJDLi6lTb2 | asdasdsad | F | YbEzm4aRVT |
| 22 | ........... | ........ . | .......... | F | ......... |
| 52 | reklawjr | password | 真实名称 | W | 13412341234 |
+----+------------------+------------+------------------+--------+-------------+
从数据库查询结果集列名来看,此时接口中没有 t_realname 这个属性,那么编写 Mapper 的时候就需要使用 AS 关键字对结果集列名做别名,取名为 tesaa,这样才能正确封装。
<!--根据 id 查询用户信息-->
<select id="selectUserByNameAndIdAndTel2" resultType="com.raingray.pojo.User">
SELECT
`id`, `name`, `password`, t_realname AS tesaa, `gender`, `tel`
FROM
`t_user`
WHERE
id = #{id}
</select>
另一种方式是用 <resultMap>
手动指定结果集与 POJO 类属性的映射关系,把属性名和结果集列名不一样的修正即可。
<resultMap id="userMap" type="com.raingray.pojo.User">
<id property="id" column="id"/>
<result property="tesaa" column="t_realname"/>
</resultMap>
<select id="selectUserByNameAndIdAndTel2" resultMap="userMap">
SELECT
`id`, `name`, `password`, `t_realname`, `gender`, `tel`
FROM
`t_user`
WHERE
id = #{id}
</select>
<resultMap>
属性 id 值是定义返回结果标识,后面需要在 select/insert/delete/update 标签中 resultMap 属性填写表示引用的是谁。type 属性是要封装成什么对象,填类全限定名。
<result>
property 属性是 POJO 类属性名,column 是 SQL 查询结果集列名,将这两个名称填写正确就能对应上。
id
不是强制填写,id 标签和 result 标签一样,只是 id 是要填主键的结果集和 POJO 类主键的属性名,这个填写上可以增加 Mybatis 运行效率。
https://mybatis.org/mybatis-3/zh_CN/sqlmap-xml.html#Result_Maps
https://mybatis.org/mybatis-3/zh_CN/sqlmap-xml.html#Auto-mapping
4 动态 SQL
动态 SQL 就是根据不同的参数拼接不同 SQL。以前 JDBC 需要手动判断,这里通过类似于 JSTL 标签解决。
4.1 if
if 标签通过 test 属性来比较条件,一旦为 True 就会拼接标签中的 SQL,需要注意 test 属性逻辑运算符只能用 and 和 or,具体能用哪些,见下表。
类别 | 运算符 | 备注 |
---|---|---|
算数运算符 | + 、- 、* 、/ 、% | |
关系运算符 | eq 、== != 、neq > 、gt >= 、get < 、lt <= 、let | == 可用 eq 平替,其他的也是一样。 |
逻辑运算符 | ! 、not <br/>and or | ! 和 not 都是将表达式运算结果 boolean 值取反。 |
取值运算符 | [] 、. | 在数组中取值,比如 Object[0] 取 Object 中的第一个元素,或者是 set.Age 执行 set 名为 Age 的属性。 |
MyBatis中的OGNL教程_mybatis ogbl-CSDN博客
最常见的一个场景,如果 test="id != null"
传参不是 null 就按照参数筛选,执行 SELECT * FROM t_user WHERE id = #{id}
,不传我就查所有 SELECT * FROM t_user
。传的参数如果是字符串类型还可以判断是否不是空字符串 test="id != ''"
<!--测试 if-->
<select id="selectUserByIdTestIf" parameterType="int" resultType="map">
SELECT * FROM t_user
<if test="id != null">
WHERE id = #{id}
</if>
</select>
4.2 where
在 if 标签中只有一个条件查询,需要手动添加 WHERE 关键字,假如有多个条件呢?这里新增了一个 gender 查询条件。
<!--测试 if-->
<select id="selectUserByIdTestIf" parameterType="int" resultType="map">
SELECT * FROM t_user
<if test="id != null">
WHERE id = #{id}
</if>
<if test="gender != null and gender != ''">
AND gender = #{gender}
</if>
</select>
id 和 gender 都有值的情况下 SQL 查询没问题。
SELECT * FROM t_user WHERE id = ? AND gender = ?
如果 id 不传值呢?SQL 最终拼接后肯定存在语法错误。
SELECT * FROM t_user AND gender = ?
where 标签在子标签有值的情况下自动对 SQL 拼接 where 关键字,如果 SQL 只有一个条件语句会自动把 AND/OR 删除避免拼接导致 SQL 语法错误。
比如前面出现出现 if 标签单语句的情况下拼接错误就可以解决。
<!--测试 if-->
<select id="selectUserByIdTestIf" parameterType="int" resultType="map">
SELECT * FROM t_user
<where>
<if test="id != null">
WHERE id = #{id}
</if>
<if test="gender != null and gender != ''">
AND gender = #{gender}
</if>
</where>
</select>
哪怕 id 值是空的,只有 gender 有值,最终 SQL 会剔除开头的 AND 关键字,保证 SQL 不受语法影响。
SELECT * FROM t_user WHERE gender = ?
如果子标签都没值,WHERE 关键字会自动剔除。
SELECT * FROM t_user
4.3 trim
trim 用作控制标签内 SQL 语句前缀后缀,有四个属性,prefix 属性在语句开头添加前缀,prefixOverrides 剔除前缀,suffix 天后缀,suffixOverrides 剔除后缀。如果 trim 内子标签没有值,则不进行操作。
比如下面的查询语句,自动添加 WHERE 关键字和后缀 ORDER BY,如果 Trim 语句开头还出现 WHERE
或 AND
字符就替换为空,如果语句结尾出现 TEST 就替换为空。
<select id="selectUserByIdTestTrim" resultType="map">
SELECT * FROM t_user
<trim prefix="WHERE" prefixOverrides="WHERE |AND " suffixOverrides="TEST" suffix="ORDER BY 1">
<if test="id != null">
WHERE id = #{id}
</if>
<if test="gender != null">
AND gender = #{gender} TEST
</if>
</trim>
</select>
只传 gender 参数,对应 SQL。
<!--没有 trim 处理的原始 SQL-->
SELECT * FROM t_user AND gender = ? TEST
<!--有 trim 处理后的 SQL-->
SELECT * FROM t_user WHERE gender = ? ORDER BY 1"
gender 和 id 都传递,对应的 SQL。
<!--没有 trim 处理的原始 SQL-->
SELECT * FROM t_user WHERE id = ? AND gender = ? TEST
<!--有 trim 处理后的 SQL-->
SELECT * FROM t_user WHERE id = ? AND gender = ? ORDER BY 1
4.4 choose
多条件判断,最终只执行其中一个条件的语句。
when 条件成立拼接其中语句,其他 when 不再判断,假如所有 when 条件不成立拼接 otherwise 内语句。
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
等同于 Java 的 if,else-if,else。
if (title != null) {
......
} else if (author != null && author.name != null) {
......
} else {
......
}
在测试中发现也可以不写 otherwise,不写的话这就跟 if 标签没区别了,那还不如直接使用 if,免得多套一层 when。
4.5 set
set 标签中有值自动填完 SET 关键字,可以在更新数据的场景用到,比如指定更新某几个字段的内容。
set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号
<update id="selectUserByIdTestSet">
UPDATE `t_user`
<set>
<if test="name neq null">`name` = #{name},</if>
<if test="password neq null">`password` = #{password},</if>
<if test="tesaa neq null">`t_realname` = #{tesaa},</if>
<if test="gender neq null">`gender` = #{gender},</if>
<if test="tel neq null">`tel` = #{tel}</if>
</set>
<where>
<if test="id != null and id > 0">`id` = #{id}</if>
</where>
</update>
set 里面的 if 标签都有值的情况下,自动添加 SET 关键字。
UPDATE `t_user` SET `name` = ?, password` = ?, t_realname` = ?, `gender` = ?, `tel` = ? WHERE id = ?
set 标签只有 name 有值,自动添加 SET 关键字,顺带把末尾的逗号给去除。
UPDATE `t_user` SET `name` = ? WHERE `id` = ?
4.6 foreach
循环集合或者数组进行拼接,常用在批量增加数据和批量删除数据的场景。
<select id="selectById" resultMap="map">
SELECT * FROM user
WHERE id IN
<foreach collection="idArray" separator="," item="element" open="(" close=")">
#{element}
</foreach>
</select>
collection 属性是要遍历的集合或者数组,没有用注解自定义参数名,默认属性名称是 array 或者是 arg0。
item 属性是循环当前的对象,这个名称可以自己取。
separator 属性是定义 item 属性对象末尾分隔符,比如每次循环获取到了 item 后都会自动在后面添加逗号。假设 idArray 一个 4 个参数,所有参数循环完毕后都在后面加上逗号分隔符,而且很智能的最后一个参数不会加,最终样子如下。
element , element , element , element
begin 和 close 属性是在循环完成后要在拼接的内容,begin 是左侧,close 是右侧,比如这里写的左侧添加 (
,右侧添加 )
。
(element , element , element , element)
下面看一个批量 INSERT 多行数据的操作。
Mapper 接口。
Integer selectUserByIdTestForeach(@Param("users") List<User> user);
直接提交 4 个 User 对象的 List 给 Mapper。
ArrayList<User> arrayList = new ArrayList<>();
arrayList.add(new User(null,"zhangliu", "123456", "", "", ""));
arrayList.add(new User(null,"zhangwu", "123456", "", "", ""));
arrayList.add(new User(null,"zhangsi", "123456", "", "", ""));
arrayList.add(new User(null,"zhangsan", "123456", "", "", ""));
Integer stringObjectMap4 = usermapper.selectUserByIdTestForeach(arrayList);
Mapper 映射文件。
<!--测试 foreach-->
<insert id="selectUserByIdTestForeach">
INSERT INTO t_user VALUES
<foreach collection="users" item="user" separator=",">
<trim prefix="(" suffix=")">
#{user.id},
#{user.name},
#{user.password},
#{user.tesaa},
#{user.tel},
#{user.gender}
</trim>
</foreach>
</insert>
循环 users 列表,每个 user 是对应的 User 对象,通过 user.xxx 获取对象的属性值,最后交由 trim 给加上小括号变成:
( ?, ?, ?, ?, ?, ? )
每次循环完成一次,给 user 后面加个逗号,循环完后就变成:
( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? )
最终 SQL 如下。
INSERT INTO t_user VALUES ( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ? )
4.7 sql
定义 SQL 后,可以在别处引用。方便复用 SQL。
<sql id="......">
......
</sql
通过 include 引用。
<select id="selectUserByIdTestChoose" resultType="map">
SELECT <include refid="......"/> FROM t_user
</select>
5 多表关联查询结果映射
就是对应着外连接和内连接查询,数据量大的情况下不要用,因为会有笛卡尔乘积的问题出现,A 表有 1000 条数据 B 表也有 5 条数据,那么链接查询最终会查 1000 × 5 次。
这里建立两个表练习关联查询。
c_class
-- ----------------------------
-- Table structure for c_class
-- ----------------------------
DROP TABLE IF EXISTS `c_class`;
CREATE TABLE `c_class` (
`c_id` int NOT NULL COMMENT '班级 ID',
`c_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '班级名称',
PRIMARY KEY (`c_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of c_class
-- ----------------------------
INSERT INTO `c_class` VALUES (1, '2013级计算机科学技术2班');
INSERT INTO `c_class` VALUES (2, '2013级网络工程1班');
SET FOREIGN_KEY_CHECKS = 1;
t_student
-- ----------------------------
-- Table structure for t_student
-- ----------------------------
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE `t_student` (
`s_id` int NOT NULL COMMENT '学生ID ',
`s_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学生名称',
`c_id` int NULL DEFAULT NULL COMMENT '班级ID',
PRIMARY KEY (`s_id`) USING BTREE,
INDEX `c_id`(`c_id` ASC) USING BTREE,
CONSTRAINT `t_student_ibfk_1` FOREIGN KEY (`c_id`) REFERENCES `c_class` (`c_id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_student
-- ----------------------------
INSERT INTO `t_student` VALUES (1, '张三', 1);
INSERT INTO `t_student` VALUES (2, '李四', 1);
INSERT INTO `t_student` VALUES (3, '王五', 2);
INSERT INTO `t_student` VALUES (4, '赵乾', 2);
INSERT INTO `t_student` VALUES (5, '孙吴', 1);
INSERT INTO `t_student` VALUES (6, '琪琪', NULL);
SET FOREIGN_KEY_CHECKS = 1;
5.1 多对一
5.1.1 级联(关联)查询
多行数据对应一行数据,或者说多行数据关联到一行数据上,主体是多行数据,可以叫主表,另一行数据叫副表,哪怕副表数据没查到也没关系,但是主表的要全部展示出来。
对应到真实需求是要从多个学生中找到他们每个人自己的信息和对应班级信息,哪怕这个学生没有班级,学生信息也要查出来。
先建立 c_class 和 t_student 表对应 POJO 类。
com.raingray.pojo.Class
package com.raingray.pojo;
public class Class {
private Integer id;
private String name;
public Class() {
}
public Class(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Class{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
com.raingray.pojo.Student。这里的学生类特意封装一个类型是 CLass 的 clazz 班级属性,后续把查询到的班级信息可以放到这个属性中,比如把班级信息封装对象塞进这个 clazz 属性中。
package com.raingray.pojo;
public class Student {
public Student() {
}
private Integer id;
private String name;
private Class clazz;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Class getclazz() {
return clazz;
}
public void setclazz(Class clazz) {
this.clazz = clazz;
}
public Student(Integer id, String name, Class clazz) {
this.id = id;
this.name = name;
this.clazz = clazz;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", clazz=" + clazz +
'}';
}
}
创建 Mapper 接口 com/raingray/mapper/StudentMapper.java。
package com.raingray.mapper;
import com.raingray.pojo.Student;
import java.util.List;
public interface StudentMapper {
List<Student> selectUserInfoById(Integer id);
}
最后就是根据接口方法编写对应 Mapper 映射文件 com/raingray/mapper/StudentMapper.xml。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.raingray.mapper.StudentMapper">
<!--多对一,第一种方式级联查询-->
<resultMap id="StudentMapper" type="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<result property="clazz.id" column="c_id"/>
<result property="clazz.name" column="c_name"/>
</resultMap>
<select id="selectUserInfoById" resultMap="StudentMapper">
SELECT
s_id, s_name, c_name, c_class.c_id
FROM t_student
LEFT OUTER JOIN c_class ON t_student.c_id = c_class.c_id
WHERE
s_id = #{id}
</select>
</mapper>
SQL 编写是采用左外联查询,把 t_student 的信息查出来后,去比对 t_student.c_id 班级 id 是否和 c_class.c_id 一致,如果一致就从展示出来,不一致就以 NULL 展示。
mysql> SELECT s_id,
-> s_name,
-> c_name,
-> c_class.c_id
-> FROM t_student LEFT OUTER
-> JOIN c_class
-> ON t_student.c_id = c_class.c_id;
+------+--------+-------------------------+------+
| s_id | s_name | c_name | c_id |
+------+--------+-------------------------+------+
| 1 | 张三 | 2013级计算机科学技术2班 | 1 |
| 2 | 李四 | 2013级计算机科学技术2班 | 1 |
| 3 | 王五 | 2013级网络工程1班 | 2 |
| 4 | 赵乾 | 2013级网络工程1班 | 2 |
| 5 | 孙吴 | 2013级计算机科学技术2班 | 1 |
| 6 | 琪琪 | NULL | NULL |
+------+--------+-------------------------+------+
6 rows in set (0.00 sec)
查询出来的结果集怎么封装才是关键,这里选择用 <resultMap>
把查到的学生信息封装成 POJO 类 com.raingray.pojo.Student,由于 Student 的 POJO 类属性和列名对不上,这种情况封装结果肯定是 NULL,所以需要用 result 标签把属性 id 映射成 SQL 查询结果集中列名 s_id,属性 name 映射程 s_name。
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
只是这个 clazz.id 和 clazz.name 什么意思?其实 clazz 就是 Student 中 clazz 属性,这里通过指定课程表 clazz 对应 POJO 类 com.raingray.pojo.Class 的属性 id 和 name,把他们为对应查询结果集列名 c_id 和 c_name,这样最终 Mybatis 会自动创建 com.raingray.pojo.Class 对象自动赋值。
<result property="clazz.id" column="c_id"/>
<result property="clazz.name" column="c_name"/>
编写单元测试类 TestStudentMapper.java 验证结果。
import com.raingray.mapper.StudentMapper;
import com.raingray.pojo.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
public class TestStudentMapper {
@Test
void testSelectUserByNameAndIdAndTel2() {
SqlSession sqlSession = null;
try {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
// 调用接口对应方法,查询用户id是1的用户信息和班级信息
StudentMapper student = sqlSession.getMapper(StudentMapper.class);
System.out.println(student.selectUserInfoById(1));
sqlSession.commit();
} catch (IOException e) {
if (sqlSession != null) {
sqlSession.rollback();
}
throw new RuntimeException(e);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
运行测试方法,确实把班级信息封装 Class 类中,最后赋值 Student.clazz 属性。
==> Preparing: SELECT s_id, s_name, c_name, c_class.c_id FROM t_student LEFT OUTER JOIN c_class ON t_student.c_id = c_class.c_id WHERE s_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name, c_name, c_id
<== Row: 1, 张三, 2013级计算机科学技术2班, 1
<== Total: 1
[Student{id=1, name='张三', clazz=Class{id=1, name='2013级计算机科学技术2班'}}]
5.1.2 association 查询
resultMap 子标签 association 是比直接写 SQL 调理更加清晰的操作方式,直接指定关联查询的结果映射关系。
association 标签 property 属性是指定关联查询结果属性,这里是要映射 Student.clazz,其类型用 javaType 属性指定要封装成的 POJO 类全限定名,子标签 result 依旧是查询结果集列名和 Class 类属性不一致的情况下手动做的映射关系,如果一致就没必要写。
Mapper 映射文件 StudentMapper.xml。
<!--多对一,第二种方式 association-->
<resultMap id="StudentMapper2" type="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<association property="clazz" javaType="com.raingray.pojo.Class">
<result property="id" column="c_id"/>
<result property="name" column="c_name"/>
</association>
</resultMap>
<select id="selectUserInfoById2" resultMap="StudentMapper2">
SELECT
s_id, s_name, c_name, c_class.c_id
FROM t_student
LEFT OUTER JOIN c_class ON t_student.c_id = c_class.c_id
<where>
<if test="s_id != null and s_id > 0">s_id = #{id}</if>
</where>
</select>
运行测试类输出结果。
==> Preparing: SELECT s_id, s_name, c_name, c_class.c_id FROM t_student LEFT OUTER JOIN c_class ON t_student.c_id = c_class.c_id WHERE s_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name, c_name, c_id
<== Row: 1, 张三, 2013级计算机科学技术2班, 1
<== Total: 1
[Student{id=1, name='张三', clazz=Class{id=1, name='2013级计算机科学技术2班'}}]
5.1.3 分步查询
分布查询大概原理是把左外连接按步骤拆分成两个 SQL 语句执行,先查学生信息,顺带把班级 id 查到,后把用户表中查到的班级 id 传入到查询班级信息的 SQL 中查询,最后把指定 ID 的学生信息和对应班级 id 的班级信息汇总到一个结果集中。
# 查询学生信息
SELECT s_id, s_name, c_id FROM t_student WHERE s_id = ?
# 查询班级信息
SELECT c_id AS id, c_name AS name FROM c_class WHERE c_id = ?
实际 Mapper 映射文件编写也是分两步,步骤一,先写 Student.xml 查询到用户信息。
这里使用 association 做关联映射,select 属性就是要把查询结果传到哪个 Mapper 接口方法中,这里是 com.raingray.mapper.ClassMapper.getClassById 班级接口查询方法,column 就是要传递的查询结果集,这里是班级 id。
<!--多对一,第三种方式分布查询,步骤一-->
<resultMap id="selectUserInfoSetup1" type="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<association property="clazz" select="com.raingray.mapper.ClassMapper.getClassById" column="c_id"/>
</resultMap>
<select id="selectUserInfoById" resultMap="selectUserInfoSetup1">
SELECT s_id, s_name, c_id FROM t_student
<where>
<if test="id != null and id > 0">s_id = #{id}</if>
</where>
</select>
步骤二,创建 Mapper 接口 com/raingray/mapper/ClassMapper.java。在里面添加个 getClassById 方法,一会儿会被查询 SQL 的语句调用传参。
package com.raingray.mapper;
import com.raingray.pojo.Class;
public interface ClassMapper {
Class getClassById(Integer id);
}
创建 ClassMapper.xml,去根据传进来的 id 查询班级信息,将其封装成 Class POJO 类。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.raingray.mapper.ClassMapper">
<!--多对一,第三种方式分布查询,步骤二-->
<select id="getClassById" resultType="com.raingray.pojo.Class">
SELECT
c_id AS id, c_name AS name
FROM c_class
WHERE
c_id = #{id}
</select>
</mapper>
运行测试程序
StudentMapper student = sqlSession.getMapper(StudentMapper.class);
System.out.println(student.selectUserInfoById(1));
从日志中会发现确实是执行的两个 SQL,首先是查询用户信息,最后把查到的 c_id 传到了查询班级 SQL 中,获取到班级相关信息,最后把有关联的两个查询结果集拼接融合。
==> Preparing: SELECT s_id, s_name, c_id FROM t_student WHERE s_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name, c_id
<== Row: 1, 张三, 1
====> Preparing: SELECT c_id AS id, c_name AS name FROM c_class WHERE c_id = ?
====> Parameters: 1(Integer)
<==== Columns: id, name
<==== Row: 1, 2013级计算机科学技术2班
<==== Total: 1
<== Total: 1
[Student{id=1, name='张三', clazz=Class{id=1, name='2013级计算机科学技术2班'}}]
尝试下查询所有信息。
StudentMapper student = sqlSession.getMapper(StudentMapper.class);
System.out.println(student.selectUserInfoById(null));
确实用户对应班级关系也都正确的匹配到。
==> Preparing: SELECT s_id, s_name, c_id FROM t_student
==> Parameters:
<== Columns: s_id, s_name, c_id
<== Row: 1, 张三, 1
====> Preparing: SELECT c_id AS id, c_name AS name FROM c_class WHERE c_id = ?
====> Parameters: 1(Integer)
<==== Columns: id, name
<==== Row: 1, 2013级计算机科学技术2班
<==== Total: 1
<== Row: 2, 李四, 1
<== Row: 3, 王五, 2
====> Preparing: SELECT c_id AS id, c_name AS name FROM c_class WHERE c_id = ?
====> Parameters: 2(Integer)
<==== Columns: id, name
<==== Row: 2, 2013级网络工程1班
<==== Total: 1
<== Row: 4, 赵乾, 2
<== Row: 5, 孙吴, 1
<== Row: 6, 琪琪, null
<== Total: 6
[Student{id=1, name='张三', clazz=Class{id=1, name='2013级计算机科学技术2班'}}, Student{id=2, name='李四', clazz=Class{id=1, name='2013级计算机科学技术2班'}}, Student{id=3, name='王五', clazz=Class{id=2, name='2013级网络工程1班'}}, Student{id=4, name='赵乾', clazz=Class{id=2, name='2013级网络工程1班'}}, Student{id=5, name='孙吴', clazz=Class{id=1, name='2013级计算机科学技术2班'}}, Student{id=6, name='琪琪', clazz=null}]
从结果来看分布查询优点是班级表的 SQL 可以被重复使用。
5.1.4 分步查询启用延迟加载
在前面分布查询中为了获取班级相关信息我们需要查询两条 SQL,有时候不会使用到班级信息是不是可以不查呢,这样效率高还省事,只有调用这个班级的属性时我们才执行获取班级信息的 SQL,使用延迟加载可以做到。
在分布查询步骤一 Student.xml 映射文件 association 添加 fetchType 属性,值是 lazy,表示启用延迟加载,eager 不启用,这也是默认值。
<!--多对一,第三种方式分布查询,步骤一-->
<resultMap id="selectUserInfoSetup1" type="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<association property="clazz" select="com.raingray.mapper.ClassMapper.getClassById" fetchType="lazy" column="c_id"/>
</resultMap>
<select id="selectUserInfoById3" resultMap="selectUserInfoSetup1">
SELECT s_id, s_name, c_id FROM t_student
<where>
<if test="id != null and id > 0">s_id = #{id}</if>
</where>
</select>
测试程序中获取学生名称。
List<Student> students1 = student.selectUserInfoById(1);
System.out.println(students1.getFirst().getName());
查询日志就没有执行查询班级的 SQL。
==> Preparing: SELECT s_id, s_name, c_id FROM t_student WHERE s_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name, c_id
<== Row: 1, 张三, 1
<== Total: 1
张三
如果我查询了班级信息。
List<Student> students1 = student.selectUserInfoById(1);
System.out.println(students1.getFirst().getName());
此时才会执行查询班级信息的 SQL。
==> Preparing: SELECT s_id, s_name, c_id FROM t_student WHERE s_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name, c_id
<== Row: 1, 张三, 1
<== Total: 1
==> Preparing: SELECT c_id AS id, c_name AS name FROM c_class WHERE c_id = ?
==> Parameters: 1(Integer)
<== Columns: id, name
<== Row: 1, 2013级计算机科学技术2班
<== Total: 1
2013级计算机科学技术2班
association 的 fetchType=lazy
,只是表示这一个分布查询启用,如果有多个的情况下,可以到 MyBatis 核心配置文件中 settings 标签添加对应设置配置。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
更改核心配置文件,唯一需要注意的是各个标签顺序,比如 properties 一定是要放到最前头,mappers 是最后面,比如本次的 settings 就是要放到 properties 后面,一旦排序错误就无法运行 Mybatis。
properties
settings
typeAliases
typeHandlers
objectFactory
objectWrapperFactory
reflectorFactory
plugins
environments
databaseIdProvider
mappers
5.2 一对多
一行数据对应多行数据,主体是一行数据。
具体需求是查询所有班级信息,每条班级信息中包含学生信息,哪怕班级里没有学生也要查出来,对于暂时没有分配班级的学生不查。
5.2.1 collection 查询
先设计对应 POJO 类。
Class.java,班级对象类。里面有个 List<Student> students
,用来存放学生对象。
package com.raingray.pojo;
import java.util.List;
public class Class {
private Integer id;
private String name;
List<Student> students;
public Class() {
}
public Class(Integer id, String name, List<Student> students) {
this.id = id;
this.name = name;
this.students = students;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
@Override
public String toString() {
return "Class{" +
"id=" + id +
", name='" + name + '\'' +
", students=" + students +
'}';
}
}
Student.java,学生对象类。
package com.raingray.pojo;
public class Student {
private Integer id;
private String name;
public Student() {
}
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
编写对应 Mapper 接口 com.raingray.mapper.ClassMapper。
package com.raingray.mapper;
import com.raingray.pojo.Class;
public interface ClassMapper {
Class getClassById(Integer id);
}
编写 Mapper 映射文件 com/raingray/mapper/ClassMapper.xml。这里 collection 标签用来指定 PJO 类 Class 中 students 这个集合属性映射关系,property 属性是 Class 类中具体对象成员变量名,为 class,ofType 属性是成员变量名全权限定名,填 com.raingray.com.pojo.Student,里面的子标签是描述这个引用类型成员变量,哪怕一样也要填,不然查不到东西。
<resultMap id="classMap" type="com.raingray.pojo.Class">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<!--一对多,第一种查询方式 coolection-->
<collection property="students" ofType="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
</resultMap>
<select id="getClassById" resultMap="classMap">
SELECT
c.c_id, c.c_name, s_id, s_name
FROM c_class c
LEFT JOIN t_student s ON c.c_id = s.c_id
WHERE
c.c_id = #{id}
</select>
编写测试程序 TestClassMapper.java
import com.raingray.mapper.ClassMapper;
import com.raingray.pojo.Class;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
public class TestClassMapper {
@Test
void testGetClassById() {
SqlSession sqlSession = null;
try {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
ClassMapper clazz = sqlSession.getMapper(ClassMapper.class);
Class clazzs = clazz.getClassById(1);
System.out.println(clazzs);
sqlSession.commit();
} catch (IOException e) {
if (sqlSession != null) {
sqlSession.rollback();
}
throw new RuntimeException(e);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
从运行日志来看,班级中对象中 students 确实有学生信息,成功实现需求。
==> Preparing: SELECT c.c_id, c.c_name, s_id, s_name FROM c_class c LEFT JOIN t_student s ON c.c_id = s.c_id WHERE c.c_id = ?
==> Parameters: 1(Integer)
<== Columns: c_id, c_name, s_id, s_name
<== Row: 1, 2013级计算机科学技术2班, 1, 张三
<== Row: 1, 2013级计算机科学技术2班, 2, 李四
<== Row: 1, 2013级计算机科学技术2班, 5, 孙吴
<== Total: 3
Class{id=1, name='2013级计算机科学技术2班', students=[Student{id=1, name='张三'}, Student{id=2, name='李四'}, Student{id=5, name='孙吴'}]}
5.2.2 分步查询
和 5.1.3 分步查询实现逻辑一样。
首先还是先编写步骤一,先查出来指定班级信息,再通过 collection 标签将 students 属性封装成集合,这个 Student 对象数据怎么来是要执行第二步 SQL 获得,要执行的 SQL 用 select 属性指定,要给第二步 SQL 传的结果集参数用 column 属性指定。
<resultMap id="classBySetupMap" type="com.raingray.pojo.Class">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<!--一对多,第二种查询方式分布查询,步骤一-->
<collection property="students"
select="com.raingray.mapper.StudentMapper.selectUserInfoByClassId"
column="c_id"
fetchType="lazy"
/>
</resultMap>
<select id="getClassByIdSetup" resultMap="classBySetupMap">
SELECT
c_id, c_name
FROM
c_class
WHERE
c_id = #{id}
</select>
第二步创建 Student 映射接口 com.raingray.pojo.Student。由于根据班级查询用户可能会出现多个,最好返回值用 List 集合来接收,在这个例子中用直接 Student 也行,有可能是自己处理了多个参数。
package com.raingray.mapper;
import com.raingray.pojo.Student;
import java.util.List;
public interface StudentMapper {
//Student selectUserInfoByClassId(Integer id);
List<Student> selectUserInfoByClassId(Integer id);
}
编写 Student 映射 Mapper。直接根据步骤一 collection 传过来的 c_id 查有哪些用户,将它们封装成 Student 对象。
<!--一对多,第二种方式分布查询,步骤二-->
<resultMap id="selectUserInfoByClassIdMap" type="com.raingray.pojo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</resultMap>
<select id="selectUserInfoByClassId" resultMap="selectUserInfoByClassIdMap">
SELECT s_id, s_name FROM t_student WHERE c_id = #{id}
</select>
执行测试代码。
ClassMapper clazz = sqlSession.getMapper(ClassMapper.class);
Class clazzs = clazz.getClassByIdSetup(1);
System.out.println(clazzs);
成功返回班级和对应学生。
==> Preparing: SELECT c_id, c_name FROM c_class WHERE c_id = ?
==> Parameters: 1(Integer)
<== Columns: c_id, c_name
<== Row: 1, 2013级计算机科学技术2班
<== Total: 1
==> Preparing: SELECT s_id, s_name FROM t_student WHERE c_id = ?
==> Parameters: 1(Integer)
<== Columns: s_id, s_name
<== Row: 1, 张三
<== Row: 2, 李四
<== Row: 5, 孙吴
<== Total: 3
Class{id=1, name='2013级计算机科学技术2班', students=[Student{id=1, name='张三'}, Student{id=2, name='李四'}, Student{id=5, name='孙吴'}]}
collection 同样也支持延迟加载,操作和原理都是一样的,就不重复演示了。
5.3 一对一🔨
把一张表当做两张表查询,内连接查询。
跟多对一操作一样,直接在里面建集合就行,比如查 A 的时候把 A 关联的数据 B 一起查出来作为属性放到 A 里。
5.4 多对多🔨
A 表里有 B,B 表里有 A,它两都是一对多的关系。
要用到在当做专项学习。
6 缓存🔨
Mybatis 中一级缓存是默认启用的,仅在查询时生效,比如第一次执行查询语句,会把结果放到缓存中,往后再查就会从缓存取,比较快。什么情况下缓存会清空?一个是事务提交时另一个是查询的数据有变化。
这种内容了解即可,毕竟不是真跑去做研发,不需要背八股文。
7 逆向工程🔨
使用 Maven 插件根据表生成 POJO 类,Mapper 接口和 Mapper 映射文件。
最近更新:
发布时间: