MyBatis 是支持定制化 SQL以及高级映射(ORM)的优秀的持久层框架。
一、Mybatis项目创建
1、创建myBatis工程

选择maven 输入组织名称、项目名称、版本号和工程名
核对信息并点击Finish按钮

项目结构图:

2、引入jar包依赖、创建配置文件
- 引入依赖在pom.xml文件中
xml
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
- 创建mybatis.xml配置文件
resources右击-new-File

- 注意 myBatis配置顺序如下,不能颠倒,也就是如果有properties属性配置,一定放在最前面,settings放到第二个位置,以此类推

- mybatis.xml配置文件(部分)
xml
<!--头部-->
<?xml version="1.0" encoding="UTF-8"?> <!--xml文件-->
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--文件约束dtd,该文件的约束规范-->
<configuration>
<settings>
<!--setting配置; logImpl:指定MyBatis所用日志的具体实现,STDOUT-LOGGING表示控制台输出日志信息-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 配置环境(可以配置多个环境)-->
<environments default="dev">
<!-- 开发环境-->
<environment id="dev">
<!--事务管理器-->
<!-- 使用JDBC事务管理-->
<transactionManager type="JDBC"></transactionManager>
<!--数据源-->
<!-- 使用连接池技术-->
<dataSource type="POOLED">
<!-- 数据库驱动-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<!-- 连接字符串-->
<property name="url" value="jdbc://mysql://localhost:3306/test?userUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"/>
<!--数据库用户名-->
<property name="username" value="root"/>
<!--数据库密码-->
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--映射文件-->
<!--使用相对于类路径的资源引用-->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
- 写跟数据库表对应的java实体类

