525 lines
19 KiB
Markdown
525 lines
19 KiB
Markdown
---
|
||
title: MyBatis简介和使用
|
||
date: 2025-11-27
|
||
---
|
||
|
||
# 一、简介
|
||
  [MyBatis](https://mybatis.org/mybatis-3/)是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
|
||
|
||
# 二、安装
|
||
### 2.1 引入依赖
|
||
  在`pom.xml`文件中,引入依赖:
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>org.mybatis</groupId>
|
||
<artifactId>mybatis</artifactId>
|
||
<version>x.x.x</version>
|
||
</dependency>
|
||
```
|
||
|
||
  可以在Github中查看[MyBatis](https://github.com/mybatis/mybatis-3)最新版本号。
|
||
|
||
### 2.2 配置文件
|
||
  在`resource`文件夹中新建mybatis-config.xml文件和mapper->BlogMapper.xml映射文件:
|
||
|
||
```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">
|
||
<!-- JDBC驱动名称 -->
|
||
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
|
||
<!-- 数据库地址 -->
|
||
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT"/>
|
||
<!-- 数据库用户名 -->
|
||
<property name="username" value="root"/>
|
||
<!-- 数据库密码-->
|
||
<property name="password" value="123456"/>
|
||
</dataSource>
|
||
</environment>
|
||
</environments>
|
||
|
||
<!-- 映射器 -->
|
||
<mappers>
|
||
<!-- mapper文件 -->
|
||
<mapper resource="mapper/BlogMapper.xml"/>
|
||
</mappers>
|
||
</configuration>
|
||
```
|
||
|
||
  注:如果使用MySql数据库,需要增加MySql驱动依赖:
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>mysql</groupId>
|
||
<artifactId>mysql-connector-java</artifactId>
|
||
<version>8.0.12</version>
|
||
</dependency>
|
||
```
|
||
|
||
### 2.3 定义映射语句
|
||
  新建dao->BlogDao.java:
|
||
|
||
```java
|
||
public interface BlogDao {
|
||
Blog selectBlog(@Param("id") Integer id);
|
||
}
|
||
```
|
||
|
||
  BlogMapper.xml:
|
||
|
||
```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.example.mybatislearn.dao.BlogDao">
|
||
<select id="selectBlog" resultType="com.example.mybatislearn.entity.Blog">
|
||
select * from Blog where author_id = #{id}
|
||
</select>
|
||
</mapper>
|
||
```
|
||
|
||
### 2.4 执行SqlSession
|
||
```java
|
||
// 配置文件路径
|
||
String resource = "mybatis-config.xml";
|
||
try {
|
||
// 读取配置文件
|
||
InputStream inputStream = Resources.getResourceAsStream(resource);
|
||
// 构建SqlSession工厂
|
||
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
|
||
// 获取SqlSession
|
||
SqlSession session = sqlSessionFactory.openSession();
|
||
// 获取映射文件
|
||
BlogDao blogDao = session.getMapper(BlogDao.class);
|
||
// 执行已映射的SQL语句
|
||
Blog blog = blogDao.selectBlog(101);
|
||
System.out.println(blog);
|
||
// 关闭SqlSession
|
||
session.close();
|
||
} catch (IOException e) {
|
||
throw new RuntimeException(e);
|
||
}
|
||
```
|
||
|
||
  每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。
|
||
  SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。
|
||
  而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
|
||
|
||
### 2.5 作用域和生命周期
|
||
1. SqlSessionFactoryBuilder
|
||
|
||
  这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
|
||
|
||
2. SqlSessionFactory
|
||
|
||
  SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
|
||
|
||
3. SqlSession
|
||
|
||
  每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
|
||
|
||
# 三、注入SpringBoot框架
|
||
## 3.1 引入依赖
|
||
  将之前MyBatis的依赖替换成MyBatis的SpringBoot Starter:
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>org.mybatis.spring.boot</groupId>
|
||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||
<version>2.3.0</version>
|
||
</dependency>
|
||
```
|
||
|
||
## 3.2 配置文件
|
||
  在Resource文件夹下新建application.yml:
|
||
|
||
```yaml
|
||
spring:
|
||
datasource:
|
||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||
url: jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
|
||
username: root
|
||
password: 123456
|
||
|
||
mybatis:
|
||
# mapper文件路径
|
||
mapper-locations: classpath*:mapper/*Mapper.xml
|
||
configuration:
|
||
# 开启日志
|
||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||
```
|
||
|
||
  注:这里的url的写法和XML文件中的写法不一致。
|
||
|
||
# 3.3 定义映射语句
|
||
  在原映射接口文件BlogDao.java中添加@Mapper注解
|
||
|
||
## 3.4 实现原理
|
||
  引入mybatis-spring-boot-starter模块之后,其可以:
|
||
|
||
1. **自动检测DataSource**
|
||
2. **使用SqlSessionFactoryBean注册SqlSessionFactory 实例,并设置DataSource数据源**
|
||
3. **基于SqlSessionFactory自动注册SqlSessionTemplate实例**
|
||
4. **自动扫描@Mapper注解类,并通过SqlSessionTemplate注册到Spring Context中**
|
||
|
||
  每次执行@Mapper映射文件中的接口时,都会自动开启一个SqlSession并在执行结束时关闭。
|
||
|
||
## 3.5 执行映射语句
|
||
```java
|
||
Blog blog = blogDao.selectBlog(101);
|
||
System.out.println(blog);
|
||
```
|
||
|
||
  相较之前的写法,节省了大量的配置工作。
|
||
|
||
# 四、高级特性
|
||
## 4.1 动态参数
|
||
```xml
|
||
#{}是参数占位符的标记,它可以防止SQL注入,当使用#{}时,MyBatis会自动处理参数的数据类型,
|
||
如果参数是字符串,它会给传入的值加上引号,这样可以有效地防止SQL注入攻击。
|
||
|
||
${}则是直接将参数值嵌入SQL语句中。当使用${}时,传入的参数会直接显示在SQL中,
|
||
MyBatis不会对参数进行任何类型转换或加引号处理。一般用在动态表名、列名或数据库名称中。
|
||
```
|
||
|
||
## 4.2 SQL片段
|
||
  可以用来定义可重复的SQL代码片段:
|
||
|
||
```xml
|
||
<sql id="userColumns">
|
||
id, username, email, phone
|
||
</sql>
|
||
|
||
<select id="findAllUsers" resultType="User">
|
||
SELECT <include refid="userColumns" /> FROM users
|
||
</select>
|
||
```
|
||
|
||
## 4.3 批量操作
|
||
  推荐使用集合方式批量操作:
|
||
|
||
```java
|
||
@Mapper
|
||
public interface UserMapper {
|
||
Integer insertUsers(@Param("list") List<User> userList);
|
||
}
|
||
|
||
```
|
||
|
||
```xml
|
||
<insert id="insertUsers">
|
||
INSERT INTO user (username, password)
|
||
VALUES
|
||
<foreach collection ="list" item="item" separator =",">
|
||
(#{item.username}, #{item.password})
|
||
</foreach>
|
||
</insert>
|
||
```
|
||
|
||
## 4.4 结果映射
|
||
  复杂结果缓存
|
||
|
||
```xml
|
||
<!-- 非常复杂的结果映射 -->
|
||
<resultMap id="detailedBlogResultMap" type="Blog">
|
||
<!-- 一般不需要 -->
|
||
<constructor>
|
||
<idArg column="blog_id" javaType="int"/>
|
||
</constructor>
|
||
<result property="title" column="blog_title"/>
|
||
<!-- 复杂对象 1对1 -->
|
||
<association property="author" javaType="Author">
|
||
<id property="id" column="author_id"/>
|
||
<result property="username" column="author_username"/>
|
||
<result property="password" column="author_password"/>
|
||
<result property="email" column="author_email"/>
|
||
<result property="bio" column="author_bio"/>
|
||
<result property="favouriteSection" column="author_favourite_section"/>
|
||
</association>
|
||
<!-- 列表 1对多-->
|
||
<collection property="posts" ofType="Post">
|
||
<id property="id" column="post_id"/>
|
||
<result property="subject" column="post_subject"/>
|
||
<association property="author" javaType="Author"/>
|
||
<collection property="comments" ofType="Comment">
|
||
<id property="id" column="comment_id"/>
|
||
</collection>
|
||
<collection property="tags" ofType="Tag" >
|
||
<id property="id" column="tag_id"/>
|
||
</collection>
|
||
<discriminator javaType="int" column="draft">
|
||
<case value="1" resultType="DraftPost"/>
|
||
</discriminator>
|
||
</collection>
|
||
</resultMap>
|
||
```
|
||
|
||
  其中`<collection>`也可以使用嵌套查询:
|
||
|
||
```xml
|
||
<collection property="posts" ofType="Post" select="queryPost"/>
|
||
|
||
<resultMap id="postResultMap" type="Post">
|
||
<id property="id" column="post_id"/>
|
||
<result property="subject" column="post_subject"/>
|
||
<association property="author" javaType="Author"/>
|
||
<collection property="comments" ofType="Comment">
|
||
<id property="id" column="comment_id"/>
|
||
</collection>
|
||
<collection property="tags" ofType="Tag" >
|
||
<id property="id" column="tag_id"/>
|
||
</collection>
|
||
<discriminator javaType="int" column="draft">
|
||
<case value="1" resultType="DraftPost"/>
|
||
</discriminator>
|
||
</resultMap>
|
||
|
||
<select id="queryPost" resultMap="postResultMap">
|
||
|
||
</select>
|
||
|
||
```
|
||
|
||
  如果需要传递参数,可以在`<collection>`添加`column`属性:
|
||
|
||
```xml
|
||
<!-- 单个参数 -->
|
||
<collection property="posts" column="name" ofType="Post" select="queryPost"/>
|
||
<!-- 多个参数 -->
|
||
<collection property="posts" column="{param1=param_1, param2=param_2}" ofType="Post" select="queryPost"/>
|
||
```
|
||
|
||
  注:建立在非列表数据时使用嵌套查询,否则每查到一个数据都会进行一次子查询操作。
|
||
|
||
## 4.5 一二级缓存
|
||
  默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>
|
||
|
||
+ 映射语句文件中的所有 select 语句的结果将会被缓存。
|
||
+ 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
|
||
+ 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession,二级缓存针对全局范围。
|
||
|
||
## 4.6 动态SQL
|
||
+ if :是/否
|
||
|
||
```xml
|
||
<select id="findActiveBlogWithTitleLike" resultType="Blog">
|
||
SELECT * FROM BLOG
|
||
WHERE state = 'ACTIVE'
|
||
<if test="title != null">
|
||
AND title like #{title}
|
||
</if>
|
||
</select>
|
||
```
|
||
|
||
+ choose、when、otherwise:选择其中一个
|
||
|
||
```xml
|
||
<select id="findActiveBlogLike" resultType="Blog">
|
||
SELECT * FROM BLOG WHERE state = 'ACTIVE'
|
||
<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>
|
||
</select>
|
||
```
|
||
|
||
+ where、set:解决SQL语法问题
|
||
|
||
```xml
|
||
<select id="findActiveBlogLike" resultType="Blog">
|
||
SELECT * FROM BLOG
|
||
<where>
|
||
<if test="state != null">
|
||
state = #{state}
|
||
</if>
|
||
<if test="title != null">
|
||
AND title like #{title}
|
||
</if>
|
||
</where>
|
||
</select>
|
||
|
||
<update id="updateAuthorIfNecessary">
|
||
update Author
|
||
<set>
|
||
<if test="username != null">username=#{username},</if>
|
||
<if test="password != null">password=#{password},</if>
|
||
</set>
|
||
where id=#{id}
|
||
</update>
|
||
```
|
||
|
||
  where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
|
||
|
||
# 五、自定义类型处理器
|
||
  MyBatis 在预处理语句(PreparedStatement)中设置参数时,会从 Java 类型(javaType)转换为 JDBC 类型(jdbcType);而从结果集中取出值时,会将 JDBC 类型转换为 Java 类型。这个转换工作就是由 TypeHandler来完成的。
|
||
  需要创建一个类来实现 org.apache.ibatis.type.TypeHandler接口,或者继承 org.apache.ibatis.type.BaseTypeHandler类实现自定义类型处理器。
|
||
  需要实现的方法:
|
||
```java
|
||
/**
|
||
* 将Java对象设置到PreparedStatement中(Java类型 → JDBC类型)
|
||
* @param ps PreparedStatement对象
|
||
* @param i 参数位置(从1开始)
|
||
* @param parameter 要设置的Java对象(非空)
|
||
* @param jdbcType JDBC类型
|
||
*/
|
||
@Override
|
||
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
|
||
// 实现转换逻辑:T → JDBC类型
|
||
}
|
||
|
||
/**
|
||
* 根据列名从ResultSet中获取值(JDBC类型 → Java类型)
|
||
* @param rs ResultSet对象
|
||
* @param columnName 列名
|
||
* @return 转换后的Java对象
|
||
*/
|
||
@Override
|
||
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||
// 实现转换逻辑:JDBC类型 → T
|
||
}
|
||
|
||
/**
|
||
* 根据列索引从ResultSet中获取值
|
||
* @param rs ResultSet对象
|
||
* @param columnIndex 列索引(从1开始)
|
||
* @return 转换后的Java对象
|
||
*/
|
||
@Override
|
||
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||
// 实现转换逻辑:JDBC类型 → T
|
||
}
|
||
|
||
/**
|
||
* 从CallableStatement中获取值(用于存储过程)
|
||
* @param cs CallableStatement对象
|
||
* @param columnIndex 列索引
|
||
* @return 转换后的Java对象
|
||
*/
|
||
@Override
|
||
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||
// 实现转换逻辑:JDBC类型 → T
|
||
}
|
||
```
|
||
|
||
  例如自定义一个处理 MEDIUMBLOB 字段与 Base64 字符串的转换:
|
||
```java
|
||
/**
|
||
* 处理 MEDIUMBLOB 字段与 Base64 字符串的转换
|
||
*/
|
||
@MappedJdbcTypes(JdbcType.BLOB)
|
||
@MappedTypes(String.class)
|
||
public class BlobToBase64TypeHandler extends BaseTypeHandler<String> {
|
||
|
||
@Override
|
||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||
// Base64字符串 -> 数据库的byte[]
|
||
if (parameter != null && !parameter.trim().isEmpty()) {
|
||
byte[] bytes = Base64.getDecoder().decode(parameter);
|
||
ps.setBytes(i, bytes);
|
||
} else {
|
||
ps.setBytes(i, null);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||
// 数据库的byte[] -> Base64字符串
|
||
byte[] bytes = rs.getBytes(columnName);
|
||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||
}
|
||
|
||
@Override
|
||
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||
byte[] bytes = rs.getBytes(columnIndex);
|
||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||
}
|
||
|
||
@Override
|
||
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||
byte[] bytes = cs.getBytes(columnIndex);
|
||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||
}
|
||
}
|
||
```
|
||
|
||
  例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:
|
||
```java
|
||
public class EncryptTypeHandler extends BaseTypeHandler<String> {
|
||
@Override
|
||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||
ps.setString(i, AESUtil.encrypt(parameter)); // 自定义加密方法
|
||
}
|
||
|
||
@Override
|
||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||
String encrypted = rs.getString(columnName);
|
||
return AESUtil.decrypt(encrypted); // 自定义解密方法
|
||
}
|
||
}
|
||
```
|
||
|
||
  然后在实体类字段上加上 @TableField(typeHandler = EncryptTypeHandler.class):
|
||
```java
|
||
@TableName("user")
|
||
public class User {
|
||
private Long id;
|
||
private String name;
|
||
|
||
// 这个字段在数据库里存的是加密后的字符串
|
||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||
private String phone; // 例如 "13800138000" → 存为 "U2FsdGVkX1+oO1W..."
|
||
|
||
@TableField(typeHandler = EncryptTypeHandler.class)
|
||
private String idCard; // 身份证号
|
||
|
||
// 普通字段,不加密
|
||
private String email;
|
||
}
|
||
```
|
||
|
||
  如果是在XML文件中查询,需要指定TypeHandler:
|
||
```xml
|
||
<resultMap id="UserResultMap" type="com.example.entity.User">
|
||
<id column="id" property="id" />
|
||
<result column="name" property="name" />
|
||
<!-- 关键:phone 字段使用 EncryptTypeHandler -->
|
||
<result column="phone" property="phone" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||
<!-- idCard 字段也用同一个处理器 -->
|
||
<result column="id_card" property="idCard" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||
<result column="email" property="email" />
|
||
</resultMap>
|
||
```
|
||
|
||
  MyBatisPlus中定义了一些常用的类型处理器,例如:JacksonTypeHandler:
|
||
```java
|
||
@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)
|
||
```
|
||
|
||
  在XML文件中,将typeHandler设置为`com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler`。
|
||
|
||
# 六、MyBatis/MyBatis Plus常见问题
|
||
1. 更新值为null的字段时会失效
|
||
|
||
  需要更改字段的更新策略:
|
||
```java
|
||
@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
|
||
```
|
||
|
||
2. 自动填充出现失效
|
||
|
||
  strictInsert/UpdateFill的默认填充策略是:如果实体类中需要自动填充的字段已经有值了,那么当前值就不会进行填充,或者你想给一个字段填充null值也是不可以的。可以替换为setFieldValByName,将其直接覆盖。(一般是更新的时候需要替换,新增默认就是没有值的) |