【Day48】MyBatis 注解开发:替代 XML 映射文件

前言

在 MyBatis 的传统开发模式中,XML映射文件是核心,但随着项目复杂度提升,大量 XML 文件会增加维护成本。MyBatis 提供的注解开发模式,可直接在 Mapper 接口方法上标注注解来编写 SQL,无需编写 XML 映射文件,极大简化了开发流程。本文将全面讲解 MyBatis 注解开发的核心用法,包括基础 CRUD、动态 SQL、关联查询,对比 XML 模式的差异,让你快速掌握注解开发的精髓。

一、环境准备

1. 核心依赖(pom.xml)

沿用之前的依赖,无需新增:

xml

XML 复制代码
<!-- MyBatis核心依赖 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.10</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
    <scope>runtime</scope>
</dependency>

<!-- JUnit单元测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

<!-- 日志依赖 -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. 数据库准备

沿用 Day47 的mybatis_demo数据库(user、order、order_item 表),无需重新创建。

3. 核心配置文件(mybatis-config.xml)

修改映射器配置,从 XML 扫描改为包扫描(注解开发核心):

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>
    <!-- 全局设置 -->
    <settings>
        <!-- 下划线转驼峰自动映射 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启日志打印 -->
        <setting name="logImpl" value="LOG4J"/>
        <!-- 开启延迟加载(关联查询用) -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <!-- 环境配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 注解开发:包扫描Mapper接口(替代XML映射器) -->
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

二、注解开发核心:基础 CRUD

MyBatis 提供了基础 CRUD 注解,完全替代 XML 中的<select>/<insert>/<update>/<delete>标签:

注解 作用 对应 XML 标签
@Select 查询 SQL <select>
@Insert 新增 SQL <insert>
@Update 修改 SQL <update>
@Delete 删除 SQL <delete>
@Results 结果集映射 <resultMap>
@Result 单个字段映射 <result>/<id>

1. 实体类准备(沿用之前的 User/Order/OrderItem)

java

运行

java 复制代码
package com.example.entity;

import java.util.Date;
import java.util.List;

public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String email;
    private Date createTime;
    private List<Order> orderList; // 一对多关联订单

    // 无参构造、getter/setter、toString(省略)
}

2. 基础 CRUD 注解实现(UserMapper)

java

运行

java 复制代码
package com.example.mapper;

import com.example.entity.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

/**
 * 用户Mapper(纯注解开发,无XML)
 */
public interface UserMapper {

    /**
     * 基础查询:根据ID查询用户
     * @Results 替代XML中的resultMap,解决字段与属性映射
     */
    @Select("SELECT * FROM user WHERE id = #{id}")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "username", property = "username"),
            @Result(column = "password", property = "password"),
            @Result(column = "age", property = "age"),
            @Result(column = "email", property = "email"),
            @Result(column = "create_time", property = "createTime") // 下划线转驼峰
    })
    User findById(Integer id);

    /**
     * 基础查询:查询所有用户
     * 复用@Results:使用@ResultMap指定已定义的结果集映射
     */
    @Select("SELECT * FROM user")
    @ResultMap("com.example.mapper.UserMapper.findById-Result") // 格式:接口全类名.方法名-Result
    List<User> findAll();

    /**
     * 基础新增:添加用户(返回自增ID)
     * @Options(useGeneratedKeys = true):开启自增ID返回
     */
    @Insert("INSERT INTO user (username, password, age, email) VALUES (#{username}, #{password}, #{age}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    int addUser(User user);

    /**
     * 基础修改:更新用户
     */
    @Update("UPDATE user SET username = #{username}, password = #{password}, age = #{age}, email = #{email} WHERE id = #{id}")
    int updateUser(User user);

    /**
     * 基础删除:删除用户
     */
    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteUser(Integer id);
}

3. 基础 CRUD 测试

java

运行

java 复制代码
package com.example.test;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

public class BasicAnnotationTest {

    @Test
    public void testFindById() {
        try (SqlSession session = MyBatisUtil.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.findById(1);
            System.out.println("根据ID查询:" + user);
        }
    }

    @Test
    public void testFindAll() {
        try (SqlSession session = MyBatisUtil.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<User> userList = mapper.findAll();
            System.out.println("查询所有用户:");
            userList.forEach(System.out::println);
        }
    }

    @Test
    public void testAddUser() {
        try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = new User();
            user.setUsername("zhaoliu");
            user.setPassword("666666");
            user.setAge(32);
            user.setEmail("zhaoliu@test.com");
            
            int rows = mapper.addUser(user);
            System.out.println("新增受影响行数:" + rows + ",自增ID:" + user.getId());
        }
    }
}

三、注解开发进阶:动态 SQL

XML 中的动态 SQL 标签(<if>/<where>/<foreach>),在注解中通过@ScriptingLanguage + OGNL 表达式实现,核心是使用<script>标签包裹动态 SQL 片段。