java
/**
* @Alias( string name):注解:起别名
*/
/*@Alias("us")*/
public class User {
private Long id;
private String username;
private String password;
public User() {
}
public User(Long id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
- 映射文件UserMapper.xml
配置dao连接数据库的sql语句(增删改查
)
映射文件位置为:src/main/resources/mapper/UserMapper.xml
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--mapper为根标签,namespace表示命名空间,暂时写为test-->
<!--UserMapper接口全限定接口名称(包名+接口名称)-->
<mapper namespace="com.tjetc.dao.UserMapper">
<!--select是查询标签,id表示唯一标识查询,resultType是查询结果返回的对象-->
<!--select标签内写sql语句(查询sql语句)-->
<!-- id的值与UserMapper的方法名称保持一致-->
<select id="selectList" resultType="User">
select `id`,`username`,`password` from `user`
</select>
<!--传入参数 #{}:占位。大括号id占位参数,JDBC会把#{id}代入sql-->
<!--resultType和parameterType都可以使用别名,别名的大小写不区分-->
<!--mybatis把java常用的类型都使用了别名,所以java的常用类型不用特写配置别名-->,
<select id="selectById" parameterType="_long" resultType="user">
select `id`,`username`,`password` from `user` where id=#{id}
</select>
<select id="selectPage" parameterType="int" resultType="user">
SELECT `id`,`username`,`password` FROM `user` ORDER BY id ASC limit #{index},#{pageSize}
</select>
<!--concat是mysql的函数,作用是拼接字符串,可以把占位符和%连在一起-->
<select id="selectLike" parameterType="String" resultType="user">
SELECT `id`,`username`,`password` from `user` WHERE username LIKE concat('%',#{username},'%')
</select>
<!-- 增加 -->
<!-- parameterType值是对象,#{}占位参数是对象名称,防止sql注入;${}原位替换,-->
<!-- userGeneratedKeys = true使用自增主键,keyProperty="id" 把自增主键赋值给User对象的id值;目的:不会发生并发问题-->
<insert id="insert" parameterType="user" useGeneratedKeys="true" keyProperty="id">
INSERT INTO USER (username,password) VALUES (#{username},#{password})
</insert>
<!-- 更新-->
<update id="update" parameterType="user">
UPDATE USER SET username=#{username},`password`=#{password} WHERE id=#{id}
</update>
<!-- 删除-->
<delete id="deleteById" parameterType="Long">
DELETE FROM USER WHERE id=#{id}
</delete>
</mapper>
- 测试程序
java
public class TestUser {
//查询全部用户
@Test
public void testQueryList() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//调用sqlSession.selectList()查询集合,参数为UserMapper.xml的namespace的值 点select标签对应 id值
List<User> users = sqlSession.selectList("test.queryList");
//打印结果
System.out.println(users);
//关闭session
sqlSession.close();
}
//根据id查询用户
@Test
public void testQueryById() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//调用sqlSession.selectOne()查询一条数据,第一个 参数为UserMapper.xml的namespace的值 点select标签对应 id值 第二个参数为参数值
User user = sqlSession.selectOne("test.queryById",1L);
//打印结果
System.out.println(user);
//关闭session
sqlSession.close();
}
}
- 使用别名
在mybatis.xml文件中配置别名
xml
<typeAliases>
<!--使用别名-->
<typeAlias type="com.tjetc.entity.User" alias="User"></typeAlias>
</typeAliases>
UserMapper.xml resultType="user",这里的user大小没有影响
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--namespace 暂时命名为test-->
<mapper namespace="test">
<select id="queryList" resultType="user">
select * from user
</select>
<select id="queryById" parameterType="int" resultType="User">
select * from user where id=#{id}
</select>
</mapper>
3、dao层创建Mapper接口映射,进行对数据库的操作
一、mapper接口映射
1、写接口
注意
1.接口名与 UserMapper.xml 除去扩展名一样(推荐)
2.UserMapper.xml 的 namespace 必须是 UserMapper 接口的全路径名

写mapper映射接口的方法遵循的原则
1、接口名与UserMapper.xml除去扩展名一样(推荐)
2、接口的方法名与UserMapper.xml的id一样
3、接口的方法的参数类型与UserMapper.xml的parameterType一样
4、接口的方法的返回值类型与 UserMapper.xml 的 resultType 一样(返回 list 接口方法的返回值类型是 List 类型)
java
public interface UserMapper {
/**
* 查询所有
* @return
*/
List<User> selectList();
/**
* 根据id查询单个
* @param id
* @return
*/
User selectById(long id);
/**
* 分页
* @param i 查询第几条数
* @param p 查询有多少条数
* @return
*/
List<User> selectPage(@Param("index") int i, @Param("pageSize") int p);
/**
* 模糊匹配
* @param username
* @return
*/
List<User> selectLike(String username);
/**
* 新增
* @param user
*/
void insert(User user);
/**
* 更新数据
* @param user
* @return 返回影响的行数
*/
int update(User user);
/**
* 根据id删除数据
* @return 返回影响的行数
*/
int deleteById(Long id);
}
修改UserMapper.xml配置
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--namespace 为Dao接口的全限定名:包名+接口名-->
<mapper namespace="com.tjetc.dao.UserMapper">
<select id="queryList" resultType="com.tjetc.entity.User">
select * from user
</select>
<select id="queryById" parameterType="int"
resultType="com.tjetc.entity.User">
select * from user where id=#{id}
</select>
</mapper>
测试类
java
@Test
public void testQueryList1() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取UserDao对象(代理对象)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//userDao.queryList()方法
List<User> users = userMapper.queryList();
//打印结果
System.out.println(users);
//关闭session
sqlSession.close();
}
@Test
public void testQueryById1() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取UserDao对象(代理对象)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//userDao.queryById()方法
User user = userMapper.queryById(1L);
//打印结果
System.out.println(user);
//关闭session
sqlSession.close();
}
4、mybatis.xml配置优化
- properties配置文件(连接数据库的信息)
在resources目录下创建db.properties
xml
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
- 修改mybatis.xml配置
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>
<properties resource="db.properties"></properties>
<settings>
<!--指定 MyBatis 所用日志的具体实现,STDOUT_LOGGING表示控制台输出日志信息-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<!--使用别名-->
<typeAlias type="com.tjetc.entity.User" alias="User"></typeAlias>
</typeAliases>
<!--配置环境(可以配置多个环境)-->
<environments default="dev">
<!--开发环境-->
<environment id="dev">
<!--使用JDBC事务管理-->
<transactionManager type="JDBC"></transactionManager>
<!--使用连接池技术-->
<dataSource type="POOLED">
<!--数据库驱动-->
<property name="driver" value="${driverName}"/>
<!--连接字符串-->
<property name="url" value="${url}"/>
<!--数据库用户名-->
<property name="username" value="${username}"/>
<!--数据库密码-->
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--映射文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
- typeAliases配置
类型别名是java类型的简写。它仅在XML配置文件中,用于简化合格的class名字
也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean
xml
<typeAliases>
<package name="com.tjetc.entity"/>
</typeAliases>
每一个在包 com.tjetc.entity 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 com.tjetc.entit.User 的别名为 user ;若有注解,则别名为其注解值 ,例如
java
@Alias("adm")
public class Admin{
//....
}
下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格

