Java-09 深入浅出 MyBatis 注解开发详解:从 CRUD 到复杂关系映射

TL;DR

  • 场景:Java 持久层开发中,需要在 MyBatis 框架下直接通过接口注解定义 SQL,避免维护额外的 XML 映射文件
  • 结论 :注解方式适合简单 CRUD 场景,复杂关联映射仍需配合 @Results/@One/@Many 使用,动态 SQL 和大段查询推荐 XML
  • 产出:完整的 MyBatis 注解速查表,含 CRUD 注解、结果映射注解、关联查询注解的使用示例与代码片段

版本矩阵

功能 版本 状态 说明
@Select / @Insert / @Update / @Delete 3.0+ ✅ 已验证 基础 CRUD 注解,替代对应 XML 标签
@Results / @Result 3.0+ ✅ 已验证 结果集映射,替代 <resultMap>
@One (一对一关联) 3.0+ ✅ 已验证 替代 <association>,支持子查询
@Many (一对多关联) 3.0+ ✅ 已验证 替代 <collection>,支持子查询
@Param 参数命名 3.0+ ✅ 已验证 解决多参数绑定歧义
@Options 选项配置 3.0+ ✅ 已验证 主键生成、缓存等额外选项
@ConstructorArgs / @Arg 3.0+ ✅ 已验证 构造函数映射
MyBatis 最新稳定版 3.5.13 ✅ 已验证 2023 年 3 月发布
mybatis-spring-boot-starter 3.0.3 ✅ 已验证 Spring Boot 3.x 配套版本
MyBatis-Plus 3.5.5 ⚠️ 参考 MyBatis-Plus 3.5 系列为当前主流

MyBatis 简介

MyBatis 是一款优秀的持久层框架,它支持通过注解的方式进行开发,而无需使用传统的 XML 配置文件。这种方式更加简洁、直观,适合于简单的应用场景或轻量级开发项目。MyBatis 提供了一系列注解,用于代替 XML 配置文件中定义的 SQL 语句和映射规则。这些注解直接写在接口或方法上,使得开发更加面向对象,也减少了配置文件的维护成本。

  • @Insert 新增
  • @Update 更新
  • @Delete 删除
  • @Select 查询
  • @Result 结果集封装
  • @Results@Result 一起使用,封装多个结果集
  • @One 实现一对一的结果封装
  • @Many 实现一对多的结果封装

@Select

用途 :用于执行查询操作。 位置 :放在 Mapper 接口的方法上。 支持功能:可以直接写简单的查询语句,支持动态参数。

代码示例

java 复制代码
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(int id);

@Select("SELECT * FROM user WHERE username LIKE CONCAT('%', #{name}, '%')")
List<User> searchByName(String name);

@Insert

用途 :用于执行插入操作。 位置 :放在 Mapper 接口的方法上。 支持功能:支持插入单条记录或批量插入,支持自动生成主键。

代码示例

java 复制代码
@Insert("INSERT INTO user(username, password, birthday) VALUES(#{username}, #{password}, #{birthday})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);

@Insert("<script>" +
        "INSERT INTO user(username, password) VALUES " +
        "<foreach collection='list' item='item' separator=','>" +
        "(#{item.username}, #{item.password})" +
        "</foreach>" +
        "</script>")
int batchInsert(@Param("list") List<User> userList);

@Update

用途 :用于执行更新操作。 位置 :放在 Mapper 接口的方法上。 支持功能:支持根据条件更新记录。

代码示例

java 复制代码
@Update("UPDATE user SET username = #{username}, password = #{password} WHERE id = #{id}")
int updateUser(User user);

@Update("UPDATE user SET password = #{newPassword} WHERE id = #{id} AND password = #{oldPassword}")
int updatePassword(@Param("id") int id, @Param("oldPassword") String oldPassword, @Param("newPassword") String newPassword);

@Delete

用途 :用于执行删除操作。 位置 :放在 Mapper 接口的方法上。 支持功能:支持根据条件删除记录。

代码示例

java 复制代码
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(int id);

@Delete("DELETE FROM user WHERE username = #{username}")
int deleteByUsername(String username);

@Results 和 @Result

用途 :用于结果集的手动映射,将查询的字段与对象属性一一对应。 位置 :放在 Mapper 接口的方法上。 支持功能:用于字段名和对象属性名不一致的情况。

代码示例

java 复制代码
@Select("SELECT id, user_name AS userName, create_time AS createTime FROM user")
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "userName", column = "user_name"),
    @Result(property = "createTime", column = "create_time")
})
List<User> findAllUsers();

@Param

用途 :用于给 SQL 中的参数命名,绑定方法的参数到 SQL 语句中的占位符。 位置 :放在 Mapper 方法的参数上。 支持功能:解决方法参数无法直接被引用或多参数的绑定问题。

@Options

用途 :用于设置方法的额外选项,比如主键生成、查询的缓存等。 位置 :放在 @Insert@Update 等注解上。

@ResultMap

用途 :引用 XML 或注解中定义的结果映射。 位置 :放在 Mapper 接口的方法上。 支持功能:简化复杂的结果映射。

@ConstructorArgs 和 @Arg

用途 :用于在构造函数映射场景中,将查询结果映射到构造函数参数。 位置:放在 Mapper 接口的方法上。

注解优点

  • 简洁:直接在代码中定义 SQL,无需额外的 XML 文件。
  • 强类型支持:与 Java 代码紧密结合,便于重构和代码检查。
  • 便于维护:SQL 紧贴业务逻辑,便于定位问题。