1. 动态 SQL 注解实现(UserMapper 扩展)

java

运行

java 复制代码
/**
 * 动态SQL:条件查询用户(if + where)
 */
@Select({
        "<script>",
        "SELECT * FROM user",
        "<where>",
        "   <if test='username != null and username != \"\"'>",
        "       AND username LIKE CONCAT('%', #{username}, '%')",
        "   </if>",
        "   <if test='age != null'>",
        "       AND age > #{age}",
        "   </if>",
        "</where>",
        "</script>"
})
@ResultMap("com.example.mapper.UserMapper.findById-Result")
List<User> findByCondition(@Param("username") String username, @Param("age") Integer age);

/**
 * 动态SQL:批量删除(foreach)
 */
@Delete({
        "<script>",
        "DELETE FROM user WHERE id IN",
        "<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
        "   #{id}",
        "</foreach>",
        "</script>"
})
int batchDelete(@Param("ids") List<Integer> ids);

/**
 * 动态SQL:动态更新(if + set)
 */
@Update({
        "<script>",
        "UPDATE user",
        "<set>",
        "   <if test='username != null and username != \"\"'>username = #{username},</if>",
        "   <if test='password != null and password != \"\"'>password = #{password},</if>",
        "   <if test='age != null'>age = #{age},</if>",
        "   <if test='email != null and email != \"\"'>email = #{email},</if>",
        "</set>",
        "WHERE id = #{id}",
        "</script>"
})
int updateUserDynamic(User user);

2. 动态 SQL 测试

java

运行

java 复制代码
@Test
public void testFindByCondition() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        // 条件:用户名包含"admin",年龄大于20
        List<User> userList = mapper.findByCondition("admin", 20);
        System.out.println("动态条件查询结果:");
        userList.forEach(System.out::println);
    }
}

@Test
public void testBatchDelete() {
    try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<Integer> ids = List.of(5, 6); // JDK9+可用List.of,低版本用new ArrayList<>()
        int rows = mapper.batchDelete(ids);
        System.out.println("批量删除受影响行数:" + rows);
    }
}

四、注解开发高级:关联查询(一对一 / 一对多)

注解开发中,关联查询通过@One(一对一)和@Many(一对多)注解实现,替代 XML 中的<association><collection>

1. 一对一关联查询(订单→用户)

OrderMapper 注解实现

java

运行

java 复制代码
package com.example.mapper;

import com.example.entity.Order;
import com.example.entity.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface OrderMapper {

    /**
     * 一对一关联查询:订单→用户(嵌套结果)
     */
    @Select("SELECT o.*, u.username, u.age, u.email FROM `order` o LEFT JOIN user u ON o.user_id = u.id WHERE o.id = #{id}")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "order_no", property = "orderNo"),
            @Result(column = "total_amount", property = "totalAmount"),
            @Result(column = "create_time", property = "createTime"),
            @Result(column = "user_id", property = "userId"),
            // 一对一关联:封装User对象
            @Result(property = "user", javaType = User.class,
                    columns = {
                            @ColumnResult(column = "user_id", property = "id"),
                            @ColumnResult(column = "username", property = "username"),
                            @ColumnResult(column = "age", property = "age"),
                            @ColumnResult(column = "email", property = "email")
                    })
    })
    Order findOrderWithUser(Integer id);

    /**
     * 一对一关联查询:订单→用户(分步查询+延迟加载)
     */
    @Select("SELECT * FROM `order` WHERE id = #{id}")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "order_no", property = "orderNo"),
            @Result(column = "total_amount", property = "totalAmount"),
            @Result(column = "user_id", property = "userId"),
            // @One:一对一分步查询,select指定查询用户的方法
            @Result(property = "user", column = "user_id",
                    one = @One(select = "com.example.mapper.UserMapper.findById", fetchType = FetchType.LAZY))
    })
    Order findOrderWithUserLazy(Integer id);
}

2. 一对多关联查询(用户→订单)

UserMapper 扩展一对多查询

java

运行

java 复制代码
/**
 * 一对多关联查询:用户→订单(分步查询+延迟加载)
 */
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
        @Result(id = true, column = "id", property = "id"),
        @Result(column = "username", property = "username"),
        @Result(column = "password", property = "password"),
        @Result(column = "age", property = "age"),
        @Result(column = "email", property = "email"),
        @Result(column = "create_time", property = "createTime"),
        // @Many:一对多分步查询,select指定查询订单的方法
        @Result(property = "orderList", column = "id",
                many = @Many(select = "com.example.mapper.OrderMapper.findOrdersByUserId", fetchType = FetchType.LAZY))
})
User findUserWithOrders(Integer id);

OrderMapper 新增查询订单方法

java

运行

java 复制代码
/**
 * 根据用户ID查询订单(供一对多关联查询使用)
 */
