MyBatis
MyBatis 基础
MyBatis 出现的原因
JDBC 进行开发存在的问题
- 数据库配置信息存在硬编码问题(如果要更换数据库等操作,需要修改源代码,需要重新打包部署项目),数据库连接创建、释放频繁造成系统资源浪费(数据库连接、释放等代码被放在持久层,而持久层代码会被多次调用),从而影响系统性能。
- SQL 语句在代码中硬编码,造成代码不易维护,实际应用 SQL 变化的可能较大,SQL 变动需要改变 Java 代码。
- 查询操作时,需要手动将结果集中的数据手动封装到实体中。
应对问题的解决方法
- 使用数据库连接池初始化连接资源。
- 将 SQL 语句抽取到 xml 配置文件中。
- 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射。
1. MyBatis 简介
MyBatis 是一个优秀的基于 ORM 的半自动(需要手动编写 SQL)轻量级(程序在启动期间消耗资源较少)持久层框架,是对 JDBC 的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 jdbc 繁杂的过程代码。
半自动与全自动:区别在于是否需要手动编写 SQL。
半自动:MyBatis
全自动:Hibernate
2. ORM 思想
ORM(Object Relational Mapping)对象关系映射。
- O - 对象模型:实体对象,即在程序中根据数据库表结构建立的一个个实体 JavaBean。
- R - 关系型数据库的数据结构:关系数据库领域的 Relational(建立的数据库表)
- M - 映射:从 R(数据库)到 O(对象模型)的映射,可通过 XML 文件映射。
实现
- 让实体类和数据库表进行一一对应关系(一个实体类对应一张数据库表,一个对象对应数据库表中的一条记录)。先让实体类和数据库表对应,再让实体类属性和表里面字段对应。
- 建立实体和数据库表之间的映射关系。Mybatis 的作用,借助 XML 或 注解 完成映射关系的确立。
- 不需要直接操作数据库表,直接操作表对应的实体类关系。
ORM 是一种思想,帮助我们跟踪实体的变化,并将实体的变化翻译成 SQL 脚本,执行到数据库中去。也就是将实体的变化映射到数据库表的变化。Mybatis 采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC api 底层访问的细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。
3. Mybatis 基本步骤
-
创建数据库及 user 表。
-- create CREATE DATABASE `mybatis_db`; USE `mybatis_db`; CREATE TABLE `user` ( `id` INT(11) N`jdbc_user`OT NULL AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL COMMENT '用户名称', `birthday` DATETIME DEFAULT NULL COMMENT '生日', `sex` CHAR(1) DEFAULT NULL COMMENT '性别', `address` VARCHAR(256) DEFAULT NULL COMMENT '地址', PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8; -- insert INSERT INTO `user`(`id`,`username`,`birthday`,`sex`,`address`) VALUES (1,'牛牛','1996-12-21 00:00:00','男','山西忻州'), (2,'个个','2001-03-15 00:00:00','女','陕西西安');
-
创建 maven 工程,在 pom.xml 导入相关依赖的坐标(MyBatis 坐标、MySQL 驱动坐标、junit 测试单元坐标)。
<!-- 指定编码和版本 --> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.complier.encoding>UTF-8</maven.complier.encoding> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <!-- 导入相关依赖 --> <dependencies> <!-- 导入mybatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.8</version> </dependency> <!-- mysql驱动坐标 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> <scope>runtime</scope> </dependency> <!-- 单元测试坐标 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>test</scope> </dependency> </dependencies>
-
编写 User 实体类。
public class User { private int id; private String username; private Date birthday; private String sex; private String address; // getter、setter和toString }
-
编写 UserMapper.xml 映射配置文件(ORM 思想)。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="user"> <!-- namespace - 命名空间,与id属性共同构成唯一标识,则定位时就使用user.findAll --> <!-- resultType - 将查询到的结果与此字段对应的类进行关联,自动完成映射封装,要求是类的全路径。 --> <select id="findAll" resultType="com.chrisniu.domain.User"> <!-- 要求没有封号 ; --> select * from user </select> </mapper>
-
编写 sqlMapConfig.xml 核心配置文件。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- environments配置的是运行环境 --> <environments default="develop"> <environment id="develop"> <!-- 当前的事务管理器是JDBC --> <transactionManager type="JDBC"/> <!-- 数据源信息,即数据库配置信息 --> <!-- POOLED:使用mybatis的连接池;UNPOOLED:不使用连接池,每一次都是新的连接 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///mybatis_db"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!-- 引入自定义类和SQL查询到结果的映射配置文件,即将4编写的映射进行配置加载 --> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
进行映射配置时,类似
<mapper resource="mapper/UserMapper.xml"></mapper>
标签中不存值时,可以简写为<mapper resource="mapper/UserMapper.xml"/>
。 -
编写测试代码。
public class MybatisTest { @Test public void mybatisQuickStartTest() throws IOException { // 1.加载核心配置文件 InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2.获取sqlSessionFactory工厂对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3.获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); // 4.执行sql,参数就是statementId,即配置sql语句时的userspace.id List<User> users = sqlSession.selectList("user.findAll"); // 5.打印查找到的结果 for (User user : users) { System.out.println(user); } // 6.关闭连接 sqlSession.close(); } }
4. MyBatis 映射文件
使用 XxxMapper.xml 文件完成 MySQL 语句的映射,进而使用映射后的方式进行 CRUD。
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 映射配置文件的DTD约束头 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 根标签,根标签中的namespace与语句中的id共同构成数据库操作的唯一标识,即namespace.id -->
<mapper namespace="user">
<!-- 映射的SQL操作 -->
</mapper>
Mybatis 查询
<mapper namespace="..."></mapper>
是根标签,根标签中的 namespace 属性与操作标签中的 id 共同构成数据库操作的唯一标识。查询语句使用 <select></select>
标签,在映射文件中使用 resultType 属性指定从数据库中查询到的数据绑定的数据类型(完全限定名)。查询操作使用的 API 是sqlSession.selectList("命名空间.id");
。
<!-- 查询所有 -->
<!-- select标签表示查询操作。添加insert,修改update,删除delete -->
<!-- namespace - 命名空间,与内部操作的id属性共同构成唯一标识,定位时使用user.findAll -->
<!-- resultType - 将查询到的结果与resultType对应的类进行关联,自动完成映射封装,要求是类的全路径 -->
<select id="findAll" resultType="com.chrisniu.domain.User">
<!-- 要执行的SQL语句,要求没有封号 ; -->
select * from user
</select>
// java语句
List<User> users = sqlSession.selectList("UserMapper.findAll");
Mybatis 新增
插入语句使用 <insert></insert>
标签,在映射文件中使用 parameterType 属性指定要插入到数据库中的数据类型(完全限定名)。
SQL 语句中使用 #{实体属性名}
方式引用实体中的属性值(准确说是 get 方法后的小写单词),且要与前面数据库中的属性一一对应。
插入操作使用的 API 是 sqlSession.insert("命名空间.id", 实体对象);
,且插入操作涉及数据库数据变化,所以要使用 sqlSession 对象进行显式地提交事务,即 sqlSession.commit();
。
<!-- 新增用户 -->
<insert id="saveUser" parameterType="com.chrisniu.domain.User">
<!-- insert into user 是对user表进行操作,后面的属性对应user表中的属性 -->
<!-- #{} 是mybatis中的占位符,等同于JDBC中的 ? ,内部的属性名与User类的属性一一对应,
准确来说是与get方法后面的首字母小写的属性相同。如getUsername,则此属性名为username -->
<!-- parameterType:指定查询到的数据要封装成的类的类型 -->
insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address})
</insert>
// java语句
sqlSession.insert("UserMapper.saveUser", user);
Mybatis 修改
修改语句使用 <update></update>
标签,在映射文件中使用 parameterType 属性指定与数据库中对应的数据类型(完全限定名),借此来完成修改操作。修改操作使用的 API 是 sqlSession.update("命名空间.id", 实体对象);
。
<!-- 修改用户 -->
<update id="updateUser" parameterType="com.chrisniu.domain.User">
update user set username = #{username}, birthday = #{birthday}, sex = #{sex}, address = #{address} where id = #{id}
</update>
// java语句
sqlSession.update("UserMapper.updateUser", user);
Mybatis 删除
修改语句使用 <delete></delete>
标签,在映射文件中使用 parameterType 属性指定传入所需的参数来完成删除操作。删除操作使用的 API 是 sqlSession.delete("命名空间.id", 参数对象);
在进行删除操作时,如果 parameterType 传入的类型是基本数据类型或者字符串类型且个数只有一个(即不存在多个属性),则 SQL 语句中的 #{}
内的值可以任意写,且标签中的 parameterType 属性可以不写;如果传入的是对象类型,则 #{}
需要与此对象类型的 get 方法后的单词小写进行对应。
<!-- 删除用户 -->
<delete id="deleteUser" parameterType="java.lang.Integer">
<!-- 由于传入的对象就是一个整型,只有一个属性,因此 #{} 里面些什么都可以 -->
delete from user where id = #{bothOK}
</delete>
// java语句
sqlSession.delete("UserMapper.deleteUser", 4);
5. Mybatis 核心配置文件
MyBatis 的配置文件包含了影响 MyBatis 行为的设置和属性信息。
配置文件的顶层结构:在配置时,要按以下相对顺序。
-
configuration - 配置
-
properties - 属性
-
settings - 设置
-
typeAliases - 类型别名
-
typeHandlers - 类型处理器
-
objectFactory - 对象工厂
-
plugins - 插件
-
environments - 环境配置
- environment - 环境变量
- transactionManager - 事务管理器
- dataSource - 数据源
- environment - 环境变量
-
databaseIdProvider - 数据库厂商标识
-
mappers - 映射器
-
MyBatis 常用配置解析
1) properties 标签
在各标签中配置属性标签 <property></property>
时,可以将配置信息单独抽取成一个 properties 文件。如配置环境配置中的数据源时,将 jdbc 的配置信息单独提取出来。
jdbc.properties 文件,其中的数据存储采用 key=value 的形式。
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis_db?characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456
MyBatis 的核心配置文件,引入外部的 properties 文件后,占位符使用 ${}
,内部的字段值要与 properties 文件中的 key 值匹配。
<!-- 加载外部的properties文件 -->
<properties resource="jdbc.properties"></properties>
<!-- environments配置的是运行环境 -->
<environments default="develop">
<environment id="develop">
<!-- 当前的事务管理器是JDBC -->
<transactionManager type="JDBC"/>
<!-- 数据源信息,即数据库配置信息。 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
2) typeAliases 标签
对 Java 类的完全限定名设置一个短的别名,目的是简化映射文件中对 Java 类型的设置。
默认情况下,Java 官方为一些预定义类设置了别名,包装类的别名对应基本数据类型名,String 的别名为 string 等。
设置别名的方式有两种:
- 第一种是使用
<typeAlias></typeAlias>
标签中的 type 和 alias 属性来对单一的类设置别名; - 第二种是使用
<package></package>
标签中的 name 指定需要设置别名的类存放的目录,然后将此目录内的全部类都设置别名为类名,且不区分大小写。
<!-- 设置类的别名 -->
<typeAliases>
<!-- 方式一:给单个实体起别名 -->
<!-- <typeAlias type="com.chrisniu.domain.User" alias="user"></typeAlias> -->
<!-- 方式一:给单个实体起别名,别名就是类名,不区分大小写 -->
<package name="com/chrisniu/domain"/>
</typeAliases>
3) plugins 标签
MyBatis 可以使用第三方的插件来对功能进行扩展。其中分页助手 PageHelper 是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。
-
在 pom.xml 文件中导入通用 PageHelper 坐标。
<!-- 分页助手 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>3.7.5</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>0.9.1</version> </dependency>
-
在 MyBatis 核心配置文件中配置 PageHelper 插件。
<!-- 分页助手的插件 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <!-- dialect:指定方言,即表示数据库使用的是mysql,则使用分页时就是用mysql的limit --> <property name="dialect" value="mysql"/> </plugin>
-
测试与使用。
@Test public void testPageHelper(){ // 向数据库执行查询操作之前,设置分页参数。中间不进行任何代码 // 参数1表示当前页,参数2表示每页显示多少条数据 PageHelper.startPage(1,2); List<User> users = userMapper.selectAll(null); // 此时查询到的结果就是分页后第一页的两条结果 for(User user : users){ System.out.println(user); } } // 其他分页的其它数据,泛型为查询结果封装到哪个实体类,参数为集合users,表示利用分页查询出来的集合数据 PageInfo<User> pageInfo = new PageInfo<>(users); System.out.println("总条数:" + pageInfo.getTotal()); System.out.println("总页数:" + pageInfo.getPages()); System.out.println("当前页:" + pageInfo.getPageNum()); System.out.println("每页显示长度:" + pageInfo.getPageSize()); System.out.println("是否第一页:" + pageInfo.isIsFirstPage()); System.out.println("是否最后一页:" + pageInfo.isIsLastPage());
4) environment 标签
完成对数据库环境的配置,支持多个环境配置。此标签的 id 属性指定当前环境的名称。
-
<transactionManager></transactionManager>
标签配置事务管理器。使用此标签的 type 属性配置事务管理器,事务管理器的类型有两种:JDBC 和 MANAGED。
- JDBC:此配置直接使用 JDBC 的提交和回滚设置,依赖于从数据源得到的连接来管理事务作用域。
- MANAGED:此配置几乎没做什么,从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。Mybatis 与 Spring 整合后,事务交给 Spring 容器管理。
-
<dataSource></dataSource>
标签配置当前数据源的类型(数据库连接的方式)。使用此标签的 type 配置数据源的类型,数据源常用类型有三种:UNPOOLED、POOLED 和 JNDI。
- UNPOOLED:此数据源的实现为每次被请求时都需要打开和关闭连接。
- POOLED:此数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
- JNDI:此数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
5) mappers 标签
加载 MyBatis 的映射文件,映射方式有四种:
- 使用 resource 属性对相对于类路径的映射文件进行引用。如
<mapper resource="mapper/UserMapper.xml"/>
。 - 使用 url 加载映射文件。如
<mapper url="file:///var/mappers/UserMapper.xml"/>
。 - 使用映射器接口的完全限定类名。如
<mapper class="org.mybatis.builder.userMapper"/>
。 - 使用包内的映射器接口全部注册为映射器。如
<package name="org.mybatis.builder"/>
。
6. MyBatis API 相关接口
SqlSession 工厂构建器 SqlSessionFactoryBuilder
SqlSessionFactoryBuild().build(InputStream inputStream);
方法通过加载 MyBatis 核心配置文件的输入流的方式来构建一个 SqlSessionFactory 即 SqlSession 类的工厂类对象。
// MyBatis核心配置文件的路径
String resource = "sqlMapConfig.xml";
// 以输入流的形式加载配置信息
// Resources是一个工具类,在org.apache.ibatis.io包中,此类可以从类路径下、文件系统或一个web URL中加载资源文件。
InputStream inputStream = Resources.getResourceAsStream(resource);
// 新建SqlSession工厂构建器的实例
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 使用构造器实例构造SqlSession工厂
SqlSessionFactory factory = builder.build(inputStream);
SqlSession 工厂对象 SqlSessionFactory
SqlSessionFactory 有多个个方法创建 SqlSession 实例。常用的有如下两个:
openSession()
- 默认开启一个事务,但此事务不会自动提交,因此在进行增删改操作时,需要手动提交该事务来将更新操作持久化到数据库中。openSession(boolean autoCommit)
- 参数表示事务是否自动提交,如果为 true,则表示无需手动提交事务。
// 3.获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession 会话
SqlSession 实例可以实现所有执行语句、提交或回滚事务 和 获取映射器实例的方法。
// 查询操作,可以查询一条数据或多条数据
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter);
// 插入操作
int insert(String statement, Object parameter);
// 修改操作
int update(String statement, Object parameter);
// 删除操作
int delete(String statement, Object parameter);
// 事务的提交和回滚
void commit();
void rollback();
7. MyBatis 的基本原理
-
先进行 MyBatis 的核心配置文件(一般命名 SqlMapConfig.xml)和映射配置文件(一般命名 Mapper1.xml …)。核心配置文件中主要配置数据源、事务,并引入映射配置文件;映射配置文件中主要配置要执行的 SQL 语句,传入、传出参数等。
-
使用 SqlSessionFactoryBuilder 对象通过 build 方法构建 SqlSession 的工厂对象 SqlSessionFactory。此过程的操作:
- 进行初始化配置:使用 dom4j 解析了配置文件,将映射配置文件中的每个 SQL 配置都封装成一个 MappedStatement 对象。每个 MappedStatement 对象中封装了多个属性,分别是要执行的 SQL 语句、resultType 和 parameterType 等参数,此过程就是将多个属性封装成了一个对象;然后将配置文件生成的多个 MappedStatement 对象封装成一个 map 集合,此集合的 key 保存的是每个 SQL 配置的 statementId(namespace.id 对应的字符串值),value 保存的是对应的 MappedStatement 对象。
- 创建了 SqlSessionFactory 工厂类对象。
-
SqlSessionFactory 对象通过 openSession() 方法创建 SqlSession 会话对象。SqlSession 对象并不会直接操作数据库,而是委派给执行器 Executor 进行执行。
执行器的底层就是 JDBC,JDBC 需要得到要执行的 SQL 语句、参数以及返回数据封装得到结果对象,而这些数据都封装在 MappedStatement 对象中。当 SqlSession 执行操作时,会将执行的操作的标识和参数类传给执行的函数,执行器就会根据这些参数在存放 MappedStatement 对象的 map 集合中找到对应的 mappedStatement,然后去执行底层的 JDBC 操作。
-
执行 JDBC 操作后,最终操作数据库或封装返回数据。
8. MyBatis 的 DAO 层开发
8.1 传统开发方式
-
编写 UseMapper 接口。
public interface UserMapper { public List<User> findAll() throws Exception; }
-
编写 UserMapper 实现类。
public class UserMapperImpl inplements UserMapper { @Override public List<User> findAll() throws Exception { // 1.加载核心配置文件 InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2.获取sqlSessionFactory工厂对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3.获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); // 4.执行sql,参数就是statementId,即配置sql语句时的userspace.id List<User> users = sqlSession.selectList("UserMapper.findAll"); // 5.关闭连接 sqlSession.close(); return users; } }
-
编写 UserMapper.xml 文件。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="UserMapper"> <!-- 查询所有 --> <select id="findAll" resultType="com.chrisniu.domain.User"> select * from user </select> </mapper>
存在的问题
- 实现类中,存在 MyBatis 模板代码重复。即加载核心配置文件、获取 SqlSession 工厂对象和获取 SqlSession 对象的操作。
- 实现类调用方法时,xml 中的 SQL statement 硬编码到 Java 代码中。即 statementId 硬编码在 Java 中,对 xml 文件中的 statementId 进行修改时也需修改大量 Java 代码中的方法参数。
由于出现的问题都存在于实现类中,因此考虑是否可以只写接口,不写实现类?
8.2 代理开发方式
采用 Mybatis 的基于接口代理方式实现持久层的开发。基于接口代理方式的开发只需要编写 Mapper 接口,Mybatis 框架会动态生成实现类的对象。
开发流程
规范:Mapper 接口开发方法只需编写 Mapper 接口(相当于 Dao 接口),由 Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同 Dao 接口实现类方法。
- Mapper.xml 映射文件中的 namespace 要与 Mapper 接口的完全限定名相同。
- Mapper 接口中的成员方法名要与 Mapper.xml 映射文件中定义的每个 statement 的 id 相同。
- Mapper 接口方法的输入参数类型要与 Mapper.xml 映射文件中定义的每个 SQL 的 parameterType 的类型相同。
- Mapper 接口方法的返回参数类型要与 Mapper.xml 映射文件中定义的每个 SQL 的 resultType 的类型相同。
-
编写 UserMapper 接口。
public interface UserMapper { public List<User> findAll() throws Exception; }
-
编写 UserMapper.xml 配置。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="UserMapper"> <select id="findAll" resultType="com.chrisniu.domain.User"> select * from user </select> </mapper>
-
映射文件的配置。
- 使用映射器接口的完全限定类名。在使用此方式时,UserMapper.xml 映射配置文件在 resources 中的目录结构与 UserMapper.java 接口文件在 java 中的目录结构须保持一致。如
<mapper class="com.chrisniu.mapper.UserMapper"/>
,其中映射配置文件位于 src/main/resources/com/chrisniu/mapper/UserMapper.xml,接口文件位于 src/main/java/com/chrisniu/mapper/UserMapper.java。 - 使用包内的映射器接口全部注册为映射器。如
<package name="com.chrisniu.mapper"/>
。此方法仍须满足相同目录结构。
<!-- 引入映射配置文件 --> <mappers> <!-- 导入xml文件 --> <!--<mapper resource="com/chrisniu/mapper/UserMapper.xml"/>--> <!-- 将接口加载为映射器 --> <!--<mapper class="com.chrisniu.mapper.UserMapper"/>--> <!-- 将包内的接口全部注册为映射器 --> <package name="com.chrisniu.mapper"/> </mappers>
- 使用映射器接口的完全限定类名。在使用此方式时,UserMapper.xml 映射配置文件在 resources 中的目录结构与 UserMapper.java 接口文件在 java 中的目录结构须保持一致。如
-
进行测试。虽然测试时仍然需要导入核心配置、获取工厂实例并获取 SqlSession 实例,但此操作是在测试中操作,而传统开发方式则需要将这些操作冗余在 Mapper 的实现类中。
public void test1() throws IOException { // 导入配置、获得工厂、获得SqlSession对象 InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(); // 获得Mapper代理对象 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 执行查询 User user = userMapper.findUserById(1); System.out.println(user); // 释放资源 sqlSession.close(); }
执行原理
此时 DAO 层只有一个接口,但接口并不进行实际工作。
-
getMapper(UserMapper.class)
方法利用接口的类型作为参数,得到的 UserMapper 接口的实现类对象实际上是由 MapperProxy 代理产生的代理对象(底层是基于 JDK 动态代理产生的代理对象)。即 userMapper 对象底层实际上是 MapperProxy 产生的对象。**代理对象调用接口中的任意方法时,底层的
invoke()
都会执行。**即当前的 userMapper 对象调用接口内的findUserById()
方法时,底层执行的其实是mapperProxy.invoke()
方法。 -
MapperProxy 对象调用
invoke()
方法时,最终调用了mapperMathod.excute(sqlSession, args)
方法。 -
在 excute 方法中,执行工作的还是 sqlSession,也即调用的是 sqlSession 的 API 方法。
MyBatis 复杂映射
1. MyBatis 高级查询
resultMap 属性与标签
映射配置文件中,进行 SQL 编写时,resultType 指定的实体类的属性名要与数据库表中的字段名保持一致(即 get 和 set 方法后面的单词小写与数据库表字段一致),才可以将查询到的结果自动封装到实体类中。
如果实体类的属性名与数据库表中的字段名不一致,则可以使用 resultMap 属性搭配 resultMap 标签 实现将数据手动封装到实体类中。
如果数据库中的字段名与实体类中的属性名一致,则在 SQL 操作标签中使用参数 resultType 将查询结果封装到指定的实体类中;
如果数据库中的字段名与实体类中的属性名不一致,则使用 resultMap 标签实现实体类的成员变量名与数据库表的字段名之间的映射关系,然后再在 SQL 操作标签中使用 resultMap 属性来传入 resultMap 标签的 id,然后封装到对应类中。一个 resultMap 标签可以被使用多次。
<!-- resultMap标签的id属性是此标签的唯一标识,type属性是对应的封装后的实体类 -->
<resultMap id="userResultMap" type="user">
<!-- 手动配置映射关系 -->
<!-- id标签用来配置主键,property属性对应实体类中的成员变量名,column属性对应数据库表中的字段名 -->
<id property="idMy" column="id"/>
<!-- result标签配置表中的其它字段,property属性对应实体类中的成员变量名,column属性对应数据库表中的字段名 -->
<result property="usernameMy" column="username"/>
<result property="birthdayMy" column="birthday"/>
<result property="sexMy" column="sex"/>
<result property="addressMy" column="address"/>
</resultMap>
<select id="findAllResultMap" resultMap="userResultMap">
select * from user
</select>
多条件查询
多条件查询有三种方式:使用 #{arg0} ~ #{argn}
或#{param1} ~ #{paramn}
获取参数、通过 @Param()
注解获取参数、使用 Pojo 对象传递参数。
1) #{...}
的方式
使用 #{arg0} ~ #{argn}
或#{param1} ~ #{paramn}
获取多个参数,其中 arg 的下标从 0 开始,param 的下标从 1 开始。此方式无需配置 parameterType,将方法传入的参数按顺序放入占位符 #{...}
中然后在数据库端进行查询。
<select id="findByIdAndUsername" resultType="user">
<!--select * from user where id = #{arg0} and username = #{arg1}-->
select * from user where id = #{param1} and username = #{param2}
</select>
2) @Param()
注解
在接口的成员方法的参数前使用 @Param
注解的形式,相当于给参数设置一个别名,此时在映射配置文件中就可以使用注解中的别名对占位符 #{...}
中的参数进行填充。
public interface UserMapper {
public List<User> findByIdAndUsername(@Param("id") Integer id, @Param("username") String username);
}
<mapper namespace="com.lagou.mapper.UserMapper">
<select id="findByIdAndUsername" resultType="user">
select * from user where id = #{id} and username = #{username}
</select>
</mapper>
3) Pojo 对象(推荐)
Pojo 对象本质上就是一个对象,这种方法将需要传递的多个属性封装到对象中,然后将此对象作为映射文件的 parameterType 参数的值。其中占位符 #{...}
中的参数名就是传入对象的成员变量名(get 方法名的小写)。
<select id="findByIdAndUsername" parameterType="user" resultType="user">
select * from user where id = #{id} and username = #{username}
</select>
模糊查询
模糊查询有两种方式:#{...}
、${...}
。
在 SQL 语句中,模糊查询为 select * from user where username like '%王%'
,因此接口传入的就是带有模糊查询符号 %...%
的字符串。
// 映射接口中的抽象方法
public interface UserMapper {
public List<User> findByUsername(String username);
}
// 测试方法中传入模糊字段
@Test
public void testFindByUsername() throws Exception {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> list = userMapper.findByUsername1("%王%");
for (User user : list) {
System.out.println(user);
}
}
1) #{...}
使用占位符 #{username}
传入字符串参数,{} 内部可以为任意参数名,且不需要加单引号 ' '
。
<mapper namespace="com.lagou.mapper.UserMapper">
<select id="findByUsername" parameterType="string" resultType="user">
select * from user where username like #{username}
</select>
</mapper>
2) ${...}
使用拼接符 ${value}
传入字符串参数,{} 内部只能为 value,且需要加单引号 ' '
。
<mapper namespace="com.lagou.mapper.UserMapper">
<select id="findByUsername" parameterType="string" resultType="user">
select * from user where username like '${value}'
</select>
</mapper>
#{}
和 ${}
的区别
#{}
表示一个占位符。- 通过
#{}
可以实现 preparedStatement 向占位符中设置值(JDBC 预编译的占位符传值),自动进行 Java 类型和 JDBC 类型转换(Java 类和数据库类的转换),#{}
可以有效防止 SQL 注入。 - 占位符
#{}
方式设置参数时,转换为 SQL 语句在数据库执行时,会自动为占位符两侧添加单引号' '
,因此无需手动添加。 #{}
可以接收简单类型(基本数据类型和 String 类)或 Pojo 属性值。- 如果 parameterType 传输单个简单类型值(单个基本数据类型或 String 类),
#{}
括号中名称可以随意设置。
- 通过
${}
表示 SQL 的原样拼接。- 通过
${}
将 parameterType 传入的内容拼接在 SQL 语句中,且不进行 Java 类型与 JDBC 类型的转换,会出现 SQL 注入问题。 - 拼接符
${}
方式拼接 SQL 语句时,不会添加单引号' '
,因此需要在两侧添加单引号'${value}'
。 ${}
可以接收简单类型(基本数据类型和 String 类)或 Pojo 属性值。- 如果 parameterType 传输单个简单类型值(单个基本数据类型或 String 类),
#{}
括号中的字段只能为 value。
- 通过
2. MyBatis 映射文件深入
2.1 返回主键
返回主键的场景:在向数据库插入一条数据后,希望立即拿到此记录在数据库中的主键值。
如 MySQL 中主键设置的是自增长的 id 值,而传入数据时不需要传入此主键值,但是想要得到此主键值。
1) userGeneratedKeys 属性
useGeneratedKeys
属性声明是否将数据库中的主键返回,keyProperty
属性表示将返回的主键值封装到实体类中的哪个属性。
<!-- Java中定义 public void saveUser(User user); 接口来返回主键 -->
<!-- useGeneratedKeys="true"声明需要返回主键 -->
<!-- keyProperty="id"表示把返回主键的值封装到user实体的id属性中 -->
<insert id="saveUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}) </insert>
只适用于主键自增的数据库,MySQL 和 SqlServer 支持,Oracle 不支持。
2) selectKey 标签(推荐)
MySQL 内置的函数 SELECT LAST_INSERT_ID();
用于查询最近一条插入数据的主键值。
<selectKey></selectKey>
标签支持所有类型的数据库。order
属性声明当前 selectKey 标签内的 SQL 语句执行在此标签外部 SQL 语句的前或后,值可以为 BEFORE
或 AFTER
;keyProperty
属性声明数据库中的主键对应的列名;keyProperty
属性声明返回的主键要存储在实体类中的哪个属性;resultType
属性声明主键的类型。
<!-- Java中定义 public void saveUser(User user); 接口来返回主键 -->
<insert id="saveUser" parameterType="user">
<!-- selectKey适用范围广,支持所有类型数据库 -->
<!-- keyColumn="id"指定数据库中主键的列名 -->
<!-- keyProperty="id"指定主键封装到实体的id属性中 -->
<!-- resultType="int"指定主键类型 -->
<!-- order="AFTER"设置在sql语句执行前(后),执行此语句 -->
<selectKey keyColumn="id" keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID();
</selectKey>
insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}) </insert>
2.2 动态 SQL
需要根据不同的场景条件执行不同的 SQL 语句时,需要使用动态 SQL,来对 SQL 语句进行动态拼接。
1) <if>
适用于在 SQL 操作时参数的个数不确定的情况,如在进行商品筛选时,有多个可选条件,可以限定某个条件或某些条件的组合限定,此时需要使用 <where></where>
标签与 <if></if>
搭配完成动态 SQL 查询操作。
<!-- Java中定义 public List<User> findByIdAndUsernameIf(User user); 返回查询结果集 -->
<!-- -->
<select id="findByIdAndUsernameIf" parameterType="user" resultType="user">
SELECT * FROM user
<!-- where标签逻辑上相当于在查询语句后面添加`where 1=1`,但是如果内部没有<if>条件,则不会拼接where关键字 -->
<where>
<!-- if标签内部的test为判断条件 -->
<if test="id != null">
AND id = #{id}
</if>
<if test="username != null">
AND username = #{username}
</if>
</where>
</select>
2) <set> 动态更新
在 SQL 操作时,对符合条件的数据进行某个或某些字段的值修改,而修改的字段数与具体字段不定时,需要使用 <set></set>
标签与 <if></if>
搭配完成动态 SQL 修改操作。
<!-- Java中定义 public void updateIf(User user); 进行更新操作 -->
<update id="updateIf" parameterType="user">
UPDATE user
<!-- set标签在执行时,自动为SQL语句添加set关键字,然后去掉最后一个条件的逗号 -->
<set>
<if test="username != null">
username = #{username},
</if>
<if test="birthday != null">
birthday = #{birthday},
</if>
<if test="sex != null">
sex = #{sex},
</if>
<if test="address != null">
address = #{address},
</if>
</set> WHERE id = #{id}
</update>
3) <foreach> 多值查询
适用于在 SQL 操作时参数为有限个同种参数的情况,如在查询时,需要查询某属性的值在某个有限集合中的数据时,需要使用 <foreach></foreach>
标签。如需要查询 id 在 [1, 2, 3] 集合内的数据,相当于 select * from user where id in (1,2,3)
。
<foreach>
标签用于数据的循环遍历,属性有:
collection
:代表要遍历的集合的类型。open
:代表语句的开始部分。close
:代表语句的结束部分。item
:遍历集合的每个元素,所生成的别名,相当于foreach(int id: ids)
中,ids 是集合或数组,id 是元素别名。在标签内部的#{}
内填充的变量名需要与 item 的值相同。sperator
:代表分隔符。
相当于拼接的 SQL 语句为:
<where>
外部的 SQL 语句 +open
属性值 +#{id}
+separator
属性值 + … +close
属性值。
<!-- Java中定义 public List<User> findByList(List<Integer> ids); 返回查询结果集合 -->
<!-- 如果查询条件为普通类型的List集合,collection属性值为:collection或者list -->
<select id="findByList" parameterType="list" resultType="user" >
SELECT * FROM user
<where>
<foreach collection="collection" open="id in(" close=")" item="id" separator=",">
<!-- #{}内部的变量名需要与item的值id相同 -->
#{id}
</foreach>
</where>
</select>
<!-- Java中定义 public List<User> findByArray(Integer[] ids); 返回查询结果集合 -->
<!-- 如果查询条件为普通类型 Array数组,collection属性值为:array -->
<select id="findByArray" parameterType="int" resultType="user">
SELECT * FROM user
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
2.3 SQL 片段
在映射文件中,可能存在重复的 SQL 语句片段,可以使用 <sql></sql>
标签将这些重复的 SQL 片段抽取出来,属性 id
为唯一标识符,使用时利用 <include/>
标签引用,属性 refid
为需要引用的 SQL 片段的 id,可以实现 SQL 片段复用。
<!-- 抽取的sql片段,赋予唯一标识符id -->
<sql id="selectUser">
SELECT * FROM `user`
</sql>
<select id="findByList" parameterType="list" resultType="user" >
<!--引入sql片段-->
<include refid="selectUser"/>
<where>
<foreach collection="collection" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
<select id="findByArray" parameterType="integer[]" resultType="user">
<!--引入sql片段-->
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
3. MyBatis 多表查询
关系型数据库表之间的关系有:一对一,多对一,一对多,多对多。MyBatis 中,将多对一关系视为一对一,如多个订单属于同一用户,对于每个订单来说,只能属于一个用户。
本质上关系有一对一(人与身份证号之间互相为一对一关系)、多对一(用户与订单关系中,对于订单来说,多个订单属于一个用户,为多对一关系)、一对多(用户与订单关系中,对于用户来说,一个用户有多个订单,为一对多关系)、多对多(学生与选修课程来说,一个学生可以选多门课程,一门课程可以有多名学生选取,为多对多关系)。
多表构建
构建 orders 表表示订单,外键 uid
指向 user 表的主键,则这两张表存在多对一关系。构建 sys_role 表表示角色,与 user 之间存在多对多关系,因此存在中间表 sys_user_role,其中的 userid
指向 user 表的主键,roleid
指向 sys_role 的主键。
-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`ordertime` VARCHAR(255) DEFAULT NULL,
`total` DOUBLE DEFAULT NULL,
`uid` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of orders
-- ----------------------------
INSERT INTO `orders` VALUES ('1', '2020-12-12', '3000', '1');
INSERT INTO `orders` VALUES ('2', '2020-12-12', '4000', '1');
INSERT INTO `orders` VALUES ('3', '2020-12-12', '5000', '2');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`rolename` VARCHAR(255) DEFAULT NULL,
`roleDesc` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'CTO', 'CTO');
INSERT INTO `sys_role` VALUES ('2', 'CEO', 'CEO');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`userid` INT(11) NOT NULL,
`roleid` INT(11) NOT NULL,
PRIMARY KEY (`userid`,`roleid`),
KEY `roleid` (`roleid`),
CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `sys_role` (`id`),
CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `user` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '1');
INSERT INTO `sys_user_role` VALUES ('1', '2');
INSERT INTO `sys_user_role` VALUES ('2', '2');
3.1 一对一(多对一)
场景:用户表与订单表的关系中,从订单角度来看,每个订单只属于一个用户,因此可以看做是一对一关系。
-- 一对一查询语句
-- 查询所有订单,同时查询每个订单所属的用户
SELECT * FROM orders o LEFT JOIN USER u ON o.`uid`=u.`id`;
当前查询出来的表相当于由两部分组成:左边是订单信息,右边是订单所属的用户及用户自身的信息。此时如果构建 Order 类,封装 orders 表的三个字段,此时 User 类和 Order 类都无法完成查询到结果的封装。由于需要查询的是订单属于哪个用户,则可以为 Order 类封装一个 User 类的属性 user,来存储订单所属用户的信息。
// 封装表示订单的类Order
public class Order {
// 表示当前订单自身的属性
private int id;
private Date orderTime;
private double money;
private int uid;
// 表示当前订单属于哪个用户
private User user;
}
// OrderMapper接口,定义findAllWithUser方法实现多表查询
public interface OrderMapper {
public List<Order> findAllWithUser();
}
为多表查询的 findAllWithUser()
方法配置映射文件 OrderMapper.xml,其在 resources 中的路径需要与 OrderMapper 接口在 java 中的路径一致且同名。
<mapper namespace="com.chrisniu.mapper.OrderMapper">
<!-- 使用resultMap用来对应数据库中字段名与实体类中的字段名的一个映射 -->
<!-- 由于查询返回的结果是两个表,且将多表查询后其中一个表的结果存储在一个实体类中
因此也可以使用resultMap标签完成将返回的多个数据封装到一个类属性中 -->
<resultMap id="orderMap" type="com.chrisniu.domain.Order">
<id column="id" property="id"/>
<result column="ordertime" property="orderTime"/>
<result column="total" property="money"/>
<result column="uid" property="uid"/>
<!-- 一对一(多对一)使用association标签关联
property="user" 封装实体的属性名,与Order类中的属性对应
javaType="user" 封装实体的属性类型,是实体类的全限定名 -->
<association property="user" javaType="com.chrisniu.domain.User">
<!-- id标签同样存储此表的主键,其表的主键应该是id,但是与orders表中的主键同名
而其主键其实与orders中的uid对应,因此使用uid的值作为当前表的主键值
也可以通过对查询到的属性起别名的方式进行区分,在SQL语句中取别名
其它属性column对应数据库的字段名,property对应user类的类名 -->
<id column="uid" property="idMy"/>
<result column="username" property="usernameMy"/>
<result column="birthday" property="birthdayMy"/>
<result column="sex" property="sexMy"/>
<result column="address" property="addressMy"/>
</association>
</resultMap>
<select id="findAllWithUser" resultMap="orderMap">
select * from orders o left join user u on o.uid=u.id
</select>
</mapper>
3.2 一对多
场景:在用户表与订单表的关系中,从用户角度来看,一个用户可以有多个订单,则用户与订单之间是一对多的关系。
-- 一对多查询语句
-- 查询所有用户,同时查询出每个用户具有的订单
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.`id` = o.`uid`;
编写实体类 User,由于每个 User 可以有多个订单信息,每个订单信息对应一个 Order 类,因此需要在 User 类中添加一个 List<Order>
属性来存放多个订单信息。
public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
// 代表当前用户具备的订单列表,包含多个Order实例
// 其中Order包含id、ordertime和money属性
private List<Order> orderList;
}
// UserMapper接口,定义findAllWithOrder方法实现一对多查询
public interface UserMapper {
public List<User> findAllWithOrder();
}
为多表查询的 findAllWithOrder()
方法配置映射文件 UserMapper.xml。将属性封装到单个类中使用 <association>
标签,而将每一条数据的属性封装到一个类,且有多条数据时,使用 collection
标签。
<resultMap id="userOrderMap" type="com.chrisniu.domain.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
<!-- 一对多使用collection标签关联
property="orderList" 封装到集合的属性名,对应User类中的集合属性
ofType="order" 封装集合的泛型类型,对应集合的泛型对应的实体类 -->
<collection property="orderList" ofType="com.chrisniu.domain.Order">
<id column="oid" property="id"/>
<result column="ordertime" property="orderTime"/>
<result column="total" property="money"/>
</collection>
</resultMap>
<select id="findAllWithOrder" resultMap="userOrderMap">
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.id=o.uid
<!-- 此处oid的用处是为orders表中查到的数据,给其主键起的一个别名,相当于orders表中主键的那一列的字段名变成oid -->
</select>
3.3 多对多
场景:在用户表和角色表的关系中,一个用户可以有多个角色,一个角色可以有多个用户使用,则用户与角色之间是多对多的关系。
多对多的关系在数据库表中的体现为:通过 user_role 表进行连接。
查询的目的是得到 user 表中的用户数据和 role 表中的角色数据,而这两张表没有直接的关联,因此需要使用中间表 user_role,进行三表联查。
-- 一对多查询语句
-- 查询所有用户,同时查询出每个用户所有的角色
SELECT * FROM USER u -- 用户表
LEFT JOIN sys_user_role ur ON u.`id` = ur.`userid` -- 左外连接中间表
LEFT JOIN sys_role r ON ur.`roleid` = r.`id`; -- 左外连接中间表
-- 使用起别名的方式
-- 则当前方式下,映射文件中resultMap中的collection内部id标签的column属性的值就可以是rid
SELECT u.*,r.id rid,r.rolename,r.roleDesc FROM user u
LEFT JOIN sys_user_role ur ON ur.userid = u.id
LEFT JOIN sys_role r ON ur.roleid = r.id
在编写完 SQL 语句后,由于目的是查询所有用户以及用户所属角色,因此从角色方面来看,多对多与一对多相似。也即多对多查询与一对多查询的区别主要在 SQL 语句。
public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
// 代表当前用户所属的角色列表
private List<Role> roleList;
}
public class Role {
private int id;
private String roleName;
private String roleDesc;
}
// UserMapper接口,定义findAllWithRole方法实现一对多查询
public interface UserMapper {
public List<User> findAllWithRole();
}
为多表查询的 findAllWithRole()
方法配置映射文件 UserMapper.xml。
需要注意的是,column 对应的是 SQL 中多表查询到的视图中的字段名,因此可以使用起别名的方式区分同名字段,或者使用主键对应中间表中的外键字段名。
<resultMap id="userRoleMap" type="com.chrisniu.domain.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
<collection property="roleList" ofType="com.chrisniu.domain.Role">
<id column="roleid" property="id"/>
<result column="rolename" property="roleName"/>
<result column="roleDesc" property="roleDesc"/>
</collection>
</resultMap>
<select id="findAllWithRole" resultMap="userRoleMap">
SELECT * FROM USER u LEFT JOIN sys_user_role ur ON u.id = ur.userid LEFT JOIN sys_role r ON ur.roleid = r.id
</select>
总结
-
多对一(一对一)配置:使用
<resultMap> + <association>
做配置。 -
一对多配置:使用
<resultMap> + <collection>
做配置。 -
多对多配置:使用
<resultMap> + <collection>
做配置。 -
一对多和多对多配置基本相同,区别在于 SQL 语句的编写。
4. MyBatis 嵌套查询
嵌套查询:将多表查询中的联合查询语句,拆分成多个单表查询,再使用 MyBatis 语法进行嵌套。
-- 需求:查询一个订单,同时查询出该订单所属的用户
-- 1. 联合查询
SELECT * FROM orders o LEFT JOIN USER u ON o.`uid`=u.`id`;
-- 2. 嵌套查询
-- 2.1 先查询订单
SELECT * FROM orders
-- 2.2 再根据订单uid外键,查询用户
SELECT * FROM user WHERE id = #{根据订单查询到的uid}
-- 2.3 最后使用MyBatis,将以上二步嵌套起来 ...
4.1 一对一(多对一)
将多表查询的 SQL 拆分成多个单表查询的 SQL。
-- 先查询订单
SELECT * FROM orders;
-- 再根据订单uid外键,查询用户
SELECT * FROM user WHERE id = #{订单的uid};
分别在 OrderMapper 和 UserMapper 接口中编写方法。
public interface OrderMapper {
// 查询所有的订单信息
public List<Order> findAllWithUser();
}
public interface UserMapper {
// 根据订单信息中的uid外键,就可以根据int类型的id查询用户
public User findById(int id);
}
需要对两个接口对应的 xml 文件进行配置。执行过程为:
- 先执行 OrderMapper.xml 文件的
<select>
标签内的语句,然后利用<resultMap>
将查询到的数据封装到 Order 实体中。 - 在
<resultMap>
中对查询到的数据进行封装,然后使用<association>
标签对 Order 中的 User 属性进行封装。 javaType
属性表示要将此属性封装到哪个实体类,select
属性表示需要查询的第二句 SQL 语句的 statementId,即namespace.id
,column
的值是需要传入第二条 SQL 语句的参数。- 再执行 UserMapper.xml 文件中对应的语句,并将结果返回到 OrderMapper.xml 文件中的
<association>
处,即可完成封装。 - 最后完成查询封装。
<!-- 一对一嵌套查询 -->
<!-- OrderMapper.xml -->
<resultMap id="orderMap" type="com.chrisniu.domain.Order">
<id column="id" property="id"/>
<result column="ordertime" property="orderTime"/>
<result column="total" property="money"/>
<!-- 这是执行的第一条SQL语句,问题:1.如何执行第二条SQL?2.第二条SQL的uid如何 -->
<!-- select属性:就是需要调用的第二条SQL在配置文件中的namespace.id
column属性:当前第一个SQL查询出来的表中,作为参数传入第二条SQL的字段名 -->
<!-- 根据订单中的 uid 外键,查询用户表 -->
<association property="user" javaType="com.chrisniu.domain.User" column="uid" select="com.chrisniu.mapper.UserMapper.findById"/>
</resultMap>
<select id="findAllWithUser" resultMap="orderMap" >
SELECT * FROM orders
</select>
<!-- UserMapper.xml -->
<select id="findById" parameterType="int" resultType="com.chris.domain.User">
SELECT * FROM `user where id = #{uid}
</select>
4.2 一对多
SQL 语句
-- 先查询用户
SELECT * FROM user;
-- 再根据用户id主键,查询订单列表
SELECT * FROM orders where uid = #{用户id};
Mapper 接口
public interface UserMapper {
public List<User> findAllWithOrder();
}
public interface OrderMapper {
public List<Order> findByUserId(int uid);
}
映射文件
<!-- 一对多嵌套查询 -->
<!-- UserMapper.xml -->
<resultMap id="userMap" type="com.chris.domain.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
<!-- ofType属性:查询 -->
<!-- 根据用户的主键id,查询订单表 -->
<collection property="orderList" column="id" ofType="com.chris.domain.Order" select="com.chrisniu.mapper.OrderMapper.findByUserId"/>
</resultMap>
<select id="findAllWithOrder" resultMap="userMap">
SELECT * FROM user
</select>
<!-- OrderMapper.xml -->
<select id="findByUserId" parameterType="int" resultType="com.chris.domain.Order">
SELECT * FROM orders where uid=#{id}
</select>
4.3 多对多
SQL 语句
-- 先查询用户
SELECT * FROM user;
-- 再根据用户id主键,查询角色列表
SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{用户id};
Mapper 接口
public interface UserMapper {
public List<User> findAllWithRole();
}
public interface RoleMapper {
public List<Role> findByUserId(Integer uid);
}
映射文件
<!-- 多对多嵌套查询 -->
<!-- UserMapper.xml -->
<resultMap id="userAndRoleMap" type="user">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
<!-- 根据用户id,查询角色列表 -->
<collection property="roleList" column="id" ofType="com.chris.domain.Role" select="com.chris.mapper.RoleMapper.findByUserId"/>
</resultMap>
<select id="findAllWithRole" resultMap="userAndRoleMap">
SELECT * FROM user
</select>
<!-- RoleMapper.xml -->
<select id="findByUserId" parameterType="int" resultType="com.chris.domain.Role">
SELECT r.id,r.role_name roleName,r.roleDesc roleDesc FROM sys_role r INNER JOIN sys_user_role ur ON r.id = ur.roleid WHERE ur.userid = #{uid}
</select>
总结
-
一对一(多对一)配置:使用
<resultMap> + <association>
做配置,通过 column 条件,执行 select 查询。 -
一对多配置:使用
<resultMap> + <collection>
做配置,通过 column 条件,执行 select 查询。 -
多对多配置:使用
<resultMap> + <collection>
做配置,通过 column 条件,执行 select 查询。 -
优点:简化多表查询操作;缺点:执行多次 SQL 语句,浪费数据库性能。
MyBatis 加载策略和注解开发
MyBatis 加载策略
立即加载
在进行一对多或多对多查询时,如查询用户及其订单信息时,是在查询用户的同时,将用户相关的订单信息一同查询(一同加载)出来,这种加载策略是立即加载。
延迟加载
同样查询用户及其订单信息时,可以只查询用户信息,当需要用户相关的订单信息时,再根据用户关联的 id 查询订单信息,这种加载策略是延迟加载。也就是在需要用到这部分数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
延迟加载是基于嵌套查询来实现的。
在查询某信息时,将其关联信息也查询出来,是立即加载;只查询出需要的信息,其关联信息在使用时才查询,是延迟加载。
延迟加载表现为,当前并没有任何方法调用嵌套查询的数据时,不进行加载;当有任意方法需要用到这部分数据时,此时进行加载。
优点:先从单表查询,需要时再从关联表进行关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表的速度要快。
缺点:因为只有当需要用到关联数据时,才会进行数据库查询(相当于二次数据库查询),这样在大批量数据查询时,由于查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
在多表查询中,一对多,多对多:通常情况下采用延迟加载;一对一(多对一):通常情况下采用立即加载。
延迟加载的实现
1. 局部延迟加载
在 <association>
和 <collection>
标签中都有一个 fetchType
属性,通过修改此属性的值,可以修改局部加载策略,此属性的值为 lazy
时表示懒加载策略,值为 eager
时表示立即加载策略。
<resultMap id="orderMap" type="com.chrisniu.domain.Order">
<id column="id" property="id"/>
<result column="ordertime" property="orderTime"/>
<result column="total" property="money"/>
<!-- fetchType="lazy" 使用延迟加载(懒加载)策略,fetchType="eager" 使用立即加载策略 -->
<association property="user" javaType="com.chrisniu.domain.User" column="uid"
select="com.chrisniu.mapper.UserMapper.findById" fetchType="lazy"/>
</resultMap>
设置触发延迟加载的方法
家在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的 equals、clone、hashCode、toString 方法时也会触发关联对象的查询。
可以在核心配置文件中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。
<settings> <!-- 所有方法都会延触发迟加载 --> <!-- 要使用此字段对特定方法的延迟加载进行评比 --> <setting name="lazyLoadTriggerMethods" value="toString()"/> </settings>
2. 全局延迟加载
在 MyBatis 的核心配置文件中可以使用在 <settings>
标签内的 <setting>
标签修改全局加载策略。
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
局部的加载策略优先级高于全局的加载策略。即如果在全局配置中开启延迟加载(懒加载),则在映射文件中不配置 fetchType
属性的语句遵循全局配置,映射文件中配置 fetchType
的语句按映射文件为准。
通常在开启全局延迟加载时,对一对一查询的加载策略修改为立即加载。
MyBatis 缓存
MyBatis 缓存的必要性
当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来后,可以保存在缓存中。当用户再次查询这些数据时,无需再通过数据库查询,直接查询缓存,可以减少网络连接和数据库查询带来的资源损耗,从而提高查询效率、减少高并发访问带来的系统性能问题。
经常查询一些不经常发生变化的数据时,可以使用缓存来提高查询效率。Mybatis中缓存分为 一级缓存 和 二级缓存。
1. 一级缓存
一级缓存是 SqlSession 级别 的缓存,是 默认开启 的。
所以在参数和 SQL 语句完全一样的情况下,使用 同一个 SqlSession 对象 多次 调用同一个 Mapper 方法(执行同一个 SQL 语句且参数相同),只执行一次 SQL 操作,因为使用 SqlSession 进行第一次查询后,MyBatis 会将查询结果放在缓存中,当再查询时,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。
当使用 SqlSession 进行增加、更新、删除操作,或是 SqlSession 调用 clearCache() | commit() | close()
方法时,都会清空此 SqlSession 的一级缓存。
结合一级缓存的查询过程:
-
第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息。如果缓存中没有此数据,再从数据库查询用户信息。
-
得到用户信息,将用户信息存储到一级缓存中。缓存区本质上是一个 map 集合,其 key 就是此 SQL 查询语句 + 参数,对应的 value 是对查询到的结果进行封装的 实体对象 中。
-
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否存在 id 为 1 的用户信息,缓存中存在,则直接从缓存中获取此用户信息。
-
如果 SqlSession 去执行commit 操作(执行插入、更新、删除),会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
在映射配置时,如果使用 flushCache
属性的值为 true 时,表示自动清空缓存。
2. 二级缓存
二级缓存是 namspace 级别(可以跨 SqlSession,也即 Mapper 映射级别)的缓存,是默认不开启的。
开启二级缓存时,多个 SqlSession 可以在同一 namespace 下的 mapper 标签内的 SQL 语句查询到的结果进行共享。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWxEcBIf-1653319955618)(…/…/markdown_img/1653121152.png)]
二级缓存的开启需要进行配置,实现二级缓存时,MyBatis 要求返回的 pojo 必须是可序列化的,也就是 要求实体类实现 Serializable 接口。只需要在映射XML文件配置 <cache/>
就可以开启二级缓存。
<!-- 核心配置文件 -->
<settings>
<!--因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
1. 值为 true 代表开启二级缓存;为 false 代表不开启二级缓存。 -->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 映射文件 -->
<mapper namespace="com.chrisniu.dao.UserMapper">
<!-- 2. 表示当前namespace的映射开启二级缓存 -->
<cache></cache>
<!-- 3. <select>标签中设置useCache="true" 代表当前这个statement要使用二级缓存。
如果不使用二级缓存可以设置为false
注意:针对每次查询都需要最新数据的 SQL,要设置成useCache="false",禁用二级缓存。 -->
<select id="findById" parameterType="int" resultType="user" useCache="true">
SELECT * FROM `user` where id = #{id}
</select>
</mapper>
关于一级缓存和二级缓存:只有当 SqlSession 执行 sqlSession.commit()
或 sqlSession.close()
方法时,一级缓存中的内容才会刷新到二级缓存。
二级缓存中所有的 sqlSession 执行的 select
操作得到的数据都会被缓存,而任一 sqlSession 执行了 insert/update/delete
操作后,会情况此 namespace 所属的二级缓存。
MyBatis 的二级缓存因为是 namespace 级别,所以在进行多表查询时会产生脏读问题(在多表查询时,比如查询用户信息并查询用户的订单信息时,需要对 user 表和 order 表进行多表查询,且查询结果缓存在 userMapper 所属的二级缓存中;而当对订单表中的数据进行更新时,使用的是 orderMap 进行更新,同时更新的是 orderMap 所属的二级缓存中的数据,而 userMapper 所属的二级缓存中的数据未更新,因此再进行用户及订单信息查询时,读到的是脏数据)。因此 MyBatis 的二级缓存在应用中不会使用,而是使用 redis 作为第三方缓存。
MyBatis 开启二级缓存后,查询的顺序是 二级缓存 -> 一级缓存 -> 数据库。当从数据库中查询到数据时,先存到一级缓存,当一级缓存的 sqlSession 执行
sqlSession.commit()
或sqlSession.close()
方法时,才存到二级缓存。
MyBatis 注解开发
MyBatis 常用的 CRUD 注解:
// 增删改查操作
@Insert // 实现新增,代替了<insert></insert>
@Delete // 实现删除,代替了<delete></delete>
@Update // 实现更新,代替了<update></update>
@Select // 实现查询,代替了<select></select>
// 多表映射
@Result // 实现结果集封装,代替了<result></result>
@Results // 可以与@Result一起使用,封装多个结果集,代替了<resultMap></resultMap>
@One // 实现一对一结果集封装,代替了<association></association>
@Many // 实现一对多结果集封装,代替了<collection></collection>
1. MyBatis 注解实现 CRUD
-
编写 UserMapper 接口。
public interface UserMapper { // 查询操作 // 注解的参数值只有一个时,value = 可以省略 @Select(value = "SELECT * FROM user") public List<User> findAll(); // 查询操作 // 传入pojo对象作为参数时,SQL语句的参数同样使用占位符#{},内部的字段名是对象的get方法名的单词首字母小写 @Insert("INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})") public void saveUser(User user); // 查询操作 @Update("UPDATE user SET username = #{username},birthday = #{birthday},sex = #{sex},address = #{address} WHERE id = #{id}") public void update(User user); // 查询操作 @Delete("DELETE FROM user WHERE id = #{id}") public void delete(int id); }
-
编写核心配置文件。
<!-- 使用了注解替代的映射文件,所以只需要加载使用了注解的Mapper接口即可 --> <mappers> <!-- 扫描使用注解的Mapper接口文件 --> <mapper class="com.chrisniu.mapper.UserMapper"/> <!-- 也可以将一个包内的全部接口都扫描 --> <package name="com.chrisniu.mapper"/> </mappers>
-
测试代码。可以将重复代码提取出来,被
@Before
注解标注的方法先于@Test
注解标注的方法;在@Test
注解标注的方法执行完之后,再执行@After
注解标注的方法。class MybatisTest { private SqlSession sqlSession; @Before public void before() throws IOException { // 1.加载核心配置文件 InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2.获取sqlSessionFactory工厂对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); // 3.获取SqlSession对象 sqlSession = sqlSessionFactory.openSession(); } @After public void after() { // 5.提交事务 sqlSession.commit(); // 6.关闭连接 sqlSession.close(); } // 查询 @Test public void testFindAll(){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> list = userMapper.findAll(); for (User user : list) { System.out.println(user); } } // 添加 @Test public void testSave(){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setUsername("于谦"); user.setBirthday(new Date()); user.setSex("男"); user.setAddress("北京德云社"); userMapper.saveUser(user); } // 更新 @Test public void testUpdate(){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setId(49); user.setUsername("郭德纲"); user.setBirthday(new Date()); user.setSex("男"); user.setAddress("北京德云社"); userMapper.update(user); } // 删除 @Test public void testDelete(){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); userMapper.delete(49); } }
2. 使用注解实现复杂映射
使用 @Results/@Result/@One/@Many
注解组合完成复杂映射的配置。
注解 | 说明 |
---|---|
@Results | 代替标签 <resultMap> ,此注解中可以使用单个 @Result 注解,也可以使用 @Result 注解集合。@Results(@Resut)()}) 或 @Results({@Resut)(), @Resut)(), @Resut)(), ...}) |
@Result | 代替标签 <id>/<result> 。@Result 注解的属性:column - 数据库的列名;property - 需要装配的实体类的属性名;one - 需要使用的 @One 注解,@Result(one = @One) 表示一对一关系;many - 需要使用的 @Many 注解,@Result(many = @Many) 表示一对多关系。 |
@One | 代替标签 <assocation> ,是多表查询的关键,在注解中用来指定子查询返回单一对象。@One 注解的属性:select - 指定用来进行多表查询的 SQL 语句,使用时 @Result(column = " ", property = " ", one = @One(select = " ")) 。 |
@Many | 代替标签 <collection> ,是多表查询的关键,在注解中用来指定子查询返回对象集合。同样属性为 select ,使用时 @Result(column = " ", property = " ", many = @Many(select = " ")) 。 |
2.1 一对一查询
查找订单信息及每个订单所属的用户。
SQL 语句
SELECT * FROM orders;
SELECT * FROM user WHERE id = #{订单的uid};
接口及注解
// 订单的接口
public interface OrderMapper {
// 先查找订单信息
@Select("SELECT * FROM orders")
// 结果封装,相当于<resultMap>标签
// 如果只有一个属性与字段名不一致,需要添加映射,则只添加一个 @Result即可
@Results({
// column - 数据库中的字段名;property - 实体类的属性
// id = true - 表示当前字段是数据库中的主键
@Result(id = true, column = "id", property = "id"),
@Result(column = "ordertime", property = "orderTime"),
@Result(column = "total", property = "money"),
// javaType - 表示当前封装到的类型
@Result(property = "user", javaType = User.class,
// column - 是传递给后面 one/many 中操作的主键,也即当前表的外键,传递给后续 SQL 操作的参数
// @One 注解的参数 select 的值是另一个 SQL 的全路径,就是接口中的某方法
// 此处其实是拿到findById方法返回的数据,如果User类属性与表中字段不匹配,则需要在findById的属性添加results注解完成映射
column = "uid", one = @One(select = "com.chrisniu.mapper.UserMapper.findById",
fetchType = FetchType.EAGER))
})
public List<Order> findAllWithUser();
}
// 用户的接口
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
public User findById(int id);
}
2.2 一对多查询
查找用户信息及每个用户拥有的订单。
SQL 语句
SELECT * FROM user;
SELECT * FROM orders WHERE uid = #{用户id};
接口及注解
public interface UserMapper {
// 先查找用户信息
@Select("SELECT * FROM user")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "username", property = "username"),
@Result(column = "brithday", property = "brithday"),
@Result(column = "sex", property = "sex"),
@Result(column = "address", property = "address"),
// javaType是一个List的类别
@Result(property = "orderList", javaType = List.class, column = "id" ,
many = @Many(select = "com.chrisniu.mapper.OrderMapper.findByUserId"))
})
public List<User> findAllWithOrder();
}
public interface OrderMapper {
@Select("SELECT * FROM orders WHERE uid = #{uid}")
public List<Order> findByUserId(int uid);
}
2.3 多对多查询
查找订单信息及每个订单所属的用户。
SQL 语句
SELECT * FROM user;
SELECT * FROM sys_role r INNER JOIN sys_user_role ur
ON r.id = ur.userid WHERE ur.userid = #{用户id};
接口及注解
public interface UserMapper {
@Select("SELECT * FROM user")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "brithday", property = "brithday"),
@Result(column = "sex", property = "sex"),
@Result(column = "address", property = "address"),
@Result(property = "roleList", javaType = List.class, column = "id" ,
many = @Many(select = "com.lagou.mapper.RoleMapper.findByUserId"))
})
public List<User> findAllWithRole();
}
public interface RoleMapper {
@Select("SELECT * FROM role r INNER JOIN user_role ur ON r.id = ur.rid WHERE ur.uid = #{uid}")
public List<Role> findByUid(Integer uid);
}
基于注解的二级缓存
-
首先,核心配置文件开启二级缓存的支持。
<settings> <!-- ,所以这一步可以省略不配置。 值为true代表开启二级缓存;为false代表不开启二级缓存。 --> <setting name="cacheEnabled" value="true"/> </settings>
-
然后,在 Mapper 接口中使用注解配置二级缓存。
@CacheNamespace public interface UserMapper {...}
基于注解的延迟加载
不论在 @One
还是 @Many
注解中,都有 fetchType
属性。
fetchType = FetchType.LAZY // 表示懒加载
fetchType = FetchType.EAGER // 表示立即加载
fetchType = FetchType.DEFAULT // 表示使用全局的延迟加载配置
XML 配置 与 注解开发 的优劣分析
-
注解开发和 xml 配置相比,从开发效率来说,注解编写更简单,效率更高。
-
从可维护性来说,注解如果要修改,必须修改源码,会导致维护成本增加;而 xml 维护性更强。