注解缺点

  • 不适合复杂 SQL:对于动态 SQL 和大段查询,注解方式不够灵活。
  • 代码冗长:复杂查询可能导致注解内容过多,影响可读性。
  • SQL 可重用性差:注解中的 SQL 不能像 XML 一样被复用。

常见使用

  • 简单 SQL 使用注解,复杂 SQL 使用 XML 配置文件。
  • 使用 @Param 命名参数,避免歧义。
  • 对大段复杂 SQL 优先采用 XML 的 <sql><include> 标签,提高复用性。
  • 结合 SpringBoot 使用 @MapperScan 注解统一管理 Mapper。

注解映射

实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用 @Results 注解、@Result 注解、@One 注解、@Many 注解结合起来完成复杂功能的开发。

一对一

查询模型

用户表和订单表的关系,一个用户有多个订单,一个订单只从属于一个用户,一对一查询的需求是:查询一个订单,与此同时查询出该订单所属的用户。

编写代码

OrderMapper

新增了一个方法 findAllWithAnnotation,通过注解的方式进行开发:

java 复制代码
@Select("select * from wzk_orders")
@Results({
        @Result(id = true, property = "id", column = "id"),
        @Result(property = "ordertime", column = "ordertime"),
        @Result(property = "total", column = "total"),
        @Result(
                property = "user", column = "uid",
                javaType = WzkUser.class,
                one = @One(select = "icu.wzk.mapper.UserMapper.findByIdWithAnnotation")
        ),
})
List<WzkOrder> findAllWithAnnotation();

截图如下所示:

UserMapper

在该类中再加入一个新的方法,也用注解的方式,因为 OrderMapper 中要用到:

java 复制代码
@Select("select * from wzk_user where id = #{id}")
WzkUser findByIdWithAnnotation(int id);

对应的截图如下所示:

调用代码

java 复制代码
package icu.wzk;

import icu.wzk.mapper.OrderMapper;
import icu.wzk.mapper.UserMapper;
import icu.wzk.model.WzkOrder;
import icu.wzk.model.WzkUser;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class WzkIcu11 {
    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
        List<WzkOrder> dataList = orderMapper.findAllWithAnnotation();
        dataList.forEach(System.out::println);
        sqlSession.close();
    }
}

对应的代码截图如下所示:

测试结果

执行代码,对应的控制台输出结果如下所示:

shell 复制代码
WzkOrder(id=1, ordertime=Mon Nov 11 00:00:00 CST 2024, total=100.0, user=WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=null, roleList=null))
WzkOrder(id=2, ordertime=Mon Nov 11 00:00:00 CST 2024, total=200.0, user=WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=null, roleList=null))
WzkOrder(id=3, ordertime=Sun Nov 10 00:00:00 CST 2024, total=150.0, user=WzkUser(id=2, username=wzk2, password=icu2, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=null, roleList=null))
24/11/13 09:02:37 DEBUG jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.JdbcConnectionImpl@75329a49]

对应的截图如下所示:


错误速查卡

症状 根因 定位 修复
BindingException: Invalid bound statement 注解方法未被发现 检查 @MapperScan 是否扫描到对应包;确认接口全限定名与 one=@One(select="...") 中路径一致 确保 Mapper 接口在 @MapperScan 指定的包路径下,或在接口上加 @Mapper 注解
SQL 中参数无法绑定(#{} 不生效) 多参数未使用 @Param 命名 确认方法参数是否直接作为 SQL 占位符使用 多参数方法加 @Param("name") 注解,SQL 中使用 #{name} 引用
字段映射结果为 null 数据库列名与对象属性名不一致且未用 @Results 映射 确认 SQL 查询的列别名与 POJO 属性名是否匹配 使用 @Results + @Result 显式指定 columnproperty 的映射
一对一关联查出的 usernull 子查询方法路径写错或未标记 @Param 检查 one=@One(select="...") 中的全限定方法名是否正确,参数名是否匹配 确认子查询方法存在且可访问,被引用方法的参数需与 column 传参对应
批量插入 foreach 语法报错 <script> 标签包裹的动态 SQL 格式错误 检查 <foreach> 标签是否正确闭合,separator 是否正确 确保 <script> 包裹整个语句,collection='list' 对应 @Param("list")
useGeneratedKeys 主键返回为 0 MySQL 驱动版本过低或 keyProperty 路径错误 确认 keyProperty="id" 是否指向对象正确的属性路径 使用 MySQL Connector/J 8.0+,确认 keyProperty 为对象属性名而非列名
延迟加载不生效 未配置 <settings> 中的延迟加载开关 检查 mybatis-config.xml 中是否设置 lazyLoadingEnabled=true 在 XML 配置或 @Select 注解方法上确认结果映射类型

作者:武子康的个人博客

相关推荐
Yolanda9410 小时前
【编程学习】复盘经典 VB OOP 示例:推翻旧认知,重学面向对象
java·面向对象
Y敲键盘的地方10 小时前
第9章 工具调用循环——Agent的行动闭环
java·服务器·前端
明月_清风10 小时前
实战选型决策树——一张图搞定"我这个场景该用什么序列化方案"
后端
专注写bug10 小时前
Java线程池——ThreadLocal上下文污染问题
java
Amctwd10 小时前
【后端】多个后端系统,如何共用一套登录状态?单点登录详解
java
神奇小汤圆10 小时前
Java 泛型解析太痛苦?你可能需要一枚「蛋」
后端
用户2986985301410 小时前
Java 进阶:在 Word 文档中动态增删页面
java·后端
likerhood10 小时前
Java 集合框架入门:List、Set、Queue 与 Map
java·开发语言·list
Java 码思客10 小时前
【Spring AI实战】第2章 大模型基础调用:同步/异步/流式输出
java·人工智能·spring·ai