5、配置参数解析:
1、typeHandlers
无论是mybatis设置一个参数给PreparedStatement,还是从PreparedStatement中返回ResultSet,都要面临一个问题:Java类型与JDBC类型之间的转换。
Java类型面向的是内存,JDBC类型面向的是各种关系型数据库 ,因此两者有很大的不同。
数据类型映射,是ORM框架中最关键的问题。
xml
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
自定义类型映射器
你可以重写类型映射器,也可以创建自己的非标准类型映射器。
需要实现org.apache.ibatis.type.TypeHandler接口,或继承类
org.apache.ibatis.type.BaseTypeHandler
java
import com.tjetc.enums.SexEnums;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SexEnumsTypeHandler extends BaseTypeHandler<SexEnums>{
//jdbc类型转换java类型调用
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, SexEnums sexEnums, JdbcType jdbcType) throws SQLException {
preparedStatement.setObject(i,sexEnums);
}
//jdbc类型转换java类型调用
@Override
public SexEnums getNullableResult(ResultSet resultSet, String s) throws SQLException {
return (SexEnums) resultSet.getObject(s);
}
//jdbc类型转换java类型调用
@Override
public SexEnums getNullableResult(ResultSet resultSet, int i) throws SQLException {
return (SexEnums) resultSet.getObject(i);
}
//调用存储过程用
@Override
public SexEnums getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return null;
}
}
objectFactory配置
当mybatis每次创建一个新的结果集对象时,都需要使用objectFactory实例。
如果希望重写默认ObjectFactory的行为,可以自定义对象工厂
- environments配置
1、mybatis允许你同时配置多个environments。如开发、测试、生产,可以同时配置三个环境。还可以同时对多个不同类型的数据库操作。
注意:虽然你可以同时配置多个环境,但是对于一个SqlSessionFactory实例,只能选择一个环境使用。
因此,如果你想同时连接两个数据库,必须要创建两个SqlSessionFactory。
2、transactionManager配置
这里有两种事务管理类型可选,type=JDBC \| MANAGED
JDBC:依赖于从dataSource返回的Connection对象,直接使用JDBC管理事务
MANAGED:由JavaEE容器管理整个事务
注意:如果使用Spring整合mybatis,无需配置TransactionManager,因为Spring会用自己的事务环境
重写mybatis的配置
3、dataSource配置
使用标准数据源接口javax.sql.DataSource配置数据库的JDBC连接
这里有三种数据源类型可选:type="UNPOOLED \| POOLED \| JNDI")
- UNPOOLED数据源
这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的
简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连
接池并不重要,这个配置就很适合这种情形 - POOLED
采用数据库连接池配置,可以优化性能,是当前web项目配置首选。 - JNDI
这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用
4、databaseIdProvider配置
DatabaseIdProvider元素主要是为了支持不同厂商的数据库,即同时支持多个数据库

这个配置非常有用,项目如何同时支持多种数据库?
传统做法是生成多套mapper文件,在mybatis.xml中配置使用那套映射文件。
这个做法的很大缺陷是:多套映射文件中,会有很多接口的实现是相同的,如果代码修改,需要同时修改多套文件。这给开发额
外增加了很大的工作量。
二、mybatis的工作原理
底层原理这里不细说,之后单独介绍;大致原理如下图所示:

