Files
blog-press/docs/Web/MySQL/MyBatis.md
2026-05-20 11:26:38 +08:00

19 KiB
Raw Blame History

title, date
title date
MyBatis简介和使用 2025-11-27

一、简介

MyBatis是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJOPlain Old Java Objects普通老式 Java 对象)为数据库中的记录。

二、安装

2.1 引入依赖

  在pom.xml文件中,引入依赖:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

可以在Github中查看MyBatis最新版本号。

2.2 配置文件

  在resource文件夹中新建mybatis-config.xml文件和mapper->BlogMapper.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&amp;useUnicode=true&amp;characterEncoding=utf8&amp;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驱动依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.12</version>
</dependency>

2.3 定义映射语句

新建dao->BlogDao.java

public interface BlogDao {
    Blog selectBlog(@Param("id") Integer id);
}

BlogMapper.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

// 配置文件路径
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 解析资源可以被释放给更重要的事情。

  1. SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

  1. SqlSession

  每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession返回一个响应后就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。

三、注入SpringBoot框架

3.1 引入依赖

将之前MyBatis的依赖替换成MyBatis的SpringBoot Starter

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.3.0</version>
</dependency>

3.2 配置文件

在Resource文件夹下新建application.yml

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 执行映射语句

Blog blog = blogDao.selectBlog(101);
System.out.println(blog);

  相较之前的写法,节省了大量的配置工作。

四、高级特性

4.1 动态参数

#{}是参数占位符的标记它可以防止SQL注入当使用#{}时MyBatis会自动处理参数的数据类型
如果参数是字符串它会给传入的值加上引号这样可以有效地防止SQL注入攻击。

${}则是直接将参数值嵌入SQL语句中。当使用${}时传入的参数会直接显示在SQL中
MyBatis不会对参数进行任何类型转换或加引号处理。一般用在动态表名、列名或数据库名称中。

4.2 SQL片段

可以用来定义可重复的SQL代码片段

<sql id="userColumns">
    id, username, email, phone
</sql>

<select id="findAllUsers" resultType="User">
    SELECT <include refid="userColumns" /> FROM users
</select>

4.3 批量操作

  推荐使用集合方式批量操作:

@Mapper
public interface UserMapper {
    Integer insertUsers(@Param("list") List<User> userList);
}

<insert id="insertUsers">
    INSERT INTO user (username, password)
    VALUES
    <foreach collection ="list" item="item" separator =",">
        (#{item.username}, #{item.password})
    </foreach>
</insert>

4.4 结果映射

  复杂结果缓存

<!-- 非常复杂的结果映射 -->
<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>也可以使用嵌套查询:

<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属性:

<!-- 单个参数 -->
<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 映射文件中添加一行:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession二级缓存针对全局范围。

4.6 动态SQL

  • if :是/否
<select id="findActiveBlogWithTitleLike" resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = 'ACTIVE'
  <if test="title != null">
    AND title like #{title}
  </if>
</select>
  • choose、when、otherwise选择其中一个
<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语法问题
<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对象设置到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 字符串的转换:

/**
 * 处理 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;
    }
}

  例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:

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)

@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

<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

@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)

在XML文件中将typeHandler设置为com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler

六、MyBatis/MyBatis Plus常见问题

  1. 更新值为null的字段时会失效

  需要更改字段的更新策略:

@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
  1. 自动填充出现失效

strictInsert/UpdateFill的默认填充策略是如果实体类中需要自动填充的字段已经有值了那么当前值就不会进行填充或者你想给一个字段填充null值也是不可以的。可以替换为setFieldValByName将其直接覆盖。一般是更新的时候需要替换新增默认就是没有值的