@Select("SELECT * FROM `order` WHERE user_id = #{userId}")
@Results({
        @Result(id = true, column = "id", property = "id"),
        @Result(column = "order_no", property = "orderNo"),
        @Result(column = "total_amount", property = "totalAmount"),
        @Result(column = "user_id", property = "userId")
})
List<Order> findOrdersByUserId(Integer userId);

3. 关联查询测试

java

运行

java 复制代码
@Test
public void testFindOrderWithUserLazy() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        OrderMapper mapper = session.getMapper(OrderMapper.class);
        // 第一步:仅查询订单,未查询用户(延迟加载)
        Order order = mapper.findOrderWithUserLazy(1);
        System.out.println("订单编号:" + order.getOrderNo());
        
        // 第二步:调用user属性时,触发延迟加载,执行查询用户的SQL
        System.out.println("关联用户:" + order.getUser().getUsername());
    }
}

@Test
public void testFindUserWithOrders() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.findUserWithOrders(1);
        System.out.println("用户信息:" + user.getUsername());
        System.out.println("关联订单:");
        user.getOrderList().forEach(order -> System.out.println(order.getOrderNo()));
    }
}

五、注解开发 vs XML 开发:对比与选型

维度 注解开发 XML 开发
开发效率 高(无需编写 XML,直接写在接口上) 低(需维护大量 XML 文件)
维护成本 简单 SQL 易维护,复杂 SQL 难维护 复杂 SQL 易维护(结构清晰)
功能完整性 支持大部分功能,动态 SQL 写法繁琐 支持所有功能,动态 SQL 语法更友好
可读性 简单 SQL 可读性高,复杂 SQL 可读性差 所有 SQL 结构清晰,可读性高
适用场景 简单 CRUD、快速开发、小型项目 复杂 SQL、动态 SQL、大型项目

选型建议

  1. 优先用注解:简单 CRUD、小型项目、快速原型开发;
  2. 优先用 XML:复杂动态 SQL、关联查询多、大型项目(便于 SQL 统一管理和优化);
  3. 混合使用:核心 CRUD 用注解,复杂 SQL(如多表联查、动态 SQL)用 XML(MyBatis 支持注解 + XML 混合开发)。

六、注解开发常见问题与解决方案

1. 结果集映射复用问题

  • 问题:多个方法需要复用相同的@Results映射;
  • 解决方案:
    1. 使用@ResultMap指定已定义的结果集(如@ResultMap("com.example.mapper.UserMapper.findById-Result"));
    2. 将通用结果集映射抽离为常量,通过反射复用(进阶)。

2. 动态 SQL 语法错误

  • 问题:注解中动态 SQL 的<script>标签内语法报错;
  • 解决方案:
    1. 确保 SQL 片段用{}包裹,字符串用""转义;
    2. 避免换行符导致的语法错误,可将动态 SQL 写在一行(或格式化工具辅助)。

3. 关联查询延迟加载失效

  • 问题:分步查询未触发延迟加载;
  • 解决方案:
    1. 核心配置文件开启lazyLoadingEnabled=trueaggressiveLazyLoading=false
    2. @One/@Many中指定fetchType = FetchType.LAZY

总结

  1. 注解开发核心 :通过@Select/@Insert/@Update/@Delete实现基础 CRUD,@Results/@Result实现结果集映射,替代 XML 核心标签;
  2. 动态 SQL 注解 :通过<script>标签包裹 XML 风格的动态 SQL 片段,实现条件查询、批量操作;
  3. 关联查询注解@One实现一对一关联,@Many实现一对多关联,配合延迟加载提升性能;
  4. 选型原则:简单场景用注解,复杂场景用 XML,也可混合使用;
  5. 核心优势:注解开发简化了开发流程,减少了文件数量,适合快速开发和小型项目;XML 开发更适合复杂 SQL 场景,便于维护和优化。

掌握 MyBatis 注解开发,你可以根据项目需求灵活选择开发模式,既享受注解的便捷性,也能通过 XML 处理复杂场景,真正做到 "因地制宜"。

相关推荐
Gary董2 小时前
java死锁
java·开发语言
LiLiYuan.2 小时前
在资源管理器打开IDEA未进行版本管理的文件的方法
java·ide·intellij-idea
XXOOXRT2 小时前
基于SpringBoot-验证码
java·spring boot·后端
lvbinemail2 小时前
配置jenkins.service
java·运维·jenkins·systemctl
ss2732 小时前
若依分离版后端集成 Camunda 7 工作流引擎
java·若依
明天…ling2 小时前
sql注入(1-10关)
java·数据库·sql
Ashley_Amanda2 小时前
SAP调用Web Service全流程详解
java·前端·数据库
笨手笨脚の2 小时前
Linux JDK NIO 源码分析
java·linux·nio
顾北122 小时前
RAG 入门到实战:Spring AI 搭建旅游问答知识库(本地 + 阿里云百炼双方案)
java·人工智能·阿里云