三、mybatis中的动态SQL
1、if判断条件是否成立
xml
<!-- 判断传入的参数是否符合条件,如果符合条件,把sql语句进行拼接-->
<select id="selectAll" parameterType="user" resultType="user">
SELECT * FROM USER WHERE 1=1
<if test="username!=null">
and username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</select>
2、choose
相当于 if elseif else 只有一个 when 或者 otherwise
被执行
xml
<!-- choose when otherwise 相当于java if else if else-->
<select id="selectAll1" parameterType="user" resultType="user">
SELECT * FROM USER where 1=1
<choose>
<when test="username!=null">
AND username=#{username}
</when>
<when test="password!=null">
AND password=#{password}
</when>
<otherwise>
AND 2=2
</otherwise>
</choose>
</select>
3、where
增加 where 条件,如果没有条件成立,where 不会出现在
sql 语句中,反之出现 sql 语句中,并且忽略 where 后面的第一个 and 或者 or
xml
<!-- 如果where标签内有条件成立,就会给sql语句添加where关键字,并且where后面的sql语句第一个and或or删除掉-->
<select id="selectAll2" parameterType="user" resultType="user">
SELECT * FROM USER
<where>
<if test="username!=null">
AND username=#{username}
</if>
<if test="password!=null">
AND password=#{password}
</if>
</where>
</select>
4、trim
例如 < trim prefix="where" prefixOverrides="and | or" > 忽略 where 后
面的第一个 and 或者 or
xml
<select id="findAll" parameterType="User" resultType="User">
SELECT * FROM user
<trim prefix="where" prefixOverrides="and | or">
<if test="username!=null">
and username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</trim>
</select>
5、set 后面必须满足于一个条件,不然不满足 sql 语法
xml
<update id="update" parameterType="user">
update user
<set>
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{password},
</if>
</set>
where id=#{id}
</update>
6、foreach
- 单参list
xml
<select id="findAll2" resultType="User">
SELECT * FROM user where id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>
UserMapper接口的方法
java
List<User> findAll2(List<Integer> list);
Test类
java
@Test
public void selectAll2(){
//获取sqlSess对象
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
//获取mapper映射对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//调用方法
//创建User对象
User user = new User();
//设置Username的值
user.setUsername("安其拉");
user.setPassword("123123");
List<User> users = mapper.selectAll2(user);
//打印结果
System.out.println(users);
//关闭sqlsession
sqlSession.close();
}
查询结果
java
==> Preparing: SELECT * FROM user where id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Row: 6, 2333322, 3222
<== Total: 3
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}, User{id=6,
username='3222', password='2333322'}]
- 单参数组
xml
<select id="findAll2" resultType="User">
SELECT * FROM user where id in
<foreach collection="array" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>
Usermapper
java
List<User> findAll2(Integer[] array);
@Test
java
@Test
public void testFindAll22() {
try {
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis.xml"))
;
// 得到 session 对象
SqlSession session = sqlSessionFactory.openSession();
//得到 mapper 接口的实现对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 通过 session 对象数据库
Integer[] array=new Integer[]{1,3,6};
List<User> list = mapper.findAll2(array);
// 控制台打印 list 对象
System.out.println(list);
// 关闭 session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
结果
java
==> Preparing: SELECT * FROM user where id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Row: 6, 2333322, 3222
<== Total: 3
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}, User{id=6,
username='3222', password='2333322'}]
- 多个参数使用map
xml
<select id="findAll2" resultType="User">
SELECT * FROM user where username like concat('%',#{username},'%') and id in
<foreach collection="ids" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>
UserMapper接口的方法
java
List<User> findAll2(Map<String,Object> map);
@Test
java
@Test
public void testFindAll22() {
try {
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis.xml"))
;
// 得到 session 对象
SqlSession session = sqlSessionFactory.openSession();
//得到 mapper 接口的实现对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 通过 session 对象数据库
Map<String,Object> map=new HashMap<>();
map.put("username", "d");
map.put("ids", Arrays.asList(1,3,6));
List<User> list = mapper.findAll2(map);
// 控制台打印 list 对象
System.out.println(list);
// 关闭 session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
结果
java
==> Preparing: SELECT * FROM user where username like '%d%' and id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Total: 2
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}]
四、MyBatis多表查询
在实际业务中,数据往往分散在多个表中,需要通过关联查询来获取完整信息。MyBatis 提供了强大的结果映射(ResultMap)功能,可以优雅地将复杂的 SQL 查询结果映射到 Java 对象中,支持一对一、一对多、多对多等关联关系。
1. 关联关系概述
数据库表之间的关联主要有三种类型:
- 一对一(One-to-One):例如一个用户(User)对应一个身份证(IDCard)。
- 一对多(One-to-Many):例如一个部门(Department)对应多个员工(Employee)。
- 多对多(Many-to-Many):例如一个学生(Student)可以选修多门课程(Course),一门课程也可以被多个学生选修。多对多关系通常通过中间表实现。
MyBatis 通过 <association>(对应一对一)和 <collection>(对应一对多/多对多)标签在 ResultMap 中定义这些关联映射。
2. 准备工作:示例表结构
假设我们有以下两张表,用于演示一对多查询:
部门表(department)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| name | VARCHAR(50) | 部门名称 |
员工表(employee)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| name | VARCHAR(50) | 员工姓名 |
| dept_id | BIGINT | 外键,关联部门表id |
对应的 Java 实体类:
java
// Department.java
public class Department {
private Long id;
private String name;
// 一对多:一个部门有多个员工
private List<Employee> employees;
// 构造方法、getter/setter、toString 省略
}
// Employee.java
public class Employee {
private Long id;
private String name;
private Long deptId;
// 多对一:一个员工属于一个部门
private Department department;
// 构造方法、getter/setter、toString 省略
}
3. 一对一关联查询(多对一)
场景:查询员工信息,并同时获取其所属的部门信息。
3.1 使用 ResultMap 的 <association>
在 EmployeeMapper.xml 中定义 ResultMap:
xml
<!-- 定义 resultMap,映射 Employee 对象及其关联的 Department 对象 -->
<resultMap id="employeeWithDeptResultMap" type="Employee">
<!-- 映射 Employee 的基本字段 -->
<id property="id" column="e_id"/>
<result property="name" column="e_name"/>
<result property="deptId" column="dept_id"/>
<!-- association 映射一对一关联对象 -->
<association property="department" javaType="Department">
<id property="id" column="d_id"/>
<result property="name" column="d_name"/>
</association>
</resultMap>
<!-- 使用 resultMap 进行查询 -->
<select id="selectEmployeeWithDepartment" resultMap="employeeWithDeptResultMap">
SELECT
e.id AS e_id,
e.name AS e_name,
e.dept_id,
d.id AS d_id,
d.name AS d_name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id
WHERE e.id = #{id}
</select>
3.2 接口方法
java
public interface EmployeeMapper {
Employee selectEmployeeWithDepartment(Long id);
}
4. 一对多关联查询
场景:查询部门信息,并同时获取该部门下的所有员工列表。
4.1 使用 ResultMap 的 <collection>
在 DepartmentMapper.xml 中定义 ResultMap:
xml
<!-- 定义 resultMap,映射 Department 对象及其关联的 Employee 集合 -->
<resultMap id="departmentWithEmployeesResultMap" type="Department">
<!-- 映射 Department 的基本字段 -->
<id property="id" column="d_id"/>
<result property="name" column="d_name"/>
<!-- collection 映射一对多关联集合 -->
<collection property="employees" ofType="Employee">
<id property="id" column="e_id"/>
<result property="name" column="e_name"/>
<result property="deptId" column="dept_id"/>
</collection>
</resultMap>
<!-- 使用 resultMap 进行查询 -->
<select id="selectDepartmentWithEmployees" resultMap="departmentWithEmployeesResultMap">
SELECT
d.id AS d_id,
d.name AS d_name,
e.id AS e_id,
e.name AS e_name,
e.dept_id
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
WHERE d.id = #{id}
</select>
4.2 接口方法
java
public interface DepartmentMapper {
Department selectDepartmentWithEmployees(Long id);
}
5. 多对多关联查询
假设新增课程表(course)和学生选课中间表(student_course):
课程表(course)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| course_name | VARCHAR(50) | 课程名称 |
学生选课中间表(student_course)
| 字段名 | 类型 | 说明 |
|---|---|---|
| student_id | BIGINT | 外键,关联学生表id |
| course_id | BIGINT | 外键,关联课程表id |
实体类:
java
// Student.java
public class Student {
private Long id;
private String name;
// 多对多:学生选修多门课程
private List<Course> courses;
// getter/setter 省略
}
// Course.java
public class Course {
private Long id;
private String courseName;
// 多对多:课程被多个学生选修
private List<Student> students;
// getter/setter 省略
}
5.1 查询学生及其选修课程
StudentMapper.xml:
xml
<resultMap id="studentWithCoursesResultMap" type="Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<!-- 多对多通过 collection 映射 -->
<collection property="courses" ofType="Course">
<id property="id" column="c_id"/>
<result property="courseName" column="course_name"/>
</collection>
</resultMap>
<select id="selectStudentWithCourses" resultMap="studentWithCoursesResultMap">
SELECT
s.id AS s_id,
s.name AS s_name,
c.id AS c_id,
c.course_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id
LEFT JOIN course c ON sc.course_id = c.id
WHERE s.id = #{id}
</select>
6. 嵌套查询(Nested Select)与延迟加载
除了使用 JOIN 一次性查询所有数据,MyBatis 还支持嵌套查询(分步查询),并可以配置延迟加载(懒加载),即用到关联对象时才执行子查询。
6.1 配置延迟加载
在 mybatis.xml 中开启延迟加载:
xml
<settings>
<!-- 其他配置... -->
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
6.2 嵌套查询示例
在 EmployeeMapper.xml 中,使用 <association> 的 select 属性指定另一个查询语句:
xml
<!-- 第一步:查询员工基本信息 -->
<resultMap id="employeeWithDeptNestedResultMap" type="Employee">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="deptId" column="dept_id"/>
<!-- 通过 select 属性引用另一个查询,column 传递参数 -->
<association property="department" column="dept_id"
select="com.tjetc.dao.DepartmentMapper.selectById"/>
</resultMap>
<select id="selectEmployeeWithDepartmentNested" resultMap="employeeWithDeptNestedResultMap">
SELECT id, name, dept_id FROM employee WHERE id = #{id}
</select>
DepartmentMapper.xml 中定义被引用的查询:
xml
<select id="selectById" resultType="Department">
SELECT id, name FROM department WHERE id = #{id}
</select>
这样,当调用 selectEmployeeWithDepartmentNested 时,只会先执行员工查询;只有当访问 employee.getDepartment() 时,MyBatis 才会执行 selectById 查询部门信息。
7. <resultMap> 与 <resultType> 的区别详解
在 MyBatis 的映射文件(Mapper XML)中,<select>、<insert>、<update>、<delete> 等语句标签的 resultType 或 resultMap 属性用于指定 SQL 查询结果的映射方式。理解两者的区别是掌握 MyBatis 结果映射的关键。
7.1 <resultType>:简单类型映射
resultType 用于指定查询结果直接映射到的 Java 类型(全限定类名或别名)。它适用于以下场景:
- 基本类型或简单对象 :当查询返回的列名与 Java 对象的属性名完全一致(或遵循驼峰命名自动映射)时,MyBatis 会自动创建该类型的实例,并将查询结果集的列值注入到对应属性中。
- 返回单个字段 :例如查询记录数 (
resultType="int"或resultType="java.lang.Integer")。 - 返回 Map :
resultType="map"会将结果集的每一行映射为一个Map<String, Object>,键为列名,值为列值。
示例:使用 resultType 映射简单对象
xml
<!-- 假设数据库 user 表的列名是 id, username, password -->
<!-- Java 类 User 的属性名也是 id, username, password -->
<select id="selectUser" resultType="com.tjetc.entity.User">
SELECT id, username, password FROM user WHERE id = #{id}
</select>
在这个例子中,MyBatis 会自动将 id、username、password 列的值映射到 User 对象的同名属性上。
优点:
- 配置简单:无需额外定义映射关系,适合属性与列名一一对应的简单场景。
- 代码简洁:直接指定类名即可。
局限性:
- 无法处理复杂映射 :当列名与属性名不一致、需要关联查询(嵌套对象或集合)、或者需要自定义类型处理器时,
resultType无能为力。 - 依赖命名约定:严重依赖于数据库列名与 Java 属性名的命名一致性(或开启驼峰映射配置)。
7.2 <resultMap>:高级自定义映射
resultMap 是一个显式定义的、可重用的映射规则 。它通过 <resultMap> 标签在 XML 中声明,并通过其 id 被其他语句引用。它适用于所有 resultType 无法处理的复杂场景。
核心能力:
- 解决列名与属性名不一致 :使用
<result>子标签明确指定column(数据库列名)到property(Java 属性名)的映射。 - 实现关联映射(一对一、一对多、多对多) :通过
<association>(关联单个对象)和<collection>(关联对象集合)标签,将复杂的 JOIN 查询结果映射到嵌套的对象或集合中。这正是上一节"多表查询"的核心。 - 支持继承 :一个
<resultMap>可以通过extends属性继承另一个,实现映射规则的复用。 - 自动映射控制 :通过
autoMapping属性可以控制是否自动映射未明确指定的列。 - 构造函数映射 :使用
<constructor>标签映射到构造方法的参数。 - 鉴别器 :使用
<discriminator>标签实现根据某列值选择不同的结果映射(类似switch-case)。
示例:使用 resultMap 处理列名不一致和关联查询
xml
<!-- 1. 定义基础的 resultMap,处理列名不一致 -->
<resultMap id="baseUserMap" type="User">
<id property="userId" column="id"/> <!-- 主键映射 -->
<result property="userName" column="username"/> <!-- 普通字段映射 -->
<result property="userPassword" column="password"/>
</resultMap>
<!-- 2. 定义扩展的 resultMap,包含部门关联(一对一) -->
<resultMap id="userWithDeptMap" type="User" extends="baseUserMap">
<!-- 使用 extends 继承基础映射 -->
<!-- association 处理一对一关联 -->
<association property="department" javaType="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
</association>
</resultMap>
<!-- 3. 在查询语句中引用自定义的 resultMap -->
<select id="selectUserWithDepartment" resultMap="userWithDeptMap">
SELECT
u.id,
u.username,
u.password,
d.id AS dept_id,
d.name AS dept_name
FROM user u
LEFT JOIN department d ON u.dept_id = d.id
WHERE u.id = #{id}
</select>
优点:
- 功能强大且灵活:可以处理任何复杂的数据结构映射。
- 解耦 SQL 与对象模型:数据库列设计可以独立于 Java 对象属性命名。
- 提升可维护性 :复杂的映射关系被集中定义和管理,
<resultMap>可以复用。
7.3 核心区别对比表
| 特性 | <resultType> |
<resultMap> |
|---|---|---|
| 定义方式 | 直接指定一个 Java 类(或别名)。 | 需要先使用 <resultMap> 标签显式定义映射规则,再通过其 id 引用。 |
| 映射机制 | 自动映射。基于列名与属性名一致(或驼峰规则)的约定。 | 显式映射 。通过 <id>, <result>, <association>, <collection> 等子标签精确控制。 |
| 处理列名不一致 | 不支持。必须列名与属性名一致,或开启全局驼峰映射。 | 支持 。通过 column 和 property 属性指定对应关系。 |
| 关联查询(嵌套对象/集合) | 不支持。只能映射到扁平的单层对象。 | 核心功能 。通过 <association> 和 <collection> 完美支持。 |
| 继承与复用 | 不支持。 | 支持 。通过 extends 属性实现映射规则的继承和复用。 |
| 适用场景 | 简单的单表查询,且对象属性与表列名完全对应。 | 所有复杂场景,包括:多表关联查询、列名与属性名不一致、自定义类型处理、构造函数映射等。 |
| 配置复杂度 | 低(几乎零配置)。 | 高(需要详细定义映射关系)。 |
7.4 如何选择?
- 优先使用
resultType:当你的查询是简单的单表查询,并且数据库列名与 JavaBean 属性名完全一致(或已配置驼峰命名自动映射)时,使用resultType可以让配置最简洁。 - 必须使用
resultMap:在以下任何情况下,都必须使用resultMap:- 查询涉及多表 JOIN,需要将结果映射到包含嵌套对象的复杂 Java 对象中。
- 数据库列名与 JavaBean 属性名不一致,且你不想或不能修改其中一方。
- 你需要对映射过程进行更精细的控制,例如使用特定的类型处理器(TypeHandler) 、映射到构造方法 ,或者使用鉴别器。
- 你需要复用映射规则。
7.5 总结
可以将 resultType 看作是 MyBatis 提供的一种"约定优于配置 "的快捷方式,它在简单场景下非常高效。而 resultMap 则是 MyBatis 映射能力的"完全体",它提供了完整的、声明式的配置能力,以应对实际开发中各种复杂的对象-关系映射需求。
五、Mybatis的缓存机制
MyBatis 内置了强大的缓存机制,可以有效减少数据库的访问次数,提升应用性能。MyBatis 的缓存分为两级:一级缓存 和二级缓存。
5.1 一级缓存(本地缓存/SqlSession 级别缓存)
一级缓存是 SqlSession 级别的缓存,默认开启,无法关闭。
5.1.1 工作原理
- 在同一个 SqlSession 中,执行相同的查询 SQL 时,第一次查询会将结果存入缓存,后续相同的查询直接从缓存中获取,不再访问数据库。
- 一级缓存的生命周期与 SqlSession 相同,当 SqlSession 关闭或执行了增删改 操作(
insert、update、delete)后,缓存会被清空。
5.1.2 示例与验证
java
@Test
public void testFirstLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,会访问数据库
User user1 = mapper.selectById(1L);
System.out.println("第一次查询: " + user1);
// 第二次查询相同数据,直接从一级缓存获取,不访问数据库
User user2 = mapper.selectById(1L);
System.out.println("第二次查询: " + user2);
// 验证是否为同一个对象(缓存命中)
System.out.println("user1 == user2: " + (user1 == user2)); // 输出 true
// 执行更新操作,清空一级缓存
user1.setUsername("newName");
mapper.update(user1);
sqlSession.commit(); // 提交事务
// 第三次查询,缓存已清空,会再次访问数据库
User user3 = mapper.selectById(1L);
System.out.println("更新后查询: " + user3);
sqlSession.close();
}
控制台输出可能如下:
==> Preparing: SELECT id, username, password FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
第一次查询: User{id=1, username='admin', password='123456'}
第二次查询: User{id=1, username='admin', password='123456'} // 无 SQL 日志,命中缓存
user1 == user2: true
==> Preparing: UPDATE user SET username=?, password=? WHERE id=?
==> Parameters: newName(String), 123456(String), 1(Long)
<== Updates: 1
==> Preparing: SELECT id, username, password FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
更新后查询: User{id=1, username='newName', password='123456'}
5.1.3 一级缓存失效场景
- SqlSession 不同:每个 SqlSession 有自己独立的一级缓存。
- SqlSession 相同,但查询条件不同:缓存基于 SQL 语句和参数,条件不同则缓存不命中。
- SqlSession 相同,两次查询之间执行了增删改操作:缓存会被清空。
- 手动清空缓存 :调用
sqlSession.clearCache()方法。 - SqlSession 关闭:缓存随之销毁。
5.2 二级缓存(全局缓存/namespace 级别缓存)
二级缓存是 namespace(Mapper 接口)级别 的缓存,多个 SqlSession 可以共享。需要手动开启。
5.2.1 开启二级缓存
步骤 1:在 mybatis-config.xml 中开启全局缓存(默认已开启,但建议显式配置)
xml
<settings>
<!-- 其他配置... -->
<!-- 开启全局二级缓存,默认为 true,可省略 -->
<setting name="cacheEnabled" value="true"/>
</settings>
步骤 2:在需要使用二级缓存的 Mapper.xml 文件中添加 <cache/> 标签
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="com.tjetc.dao.UserMapper">
<!-- 启用本 Mapper 的二级缓存 -->
<cache/>
<!-- 其他 SQL 映射语句 -->
<select id="selectById" resultType="User">
SELECT id, username, password FROM user WHERE id = #{id}
</select>
</mapper>
步骤 3:实体类实现 Serializable 接口(因为二级缓存可能将数据序列化到磁盘)
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... 原有属性和方法
}
5.2.2 <cache> 标签常用属性
xml
<cache
eviction="LRU" <!-- 回收策略:LRU(最近最少使用)、FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用) -->
flushInterval="60000" <!-- 刷新间隔(毫秒),默认不清空 -->
size="512" <!-- 缓存最多可以存储的对象数目 -->
readOnly="true" <!-- 是否只读:true(返回缓存对象的引用,性能高)、false(返回缓存对象的拷贝,安全) -->
blocking="false" <!-- 是否使用阻塞缓存 -->
/>
5.2.3 二级缓存工作原理与示例
java
@Test
public void testSecondLevelCache() {
// 第一个 SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
System.out.println("SqlSession1 第一次查询: " + user1);
sqlSession1.close(); // 必须关闭 SqlSession,查询结果才会存入二级缓存
// 第二个 SqlSession(不同 SqlSession)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); // 从二级缓存获取,不查询数据库
System.out.println("SqlSession2 查询(命中二级缓存): " + user2);
// 验证是否为同一个对象(取决于 readOnly 配置)
System.out.println("user1 == user2: " + (user1 == user2)); // readOnly=true 时为 false
sqlSession2.close();
}
5.2.4 二级缓存注意事项
- 事务提交后缓存生效 :只有 SqlSession 执行了
commit()或close()后,查询结果才会存入二级缓存。 - 增删改操作会清空对应 namespace 的缓存 :执行
insert、update、delete后,该 namespace 下的所有缓存都会被清空。 - 缓存共享问题:多个 SqlSession 共享缓存时,需要注意数据一致性问题。
- 缓存策略选择 :根据业务场景选择合适的
eviction(回收策略)和readOnly属性。
5.3 使用第三方缓存(如 Ehcache、Redis)
MyBatis 提供了缓存接口 org.apache.ibatis.cache.Cache,可以方便地集成第三方缓存。
5.3.1 集成 Ehcache 示例
步骤 1:添加依赖
xml
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
步骤 2:在 Mapper.xml 中指定缓存实现
xml
<mapper namespace="com.tjetc.dao.UserMapper">
<!-- 使用 Ehcache 作为二级缓存实现 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<!-- 其他配置 -->
</mapper>
步骤 3:添加 ehcache.xml 配置文件(可选,用于自定义配置)
xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
5.3.2 集成 Redis 缓存
类似地,可以使用 mybatis-redis 等适配器,将二级缓存数据存储到 Redis 中,实现分布式缓存。
5.4 缓存使用建议与最佳实践
-
合理使用缓存级别:
- 对于频繁查询且很少修改的数据,使用二级缓存。
- 对于会话级别的数据,依赖一级缓存即可。
- 对于实时性要求高的数据,不建议使用缓存或设置较短的过期时间。
-
避免缓存穿透:
- 对于查询结果为
null的情况,也可以缓存(缓存空对象),但需要设置较短的过期时间。
- 对于查询结果为
-
注意缓存范围:
- 二级缓存是 namespace 级别的,如果多个 Mapper 操作同一张表,需要考虑缓存同步问题。
- 可以通过
<cache-ref>标签让多个 Mapper 共享同一个缓存空间。
-
监控缓存命中率:
- 在生产环境中,应监控缓存的命中率,根据实际情况调整缓存策略。
-
结合业务场景选择缓存策略:
- 查询为主的应用:可以大量使用缓存提升性能。
- 写多读少的应用:需要谨慎使用缓存,避免频繁的缓存失效。
5.5 总结
MyBatis 的缓存机制是其高性能的重要保障:
- 一级缓存:SqlSession 级别,默认开启,适用于会话内的数据重复查询。
- 二级缓存:namespace 级别,需要手动配置,适用于跨会话的共享数据查询。
- 第三方缓存:可以通过实现 Cache 接口集成 Ehcache、Redis 等,满足分布式、高可用的缓存需求。
合理使用缓存可以显著提升应用性能,但同时也需要关注数据一致性和内存占用问题。在实际项目中,应根据具体业务场景选择合适的缓存策略和配置参数。