javaweb-Day08-Mybatis

TJCcc 发布于 11 天前 18 次阅读


1. MyBatis 简介

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。与 Hibernate 等全自动 ORM 框架不同,MyBatis 是半自动的,需要开发者自己编写 SQL,但提供了强大的结果映射和动态 SQL 能力,让你能更精细地控制数据库操作。

核心特点:

  • SQL 与代码分离:SQL 写在 XML 文件或注解中,便于集中管理和优化。
  • 强大的结果映射:自动将查询结果封装为 Java 对象(POJO、Map 等)。
  • 动态 SQL:根据条件动态拼接 SQL,解决复杂查询难题。
  • 预编译支持:有效防止 SQL 注入,提高性能。

2. 底层核心原理

2.1 预编译 SQL(#{} 防注入)

在 MyBatis 中,#{} 会被解析为 JDBC 的 PreparedStatement 参数占位符 ?,实现预编译

工作流程:

  1. 解析 SQL,将 #{param} 替换为 ?,形成预编译模板。
  2. 将参数值通过 PreparedStatement.setXxx() 安全地设置进去。
  3. 数据库只编译一次 SQL 骨架,后续执行只需传入参数,提升性能。

SQL 注入防护原理
参数值不会直接参与 SQL 语句的编译,而是作为纯数据绑定,因此恶意输入不会改变 SQL 结构。

<select id="findByName" resultType="User">
    SELECT * FROM user WHERE name = #{name}
</select>

最终执行的 SQL 类似:SELECT * FROM user WHERE name = ?,参数 name 的值无论包含什么特殊字符,都不会破坏 SQL 语法。

对比 ${}
${} 是字符串替换,会直接将参数值拼接到 SQL 中,存在注入风险,一般只用于动态表名、列名等无法预编译的场景。

<!-- 危险:可能引发 SQL 注入 -->
<select id="findByName" resultType="User">
    SELECT * FROM user WHERE name = '${name}'
</select>

2.2 Mapper 接口自动生成代理类对象

MyBatis 最神奇的地方:你只定义了一个接口,没有实现类,却能直接调用方法执行 SQL。这依赖 JDK 动态代理

代理生成过程:

  1. 解析映射文件/注解:启动时,MyBatis 加载 XML 或注解中的 SQL,构造 MappedStatement 对象,以 namespace.id 为键存入 Configuration
  2. 获取 Mapper 代理:调用 SqlSession.getMapper(Class<T> type) 时,由 MapperProxyFactory 创建代理实例。
  3. 调用拦截:代理类 MapperProxy 实现了 InvocationHandler,方法调用被 invoke() 方法拦截。
  4. 定位 SQL:根据接口全限定名 + 方法名(即 namespace.id)找到对应的 MappedStatement
  5. 执行 SQL:通过 SqlSession 的对应方法(selectOneinsert 等)执行,并将参数传递过去。

简要代码示意:

// MapperProxy 内部逻辑(简化)
public Object invoke(Object proxy, Method method, Object[] args) {
    String statement = method.getDeclaringClass().getName() + "." + method.getName();
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 根据 SQL 类型(SELECT/INSERT...)调用 SqlSession 的不同方法
    return sqlSession.selectOne(statement, args);
}

所以,当你调用 userMapper.findById(1) 时,实际上是通过代理执行了 sqlSession.selectOne("com.xxx.mapper.UserMapper.findById", 1)

2.3 依赖注入(Spring 集成原理)

在 Spring 环境中,我们能用 @Autowired 直接注入 Mapper 接口,这背后是 FactoryBean + 扫描注册 机制。

核心类:

  • MapperFactoryBean:为单个 Mapper 接口创建代理的工厂 Bean,getObject() 返回 SqlSession.getMapper(Class) 的代理对象。
  • MapperScannerConfigurer@MapperScan:批量扫描指定包下的接口,为每个接口生成 MapperFactoryBean 的 Bean 定义并注册到容器。

当容器启动时,MapperScannerConfigurer 会:

  1. 扫描包路径,获取所有接口。
  2. 将接口类型构造为 MapperFactoryBean 的 BeanDefinition。
  3. 设置 SqlSessionFactorySqlSessionTemplate 等属性。
  4. 在注入时,Spring 调用 MapperFactoryBean.getObject() 获得 Mapper 代理,完成注入。

这样你的 Service 层就可以像使用普通 Bean 一样使用 Mapper 了。


3. 注解方式配置 SQL 实现 CRUD

MyBatis 提供简单的注解来快速实现增删改查,适合简单、不变的 SQL。

常用注解:

  • @Select / @Insert / @Update / @Delete
  • @Param:为参数命名,在 SQL 中通过 #{name} 引用
  • @Options:配置自增主键等选项

示例:UserMapper 接口

public interface UserMapper {

    @Select("SELECT id, name, age FROM user WHERE id = #{id}")
    User findById(@Param("id") Long id);

    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @Update("UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}")
    int update(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    int delete(@Param("id") Long id);
}

注意:

  • 当方法参数只有一个简单类型且未用 @Param 时,SQL 中可以直接用 #{value} 或任意名字,但建议始终使用 @Param 明确命名。
  • 注解方式写复杂 SQL 不易阅读,动态 SQL 支持差(可以用 <script> 在注解中嵌入 XML 动态标签,但很少用),所以复杂业务推荐 XML 映射方式

4. XML 映射文件配置 SQL 实现 CRUD(重点)

XML 映射文件是 MyBatis 的精华,适合复杂 SQL、动态 SQL 及结果映射。

4.1 文件结构与 namespace

<?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 必须是对应 Mapper 接口的全限定类名 -->

<mapper namespace="com.example.mapper.UserMapper">

    <!-- SQL 语句定义在这里 -->

</mapper>

4.2 基本 CRUD 标签

查询 select

  • id:方法名
  • resultType:返回值类型(全限定类名或别名,如 Usermap
  • parameterType:可省略,MyBatis 自动识别
<select id="findById" resultType="com.example.model.User">
    SELECT id, name, age FROM user WHERE id = #{id}
</select>

自动将查询列映射到 User 对象的属性(要求列名和属性名一致,或开启驼峰映射)。

插入 insert

  • useGeneratedKeys="true" 配合 keyProperty 可获取自增主键
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user(name, age) VALUES(#{name}, #{age})
</insert>

执行后,传入的 User 对象的 id 会被自动赋值。

更新 update

<update id="update">
    UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
</update>

删除 delete

<delete id="delete">
    DELETE FROM user WHERE id = #{id}
</delete>

4.3 参数传递详解

单个基本类型参数:SQL 中直接 #{任意名},但建议用 @Param 命名。

多个参数:默认 MyBatis 会以 param1, param2...arg0, arg1... 表示,必须使用 @Param 指定名字

User findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
<select id="findByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>

POJO 对象:直接 #{属性名}

Map 参数#{key}

#{ }${ } 区别
#{} 预编译占位,安全;${} 字符串替换,用于列名、表名等,有注入风险。

4.4 结果映射 resultMap

当列名与属性名不一致时,或需要关联映射时使用。

<resultMap id="userMap" type="com.example.model.User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <result property="age" column="user_age"/>
</resultMap>

<select id="findById" resultMap="userMap">
    SELECT user_id, user_name, user_age FROM user WHERE user_id = #{id}
</select>

5. 动态 SQL(XML 方式)

动态 SQL 是 MyBatis 的强大特性,让你能根据传入参数的条件生成不同的 SQL 语句。

5.1 <if>

根据条件动态包含 SQL 片段。

<select id="findByCondition" resultType="User">
    SELECT * FROM user WHERE 1=1
    <if test="name != null and name != ''">
        AND name = #{name}
    </if>
    <if test="age != null">
        AND age = #{age}
    </if>
</select>

test 属性是 OGNL 表达式,从参数取值。此例中如果 name 为空,对应的 AND 就不会拼接,但 WHERE 1=1 是为了防止所有条件都空时出现多余的 WHERE,这可以通过 <where> 改善。

5.2 <where>

自动处理 WHERE 关键字和开头的 AND/OR,当内部至少有一个条件成立时插入 WHERE,并自动去掉首个 AND/OR

<select id="findByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

效果:如果所有 if 都不成立,不会生成 WHERE;如果第一个成立会生成 WHERE name=?,第二个成立时会自动去掉 AND age=? 前的 AND,完美拼接。

5.3 <set>

用于 UPDATE 语句,动态添加 SET 子句,并自动去掉末尾的逗号。

<update id="updateSelective">
    UPDATE user
    <set>
        <if test="name != null">
            name = #{name},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
    </set>
    WHERE id = #{id}
</update>

若只有 name 传入,则生成 UPDATE user SET name = ? WHERE id = ?;若都不传,则 <set> 为空,MyBatis 会报错(因为没有 SET 子句),实际使用时应保证至少一个非 null。

5.4 <foreach>

遍历集合,常用于 IN 查询和批量插入。

属性:

  • collection:集合参数名(@Param 指定,或默认 list/array)
  • item:迭代项
  • open:前缀
  • close:后缀
  • separator:分隔符
  • index:可选,迭代序号(Map 时为 key)

示例 1:批量删除

int deleteByIds(@Param("ids") List<Long> ids);
<delete id="deleteByIds">
    DELETE FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</delete>

生成 SQL:DELETE FROM user WHERE id IN (1,2,3)

示例 2:批量插入

<insert id="batchInsert">
    INSERT INTO user(name, age) VALUES
    <foreach collection="users" item="user" separator=",">
        (#{user.name}, #{user.age})
    </foreach>
</insert>

5.5 <sql><include>

用于定义可复用的 SQL 片段,提高维护性。

定义片段:

<sql id="userColumns">
    id, name, age
</sql>

引用片段:

<select id="findAll" resultType="User">
    SELECT <include refid="userColumns"/> FROM user
</select>

<sql> 内还可以包含动态标签,<include> 支持通过 <property> 传值,实现片段参数化。

<sql id="userWhere">
    <where>
        <if test="name != null">AND name = #{name}</if>
        <if test="age != null">AND age = #{age}</if>
    </where>
</sql>

<select id="findByCondition" resultType="User">
    SELECT <include refid="userColumns"/> FROM user
    <include refid="userWhere"/>
</select>

6. 总结与最佳实践

  • SQL 安全:始终使用 #{} 进行参数绑定,避免 SQL 注入;只在动态表名、列名等少数场景使用 ${}
  • 动态 SQL:善用 <where><set><foreach>,让 SQL 简洁且健壮;<sql><include> 减少重复代码。
  • 结果映射:建议开启 mapUnderscoreToCamelCase(驼峰自动映射),简单场景不用写 resultMap。
  • 参数规范:多个参数务必使用 @Param 命名;DAO 层方法名与 XML id 一致。
  • 集成 Spring Boot:使用 @MapperScan 扫描包,Mapper 可直接注入;XML 文件通常放在 resources/mapper 下。
  • 性能:预编译带来的性能提升;尽量少用 ${} 破坏预编译;合理使用连接池。

MyBatis 的精髓在于让你掌控 SQL,同时通过动态代理和反射技术将 SQL 与接口无缝连接。理解其底层原理会让你在遇到问题时更容易定位根源,写出更优雅、更高效的数据库访问代码。


7. Spring Boot 集成 MyBatis:application.properties 配置

在 Spring Boot 项目中,我们通常使用 mybatis-spring-boot-starter 来整合 MyBatis,所有配置都可以在 application.properties(或 application.yml)中完成。

7.1 引入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version> <!-- 根据实际 Spring Boot 版本选择 -->
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 如果使用 Druid 连接池可单独引入 -->

7.2 基础数据源配置

# 数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

7.3 MyBatis 核心配置

# ---------- MyBatis 配置 ----------
# Mapper XML 文件位置(支持通配符,多个用逗号分隔)
mybatis.mapper-locations=classpath:mapper/*.xml

# 类型别名包(扫描后实体类首字母小写即为别名,如 com.example.model.User -> user)
mybatis.type-aliases-package=com.example.model

# MyBatis 配置文件路径(如需自定义 settings、typeHandlers 等)
mybatis.config-location=classpath:mybatis-config.xml

# 配置 MyBatis 的 Configuration 参数(可代替 mybatis-config.xml 中的 settings)
# 开启驼峰命名自动映射(user_name -> userName)
mybatis.configuration.map-underscore-to-camel-case=true
# 开启二级缓存,默认 true
mybatis.configuration.cache-enabled=true
# 当返回行为空时,自动给基本类型赋默认值,避免 NPE
mybatis.configuration.call-setters-on-nulls=true
# 日志实现,可指定 STDOUT_LOGGING(标准输出) 或 SLF4J
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 使用 JDBC 生成的键(用于 insert 返回自增主键),默认 false
mybatis.configuration.use-generated-keys=true
# 允许在语句中使用列标签,建议保持默认 true
mybatis.configuration.use-column-label=true

7.4 使用 Druid 连接池时的配置(可选)

# 指定数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# Druid 专用配置
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-wait=60000
# 监控统计
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000

7.5 开启日志打印(SQL 语句与参数)

如果希望看到 MyBatis 实际执行的 SQL 和参数,除了上面 log-impl 外,还可以用日志级别控制:

# 日志级别:打印 MyBatis SQL(StdOutImpl 方式)
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 若使用 logback,可通过日志级别打印 SQL(推荐)
logging.level.com.example.mapper=debug

logging.level.包名.mapper=debug 时,MyBatis 会通过 Slf4j 输出详细 SQL,格式如:

==>  Preparing: SELECT id, name, age FROM user WHERE id = ?
==> Parameters: 1(Long)
<==      Total: 1

7.6 完整配置示例

# ========== 数据源 ==========
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# ========== MyBatis ==========
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.model
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.cache-enabled=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# ========== 日志打印 SQL ==========
logging.level.com.example.mapper=debug

唯有极致沉淀,才能造就辉煌。
最后更新于 2026-08-12