当前位置: 首页 > article >正文

Mybatis——Mybatis开发经验总结

摘要

本文主要介绍了MyBatis框架的设计与通用性,阐述了其作为Java持久化框架的亮点,包括精良的架构设计、丰富的扩展点以及易用性和可靠性。同时,对比了常见持久层框架,分析了MyBatis在关系型数据库交互中的优势。此外,还提供了订单系统持久层示例分析,涵盖从架包依赖到单元测试类的创建等步骤,并总结了MyBatis编码经验,给出了相关强制和推荐规范。

1. Mybatis框架的设计与通用性

MyBatis 是 Java 生态中非常著名的一款 ORM 框架。这是一款很值得你学习和研究的 Java 持久化框架。原因主要有两个:

  • MyBatis 自身有很多亮点值得你深挖;
  • MyBatis 在一线互联网大厂中应用广泛,已经成为你进入大厂的必备技能。

MyBatis 所具备的亮点可总结为如下三个方面:

  1. MyBatis 本身就是一款设计非常精良、架构设计非常清晰的持久层框架,并且 MyBatis 中还使用到了很多经典的设计模式,例如,工厂方法模式、适配器模式、装饰器模式、代理模式等。 在阅读 MyBatis 代码的时候,你也许会惊奇地发现:原来大师设计出来的代码真的是一种艺术。所以,从这个层面来讲,深入研究 MyBatis 原理,甚至阅读它的源码,不仅可以帮助你快速解决工作中遇到的 MyBatis 相关问题,还可以提高你的设计思维。
  2. MyBatis 提供了很多扩展点,例如,MyBatis 的插件机制、对第三方日志框架和第三方数据源的兼容等。 正由于这种可扩展的能力,让 MyBatis 的生命力非常旺盛,这也是很多 Java 开发人员将 MyBatis 作为自己首选 Java 持久化框架的原因之一,反过来促进了 MyBatis 用户的不断壮大。
  3. 开发人员使用 MyBatis 上手会非常快,具有很强的易用性和可靠性。这也是 MyBatis 流行的一个很重要的原因。当你具备了 MySQL 和 JDBC 的基础知识之后,学习 MyBatis 的难度远远小于 Hibernate 等持久化框架。

作为一名 Java 工程师,深入掌握一款持久化框架已经是一项必备技能,并且成为个人职场竞争力的关键项。多个招聘软件显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用过某种持久化框架,其中以 MyBatis 居多,“熟悉 MyBatis” 或是“精通 MyBatis” 等字眼更是频繁出现在岗位职责中。如果你想要进入一线大厂,能够熟练使用 MyBatis 开发已经是一项非常基本的技能,同时大厂也更希望自己的开发人员深入了解 MyBatis 框架的原理和核心实现。

2. 常见持久层框架对比

在绝大多数在线应用场景中,数据是存储在关系型数据库中的,当然,有特殊要求的场景中,我们也会将其他持久化存储(如 ElasticSearch、HBase、MongoDB 等)作为辅助存储。但不可否认的是,关系型数据库凭借几十年的发展、生态积累、众多成功的案例,依然是互联网企业的核心存储。

作为一个 Java 开发者,几乎天天与关系型数据库打交道,在生产环境中常用的关系型数据库产品有 SQL Server、MySQL、Oracle 等。在使用这些数据库产品的时候,基本上是如下思路:

  • 在写 Java 代码的过程中,使用的是面向对象的思维去实现业务逻辑;
  • 在设计数据库表的时候,考虑的是第一范式、第二范式和第三范式;
  • 在操作数据库记录的时候,使用 SQL 语句以及集合思维去考虑表的连接、条件语句、子查询等的编写。

这个时候,就需要一座桥梁将Java 类(或是其他数据结构)与关系型数据库中的表,以及 Java 对象与表中的数据映射起来,实现 Java 程序与数据库之间的交互。JDBC(Java DataBase Connectivity)是 Java 程序与关系型数据库交互的统一 API。实际上,JDBC 由两部分 API 构成:第一部分是面向 Java 开发者的 Java API,它是一个统一的、标准的 Java API,独立于各个数据库产品的接口规范;第二部分是面向数据库驱动程序开发者的 API,它是由各个数据库厂家提供的数据库驱动,是第一部分接口规范的底层实现,用于连接具体的数据库产品。在实际开发 Java 程序时,我们可以通过JDBC连接到数据库,并完成各种各样的数据库操作,例如 CRUD 数据、执行 DDL 语句。

这里以 JDBC 编程中执行一条 Select 查询语句作为例子,说明 JDBC 操作的核心步骤,具体如下:

  1. 注册数据库驱动类,指定数据库地址,其中包括 DB 的用户名、密码及其他连接信息;
  2. 调用 DriverManager.getConnection() 方法创建 Connection 连接到数据库;
  3. 调用 Connection 的 createStatement() 或 prepareStatement() 方法,创建 Statement 对象,此时会指定 SQL(或是 SQL 语句模板 + SQL 参数);
  4. 通过 Statement 对象执行 SQL 语句,得到 ResultSet 对象,也就是查询结果集;
  5. 遍历 ResultSet,从结果集中读取数据,并将每一行数据库记录转换成一个 JavaBean 对象;
  6. 关闭 ResultSet 结果集、Statement 对象及数据库 Connection,从而释放这些对象占用的底层资源。

无论是执行查询操作,还是执行其他 DML 操作,1、2、3、4、6 这些步骤都会重复出现。为了简化重复逻辑,提高代码的可维护性,可以将上述重复逻辑封装到一个类似 DBUtils 的工具类中,在使用时只需要调用 DBUtils 工具类中的方法即可。当然,我们也可以使用“反射+配置”的方式,将步骤 5 中关系模型到对象模型的转换进行封装,但是这种封装要做到通用化且兼顾灵活性,就需要一定的编程功底。

为了处理上述代码重复的问题以及后续的维护问题,我们在实践中会进行一系列评估,选择一款适合项目需求、符合人员能力的 ORM(Object Relational Mapping,对象-关系映射)框架来封装 1~6 步的重复性代码,实现对象模型、关系模型之间的转换。这正是ORM 框架的核心功能:根据配置(配置文件或是注解)实现对象模型、关系模型两者之间无感知的映射(如下图)。

对象模型与关系模型的映射

在生产环境中,数据库一般都是比较稀缺的,数据库连接也是整个服务中比较珍贵的资源之一。建立数据库连接涉及鉴权、握手等一系列网络操作,是一个比较耗时的操作,所以我们不能像上述 JDBC 基本操作流程那样直接释放掉数据库连接,否则持久层很容易成为整个系统的性能瓶颈。

Java 程序员一般会使用数据库连接池的方式进行优化,此时就需要引入第三方的连接池实现,当然,也可以自研一个连接池,但是要处理连接活跃数、控制连接的状态等一系列操作还是有一定难度的。另外,有一些查询返回的数据是需要本地缓存的,这样可以提高整个程序的查询性能,这就需要缓存的支持。

如果没有 ORM 框架的存在,这就需要我们 Java 开发者熟悉相关连接池、缓存等组件的 API 并手动编写一些“黏合”代码来完成集成,而且这些代码重复度很高,这显然不是我们希望看到的结果。

很多 ORM 框架都支持集成第三方缓存、第三方数据源等常用组件,并对外提供统一的配置接入方式,这样我们只需要使用简单的配置即可完成第三方组件的集成。当我们需要更换某个第三方组件的时候,只需要引入相关依赖并更新配置即可,这就大大提高了开发效率以及整个系统的可维护性

2.1.1. Hibernate 全自动持久化框架

Hibernate 是 Java 生态中著名的 ORM 框架之一。Hibernate 现在也在扩展自己的生态,开始支持多种异构数据的持久化,不仅仅提供 ORM 框架,还提供了 Hibernate Search 来支持全文搜索,提供 validation 来进行数据校验,提供 Hibernate OGM 来支持 NoSQL 解决方案。

在使用 Hibernate 的时候,Java 开发可以使用映射文件或是注解定义 Java 语言中的类与数据库中的表之间的各种映射关系,这里使用到的映射文件后缀为“.hbm.xml”。hbm.xml 映射文件将一张数据库表与一个 Java 类进行关联之后,该数据库表中的每一行记录都可以被转换成对应的一个 Java 对象。正是由于 Hibernate 映射的存在,Java 开发只需要使用面向对象思维就可以完成数据库表的设计。

在 Java 这种纯面向对象的语言中,两个 Java 对象之间可能存在一对一、一对多或多对多等复杂关联关系。Hibernate中的映射文件也必须要能够表达这种复杂关联关系才能够满足我们的需求,同时,还要能够将这种关联关系与数据库中的关联表、外键等一系列关系模型中的概念进行映射,这也就是 ORM 框架中常提到的“关联映射”。

下面我们就来结合示例介绍“一对多”关联关系。例如,一个顾客(Customer)可以创建多个订单(Order),而一个订单(Order)只属于一个顾客(Customer),两者之间存在一对多的关系。在 Java 程序中,可以在 Customer 类中添加一个 List 类型的字段来维护这种一对多的关系;在数据库中,可以在订单表(t_order)中添加一个 customer_id 列作为外键,指向顾客表(t_customer)的主键 id,从而维护这种一对多的关系,如下图所示:

关系模型中的一对多和对象模型中的一对多

在 Hibernate 中,可以通过如下 Customer.hbm.xml 配置文件将这两种关系进行映射:

<hibernate-mapping>
    <!-- 这里指定了Customer类与t_customer表之间的映射 -->
    <class name="com.mybatis.test.Customer" table="t_customer">
        <!-- Customer类中的id属性与t_customer表中主键id之间的映射 -->
        <id name="id" column="id"/>
        <!-- Customer类中的name属性与t_customer表中name字段之间的映射 -->
        <property name="name" column="name"/>
        <!-- Customer指定了Order与Customer 一对多的映射关系 -->
        <set name="orders" cascade="save,update,delete">
            <key column="customer_id"/>
            <one-to-many class="com.mybatis.test.Order"/>
        </set>
    </class>
</hibernate-mapping>

如果是双向关联,则在 Java 代码中,可以直接在 Order 类中添加 Customer 类型的字段指向关联的 Customer 对象,并在相应的 Order.hbm.xml 配置文件中进行如下配置:

<hibernate-mapping>
    <!-- 这里指定了Order类与t_order表之间的映射 -->
    <class name="com.mybatis.test.Order" table="t_order">
        <!-- Order类中的id属性与t_order表中主键id之间的映射 -->
        <id name="id" column="id"/>
        <!-- Order类中的address属性与t_order表中address列之间的映射 -->
        <property name="address" column="address"/>
        <!-- Order类中的tele属性与t_order表中tele列之间的映射 -->
        <property name="tele" column="tele"/>
        <!-- Order类中customer属性与t_order表中customer_id之间的映射,同时也指定Order与Customer之间的多对一的关系 -->
        <many-to-one name="customer" column="customer_id"></many-to-one>
    </class>
</hibernate-mapping>

一对一、多对多等关联映射在 Hibernate 映射文件中,都定义了相应的 XML 标签,原理与“一对多”基本一致,只是使用方式和场景略有不同。

除了能够完成面向对象模型与数据库中关系模型的映射,Hibernate 还可以帮助我们屏蔽不同数据库产品中 SQL 语句的差异。

我们知道,虽然目前有 SQL 标准,但是不同的关系型数据库产品对 SQL 标准的支持有细微不同,这就会出现一些非常尴尬的情况,例如,一条 SQL 语句在 MySQL 上可以正常执行,而在 Oracle 数据库上执行会报错。Hibernate封装了数据库层面的全部操作,Java 程序员不再需要直接编写 SQL 语句,只需要使用 Hibernate 提供的 API 即可完成数据库操作。

例如,Hibernate 为用户提供的 Criteria 是一套灵活的、可扩展的数据操纵 API,最重要的是 Criteria 是一套面向对象的 API,使用它操作数据库的时候,Java 开发者只需要关注 Criteria 这套 API 以及返回的 Java 对象,不需要考虑数据库底层如何实现、SQL 语句如何编写,等等。

下面是 Criteria API 的一个简单示例:

// 创建Criteria对象,用来查询Customer对象
Criteria criteria = session.createCriteria(Customer.class, "u");
//查询出id大于0,且名字中以yang开头的顾客数据
List<Customer> list = criteria.add(Restrictions.like("name","yang%"))
                            .add(Restrictions.gt("id", 0))
                            .list();

除了 Criteria API 之外,Hibernate 还提供了一套面向对象的查询语言—— HQL(Hibernate Query Language)。从语句的结构上来看,HQL 语句与 SQL 语句十分类似,但这二者也是有区别的:HQL 是面向对象的查询语言,而 SQL 是面向关系型的查询语言。在实现复杂数据库操作的时候,我们可以使用 HQL 这种面向对象的查询语句来实现,Hibernate 的 HQL 引擎会根据底层使用的数据库产品,将 HQL 语句转换成合法的 SQL 语句。

Hibernate 通过其简洁的 API 以及统一的 HQL 语句,帮助上层程序屏蔽掉底层数据库的差异,增强了程序的可移植性。另外,Hibernate 还具有如下的一些其他优点:

  • Hibernate API 本身没有侵入性,也就是说,业务逻辑感知不到 Hibernate 的存在,也不需要继承任何 Hibernate 包中的接口;
  • Hibernate 默认提供一级缓存、二级缓存(一级缓存默认开启,二级缓存需要配置开启),这两级缓存可以降低数据库的查询压力,提高服务的性能;
  • Hibernate 提供了延迟加载的功能,可以避免无效查询;
  • Hibernate 还提供了由对象模型自动生成数据库表的逆向操作。

但需要注意的是,Hibernate并不是无所不能我们无法在面向对象模型中找到数据库中所有概念的映射,例如,索引、函数、存储过程等。在享受 Hibernate 带来便捷的同时,我们还需要忍受它的一些缺点。例如,索引对提升数据库查询性能有很大帮助,我们建立索引并适当优化 SQL 语句,就会让数据库使用合适的索引提高整个查询的速度。但是,我们很难修改 Hibernate 生成的 SQL 语句。为什么这么说呢?因为在一些场景中,数据库设计非常复杂,表与表之间的关系错综复杂,Hibernate 引擎生成的 SQL 语句会非常难以理解,要让生成的 SQL 语句使用正确的索引更是难上加难,这就很容易生成慢查询 SQL

另外,在一些大数据量、高并发、低延迟的场景中,Hibernate 在性能方面带来的损失就会逐渐显现出来。当然,从其他角度来看 Hibernate,还会有一些其他的问题,这里就不再展开介绍,你若感兴趣的话可以自行去查阅一些资料进行深入了解。

2.1.2. Spring Data JPA持久化框架

JPA 是在 JDK 5.0 后提出的 Java 持久化规范(JSR 338)。JPA 规范本身是为了整合市面上已有的 ORM 框架,结束 Hibernate、EclipseLink、JDO 等 ORM 框架各自为战的割裂局面,简化 Java 持久层开发。JPA 规范从现有的 ORM 框架中借鉴了很多优点,例如,Gavin King 作为 Hibernate 创始人,同时也参与了 JPA 规范的编写,所以在 JPA 规范中可以看到很多与 Hibernate 类似的概念和设计。既然 JPA 是一个持久化规范,没有提供具体持久化实现,那谁来提供实现呢?答案是市面上的 ORM 框架,例如,Hibernate、EclipseLink 等都提供了符合 JPA 规范的具体实现,如下图所示:

JPA 有三个核心部分:ORM 映射元数据、操作实体对象 API 和面向对象的查询语言(JPQL)。这与 Hibernate 的核心功能基本类似。Java 开发者应该都知道“Spring 全家桶”的强大,Spring 目前已经成为事实上的标准了,很少有企业会完全离开 Spring 来开发 Java 程序。现在的 Spring 已经不仅仅是最早的 IoC 容器了,而是整个 Spring 生态,例如,Spring Cloud、Spring Boot、Spring Security 等,其中就包含了 Spring Data。Spring Data 是 Spring 在持久化方面做的一系列扩展和整合,下图就展示了 Spring Data 中的子项目:

Spring Data 中的每个子项目都对应一个持久化存储,通过不断的整合接入各种持久化存储的能力,Spring 的生态又向前迈进了一大步,其中最常被大家用到的应该就是 Spring Data JPA。Spring Data JPA 是符合 JPA 规范的一个 Repository 层的实现,其所在的位置如下图所示:

虽然市面上的绝大多数 ORM 框架都实现了 JPA 规范,但是它们在 JPA 基础上也有各自的发展和修改,这样导致我们在使用 JPA 的时候,依旧无法无缝切换底层的 ORM 框架实现。而使用 Spring Data JPA 时,由于Spring Data JPA 帮助我们抹平了各个 ORM 框架的差异,从而可以让我们的上层业务无缝地切换 ORM 实现框架

2.1.3. MyBatis半自动持久化框架

Apache 基金会中的 iBatis 项目是 MyBatis 的前身。iBatis 项目由于各种原因,在 Apache 基金会并没有得到很好的发展,最终于 2010 年脱离 Apache,并更名为 MyBatis。三年后,也就是 2013 年,MyBatis 将源代码迁移到了 GitHub。

MyBatis 中一个重要的功能就是可以帮助 Java 开发封装重复性的 JDBC 代码,这与前文分析的 Spring Data JPA 、Hibernate 等 ORM 框架一样。MyBatis 封装重复性代码的方式是通过 Mapper 映射配置文件以及相关注解,将 ResultSet 结果映射为 Java 对象,在具体的映射规则中可以嵌套其他映射规则和必要的子查询,这样就可以轻松实现复杂映射的逻辑,当然,也能够实现一对一、一对多、多对多关系映射以及相应的双向关系映射。

很多人会将 Hibernate 和 MyBatis 做比较,认为 Hibernate 是全自动 ORM 框架,而 MyBatis 只是半自动的 ORM 框架或是一个 SQL 模板引擎。其实,这些比较都无法完全说明一个框架比另一个框架先进,关键还是看应用场景。

MyBatis 相较于 Hibernate 和各类 JPA 实现框架更加灵活、更加轻量级、更加可控

  • 我们可以在 MyBatis 的 Mapper 映射文件中,直接编写原生的 SQL 语句,应用底层数据库产品的方言,这就给了我们直接优化 SQL 语句的机会
  • 我们还可以按照数据库的使用规则,让原生 SQL 语句选择我们期望的索引,从而保证服务的性能,这就特别适合大数据量、高并发等需要将 SQL 优化到极致的场景
  • 在编写原生 SQL 语句时,我们也能够更加方便地控制结果集中的列,而不是查询所有列并映射对象后返回,这在列比较多的时候也能起到一定的优化效果。(当然,Hibernate 也能实现这种效果,需要在实体类添加对应的构造方法。)

在实际业务中,对同一数据集的查询条件可能是动态变化的,如果你有使用 JDBC 或其他类似框架的经历应该能体会到,拼接 SQL 语句字符串是一件非常麻烦的事情,尤其是条件复杂的场景中,拼接过程要特别小心,要确保在合适的位置添加“where”“and”“in”等 SQL 语句的关键字以及空格、逗号、等号等分隔符,而且这个拼接过程非常枯燥、没有技术含量,可能经过反复调试才能得到一个可执行的 SQL 语句。
MyBatis 提供了强大的动态 SQL 功能来帮助我们开发者摆脱这种重复劳动,我们只需要在映射配置文件中编写好动态 SQL 语句,MyBatis 就可以根据执行时传入的实际参数值拼凑出完整的、可执行的 SQL 语句。

3. DDD订单系项目中Mybatis实战

一个简易订单系统的持久化层为例进行讲解,整体的讲解逻辑是这样的:

  • 首先介绍订单系统 domain 层的设计,了解如何将业务概念抽象成 Java 类;
  • 接下来介绍数据库表的设计,同时说明关系型的数据库表与面向对象模型的类之间的映射关系;
  • 随后介绍订单系统的 DAO 接口层,DAO 接口层是操作数据的最小化单元,也是读写数据库的地基;
  • 最后再简单提供了一个 Service 层和测试用例,用来检测前面的代码实现是否能正常工作。

3.1. Mybatis架包依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zhuangxiaoyan</groupId>
    <artifactId>springboot_mybatis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_mybatis</name>
    <description>springboot_mybatis</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <springboot-mybatis-version>2.2.2</springboot-mybatis-version>
        <lombok-version>1.18.30</lombok-version>
        <druid-version>1.2.22</druid-version>
        <mysql-connector-verison>5.1.49</mysql-connector-verison>
        <junit4-version>4.12</junit4-version>
    </properties>
    <dependencies>
        <!-- springboot-web依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springboot-test依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- MyBatis依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${springboot-mybatis-version}</version>
        </dependency>
        <!--MySQL JDBC依赖,用来连接数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-verison}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Lombok注解 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok-version}</version>
            <scope>provided</scope>
        </dependency>
   
        <!-- JUnit 4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit4-version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.zhuangxiaoyan.springboot.mybatis.SpringbootMybatisApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

3.2. 领域Domain模型设计

在业务系统的开发中,domain 层的主要目的就是将业务上的概念抽象成面向对象模型中的类,这些类是业务系统运作的基础。在我们的简易订单系统中,有用户、地址、订单、订单条目和商品这五个核心的概念。

简易订单系统 domain 层设计图说明

  • 在上图中,Customer 类抽象的是电商平台中的用户,其中记录了用户的唯一标识(id 字段)、姓名(name 字段)以及手机号(phone 字段),另外,还记录了当前用户添加的全部送货地址。
  • Address 类抽象了用户的送货地址,其中记录了街道(street 字段)、城市(city 字段)、国家(country 字段)等信息,还维护了一个 Customer 类型的引用,指向所属的用户。
  • Order 类抽象的是电商平台中的订单,记录了订单的唯一标识(id 字段)、订单创建时间(createTime 字段),其中通过 customer 字段(Customer 类型)指向了订单关联的用户,通过 deliveryAddress 字段(Address 类型)指向了该订单的送货地址。另外,还可以通过 orderItems 集合(List 集合)记录订单内的具体条目。
  • OrderItem 类抽象了订单中的购物条目,记录了购物条目的唯一标识(id 字段),其中 product 字段(Product 类型)指向了该购物条目中具体购买的商品,amount 字段记录购买商品的个数,price 字段则是该 OrderItem 的总金额(即 Product.price * amount),Order 订单的总价格(totalPrice 字段)则是由其中全部 OrderItem 的 price 累加得到的。注意,这里的 OrderItem 总金额以及 Order 总金额,都不会持久化到数据,而是实时计算得到的。
  • Product 类抽象了电商平台中商品的概念,其中记录了商品的唯一标识(id 字段)、商品名称(name 字段)、商品描述(description 字段)以及商品价格(price 字段)。

结合前面的介绍以及类图分析,你可以看到:

  • 通过 Customer.addresses 以及 Address.customer 这两个属性,维护了 Customer 与 Address 之间一对多关系;
  • 通过 Order.customer 属性,维护了 Customer 与 Order 之间的一对多关系;
  • 通过 Order.deliveryAddress 属性,维护了 Order 与 Address 之间的一对一关系;
  • 通过 OrderItem.orderId 属性,维护了 Order 与 OrderItem 之间的一对多关系;
  • 通过 OrderItem.product 属性,维护了 OrderItem 与 Product 之间的一对一关系。

3.3. 数据库表设计

介绍完 domain 层的设计,下面我们再来看对应的数据库表设计,如下图所示:

与前面的domain 层设计图相比,其中的各项是可以一一对应起来的。

  • t_customer 表对应 Customer 类,t_product 表对应 Product 类。
  • t_address 表对应 Address 类,其中 customer_id 列作为外键指向 t_customer.id,实现了 Customer 与 Address 的一对多关系。
  • t_order_item 表对应 OrderItem 类,其中 product_id 列作为外键指向 t_product.id,实现了 OrderItem 与 Product 的一对一关系;order_id 列作为外键指向 t_order.id,实现了 Order 与 OrderItem 的一对多关系。
  • t_order 表对应 Order 类,其中的 customer_id 列指向 t_customer.id,实现了 Customer 与 Order 的一对多关系;address_id 列指向 t_address.id,实现了 Order 与 Address 的一对一关系。
CREATE
DATABASE springboot_mybatis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

use
springboot_mybatis;

-- springboot_mybatis.address definition

CREATE TABLE `address`
(
  `id`          bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `street`      varchar(128) NOT NULL COMMENT '街道',
  `city`        varchar(128) NOT NULL COMMENT '城市',
  `country`     varchar(128) NOT NULL COMMENT '国家',
  `gmt_create`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modify`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `version`     varchar(50)           DEFAULT NULL COMMENT '版本号',
  `extra_info`  text COMMENT '扩展信息',
  `customer_id` varchar(100)          DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Address 表';


-- springboot_mybatis.customer definition

CREATE TABLE `customer`
(
  `id`         bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name`       varchar(255)          DEFAULT NULL COMMENT '客户名称',
  `phone`      varchar(20)           DEFAULT NULL COMMENT '客户电话',
  `gmt_create` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modify` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `version`    varchar(50)           DEFAULT NULL COMMENT '版本号',
  `extra_info` text COMMENT '扩展信息',
  `order_id`   varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Customer 表';


-- springboot_mybatis.`order` definition

CREATE TABLE `order`
(
  `id`                  bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `customer_id`         varchar(100) NOT NULL COMMENT '客户ID',
  `delivery_address_id` varchar(100) NOT NULL COMMENT '收货地址ID',
  `total_price`         decimal(10, 2)        DEFAULT NULL COMMENT '订单总价',
  `gmt_create`          datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modify`          datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `version`             varchar(50)           DEFAULT NULL COMMENT '版本号',
  `extra_info`          text COMMENT '扩展信息',
  `order_item_id`       varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';


-- springboot_mybatis.order_item definition

CREATE TABLE `order_item`
(
  `id`         bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `product_id` bigint(20) unsigned NOT NULL COMMENT '商品ID',
  `amount`     int(11) DEFAULT NULL COMMENT '数量',
  `price`      decimal(10, 2)    DEFAULT NULL COMMENT '单价',
  `order_id`   bigint(20) unsigned NOT NULL COMMENT '订单ID',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modify` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `version`    varchar(50)       DEFAULT NULL COMMENT '版本号',
  `extra_info` text COMMENT '扩展信息',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';


-- springboot_mybatis.product definition

CREATE TABLE `product`
(
    `id`            bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '商品ID',
    `name`          varchar(255)   NOT NULL COMMENT '商品名称',
    `description`   text COMMENT '商品描述',
    `price`         decimal(10, 2) NOT NULL COMMENT '商品价格',
    `gmt_create`    datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modify`    datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    `version`       varchar(50)             DEFAULT NULL COMMENT '版本号',
    `extra_info`    text COMMENT '扩展信息',
    `order_item_id` bigint(20) unsigned NOT NULL COMMENT '关联order_item_id',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

3.4. 创建Java实体类

package com.zhuangxiaoyan.springboot.mybatis.domain;

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.math.BigDecimal;
import java.util.Date;

/**
 * Product
 *
 * @author xjl
 * @version 2025/01/11 16:58
 **/

@Data
@EqualsAndHashCode
public class Product {

    private long id;
    /**
     * 名称
     */
    private String name;
    /**
     * 描述
     */
    private String description;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 创建时间
     */
    private Date gmtCreate;
    /**
     * 修改时间
     */
    private Date gmtModify;
    /**
     * 版本号
     */
    private String version;
    /**
     * 扩展信息
     */
    private String extraInfo;

    /**
     * 订单项id
     */
    private long OrderItemId;

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", price=" + price +
                ", gmtCreate=" + gmtCreate +
                ", gmtModify=" + gmtModify +
                ", version='" + version + '\'' +
                ", extraInfo='" + extraInfo + '\'' +
                '}';
    }
}

3.5. 配置数据库连接配置

server:
  port: 8080                 # 设置应用程序运行端口
  servlet:
    context-path: /api       # 设置应用程序的上下文路径

spring:
  application:
    name: springboot-mybatis-app  # 设置 Spring Boot 应用程序的名称
  datasource:
    driver-class-name: com.mysql.jdbc.Driver   # MySQL数据库驱动
    url: jdbc:mysql://192.168.3.13:3306/springboot_mybatis?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&connectTimeout=10000&socketTimeout=10000  # 数据库连接URL
    username: root   # 数据库用户名
    password: root   # 数据库密码
    hikari:                   # 配置 Hikari 数据源连接池(Spring Boot 2 默认使用 HikariCP)
      minimum-idle: 5         # 最小空闲连接数
      maximum-pool-size: 10   # 最大连接池大小
      idle-timeout: 30000     # 空闲连接的最大生命周期(毫秒)
      connection-timeout: 30000  # 连接超时时间(毫秒)
      pool-name: HikariCP      # 连接池名称
  jackson:
    serialization:
      fail-on-empty-beans: false  # 禁用 Jackson 序列化空 JavaBean 错误
  thymeleaf:
    cache: false               # 开启/关闭 Thymeleaf 模板缓存
  messages:
    basename: messages         # 配置国际化消息文件路径(messages.properties)

logging:
  level:
    root: INFO                 # 设置根日志级别
    org.apache.ibatis: DEBUG   # 设置根mybatis的日志级别
    org.springframework.web: DEBUG  # 设置 Spring Web 的日志级别
    com.zhuangxiaoyan.springboot: DEBUG  # 设置自定义包的日志级别
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"  # 设置日志输出格式

3.6. 创建MyBatis Mapper接口类

package com.zhuangxiaoyan.springboot.mybatis.dao;

import com.zhuangxiaoyan.springboot.mybatis.domain.Product;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * ProductDAO
 *
 * @author xjl
 * @version 2025/01/11 21:22
 **/
@Mapper
public interface ProductMapper {

    /**
     * 插入商品
     *
     * @param product
     * @return
     */
    int insertProduct(Product product);

    /**
     * 更新商品
     *
     * @param product
     * @return
     */
    int updateProduct(Product product);

    /**
     * 删除商品
     *
     * @param id
     * @return
     */
    int removeProduct(long id);

    /**
     * 根据id查询商品
     *
     * @param id
     * @return
     */
    Product getProductById(long id);

    /**
     * 查询所有商品
     *
     * @return
     */
    List<Product> getAllProducts();
}

3.7. 配置Mapper XML文件

<?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">
<mapper namespace="com.zhuangxiaoyan.springboot.mybatis.dao.ProductMapper">
    <!-- 定义映射关系 -->
    <resultMap id="BaseResultMap" type="com.zhuangxiaoyan.springboot.mybatis.domain.Product">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="name" column="name" jdbcType="VARCHAR"/>
        <result property="description" column="description" jdbcType="VARCHAR"/>
        <result property="price" column="price" jdbcType="DECIMAL"/>
        <result property="gmtCreate" column="gmt_create" jdbcType="TIMESTAMP"/>
        <result property="gmtModify" column="gmt_modify" jdbcType="TIMESTAMP"/>
        <result property="version" column="version" jdbcType="VARCHAR"/>
        <result property="extraInfo" column="extra_info" jdbcType="VARCHAR"/>
        <result property="orderItemId" column="order_item_id" jdbcType="VARCHAR"/>
    </resultMap>

    <!-- 数据表全列名 -->
    <sql id="Base_Column_List">
        id
        ,name,description,price,gmt_create,gmt_modify,version,extra_info,order_item_id
    </sql>

    <!-- 添加product -->
    <insert id="insertProduct" useGeneratedKeys="true" keyProperty="id"
            parameterType="com.zhuangxiaoyan.springboot.mybatis.domain.Product">
        <if test="OrderItemId == 0">
            <!-- 抛出异常 保障product 必须有相关关系 -->
            THROW EXCEPTION 'order_item_id cannot be 0';
        </if>
        insert into product (name, description, price, gmt_create, gmt_modify, version, extra_info, order_item_id)
        values (#{name}, #{description}, #{price}, now(), now(), 1, #{extraInfo}, #{OrderItemId})
    </insert>

    <!-- 更新product -->
    <update id="updateProduct" parameterType="com.zhuangxiaoyan.springboot.mybatis.domain.Product">
        update product
        <set>
            gmt_modify = now(),
            <if test="name != null">
                name = #{name},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="orderItemId != null">
                order_item_id = #{OrderItemId},
            </if>
            <if test="price != null">
                price = #{price}
            </if>
        </set>
        where id = #{id}
    </update>

    <!-- 删除product -->
    <delete id="removeProduct">
        delete
        from product
        where id = #{id}
    </delete>

    <!-- 通过id查询product -->
    <select id="getProductById" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from product
        where id = #{id}
    </select>

    <!-- 获取所有product -->
    <select id="getAllProducts" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from product
    </select>
</mapper>

3.8. 创建Mapper单元测试类

package com.zhuangxiaoyan.springboot.mybatis.service;

import com.zhuangxiaoyan.springboot.mybatis.SpringbootMybatisApplication;
import com.zhuangxiaoyan.springboot.mybatis.dao.ProductMapper;
import com.zhuangxiaoyan.springboot.mybatis.domain.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * ProductServiceTest
 *
 * @author xjl
 * @version 2025/01/11 19:15
 **/
@SpringBootTest(classes = SpringbootMybatisApplication.class)
public class ProductServiceTest {

    @Autowired
    private ProductMapper productMapper;

    private Product product;

    @Test
    public void testAddProduct() {
        // 初始化 Product 对象
        product = new Product();
        product.setId(1L);
        product.setName("Product A");
        product.setDescription("Description of Product A");
        product.setPrice(new BigDecimal("99.99"));
        product.setVersion("v1.0");
        product.setExtraInfo("Extra info for Product A");
        product.setOrderItemId(10L);
        int addProductId = productMapper.insertProduct(product);
        System.out.println("Add product success, product id is " + addProductId);
    }

    @Test
    public void testUpdateProduct() {
        product = new Product();
        product.setId(1L);
        product.setPrice(new BigDecimal("101.99"));
        product.setDescription("update test");
        product.setOrderItemId(10L);
        int result = productMapper.updateProduct(product);
        assertEquals(1, result);
        System.out.println(result);
    }

    @Test
    public void testRemoveProduct() {
        product = new Product();
        product.setId(8L);
        int result = productMapper.removeProduct(product.getId());
        assertEquals(1, result);
        System.out.println(result);
    }

    @Test
    public void testGetProductById() {
        Product queryProduct = productMapper.getProductById(1L);
        assertNotNull(queryProduct);
        System.out.println(queryProduct);
    }

    @Test
    public void testGetAllProducts() {
        List<Product> allProducts = productMapper.getAllProducts();
        allProducts.stream().forEach(System.out::println);
    }
}

4. Mybatis编码经验总结

4.1. 【强制】数据库设计的不要使用级联关系,而是使用外键来关联

在数据库设计中,是否使用级联关系(Cascade)以及如何利用外键主要取决于业务需求和系统性能考虑。以下是不用级联关系、仅使用外键关联的优缺点及实践建议:

为什么不使用级联关系?

  • 复杂性增加:级联操作(如级联删除或更新)可能导致复杂的联动操作,尤其是在关系较为复杂的数据库中,容易引发意外的连锁更新或删除。
  • 性能问题:当涉及到大数据量时,级联更新或删除会占用大量数据库资源,导致性能下降。大量的级联操作可能导致事务锁表,从而影响数据库的并发性能。
  • 业务逻辑不明确:级联操作将业务逻辑嵌入到数据库层,可能导致代码和数据库逻辑分离,增加调试难度。
  • 控制力弱:数据库自动执行级联操作,程序员难以在操作中介入或控制,增加了意外数据丢失的风险。

使用外键关联的好处

  • 数据一致性:外键确保了表与表之间的引用完整性(Referential Integrity),避免了孤立或无效的数据。
  • 操作更灵活:手动控制关联数据的更新和删除,可以在代码层面清晰地定义和实现业务规则,增加灵活性。
  • 提高可读性:业务逻辑在应用层实现,便于代码维护、调试和扩展。

设计与优化建议

  • 使用外键但禁用级联:在数据库设计中可以定义外键关系,但禁止设置级联更新和级联删除(ON DELETE CASCADEON UPDATE CASCADE)。
  • 好处:避免复杂级联操作影响性能。外键约束仍然能帮助验证数据一致性。
  • 在应用层手动实现级联逻辑:在需要删除或更新数据时,明确在代码中实现逻辑。比如,删除 Customer 时,先删除关联的 AddressOrder
// 手动处理关联关系
void deleteCustomer(Long customerId) {
    // 删除地址
    addressRepository.deleteByCustomerId(customerId);
    // 删除订单及关联的OrderItem
    List<Order> orders = orderRepository.findByCustomerId(customerId);
    for (Order order : orders) {
        orderItemRepository.deleteByOrderId(order.getId());
    }
    orderRepository.deleteByCustomerId(customerId);
    // 最后删除Customer
    customerRepository.deleteById(customerId);
}
  • 对外键列加索引:外键列通常用于查询和连接操作,为提高查询性能,建议对外键列添加索引:
CREATE INDEX idx_customer_id ON t_address(customer_id);
CREATE INDEX idx_product_id ON t_order_item(product_id);
  • 设计事务确保数据完整性:在手动处理关联关系时,确保所有相关操作在同一个事务中完成,以避免数据不一致。
@Transactional
public void deleteCustomer(Long customerId) {
    // 删除操作
}

4.2. 【强制】数据库字段Price等字段应该使用BigDecimal类型,而不是String类型。

为什么使用 BigDecimal 类型?

  • 精确性:BigDecimal 是 Java 提供的高精度数据类型,专门用于处理需要精确计算的小数,尤其适合货币金额和财务数据的存储和运算。使用 BigDecimal 可以避免浮点数 (floatdouble) 存在的精度问题。
  • 方便进行运算:金额字段通常需要进行加、减、乘、除等运算,BigDecimal 提供了完善的 API 支持精确运算,而 String 类型无法直接进行数学运算。

使用 String 的问题

  • 不直观String 类型存储金额会让数据显得难以理解,需要手动解析为数值类型进行运算。
  • 容易出错String 类型不能直接比较大小或计算,可能引入意外错误。
  • 性能问题:每次操作都需要将 String 转换为数值类型,增加额外的性能开销。

注意: 避免使用 new BigDecimal(double),因为 double 会引入精度问题,推荐使用 StringBigDecimal.valueOf

假设你的数据库中 price 字段类型为 DECIMAL(10,2),对应的 Java 实体类字段声明为:

import java.math.BigDecimal;

public class Product {
    private BigDecimal price; // 高精度存储金额
}

总结

  • 数据库字段建议使用 DECIMALNUMERIC 类型。
  • Java 实体类字段建议使用 BigDecimal 类型。
  • 避免使用 String 类型存储金额或价格数据,以防数据不直观且难以运算和验证。

4.3. 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

说明: 增加查询分析器解析成本、增减字段容易与 resultMap 配置不一致、无用字段增加网络 消耗,尤其是 text 类型的字段。

假设我们需要查询 usersorders 两个表的数据,可以使用 JOIN 操作,并且避免使用 * 来避免字段冲突。

<!-- 正确示例:明确指定需要查询的字段,避免字段冲突 -->

<!-- 定义通用的列 -->
<sql id="Base_Column_List">
    id, customer_id, street, city, country, gmt_create, gmt_modify, version, extra_info
</sql>

<select id="selectUserOrders" resultMap="userOrderResultMap">
    SELECT 
        <include refid="Base_Column_List"/>
    FROM users u
    JOIN orders o ON u.id = o.user_id;
</select>

在这个例子中,我们通过 AS 为表 usersorders 中同名的 id 字段添加别名(user_idorder_id),避免了字段冲突。这样做可以确保查询结果的字段名称清晰且不重复。

定义 resultMap 映射:为了将查询结果正确映射到 Java 对象,我们需要定义 resultMap 来映射字段到对象属性。例如,我们有 UserOrder 类,分别表示 usersorders 表。

<resultMap id="userOrderResultMap" type="UserOrderDTO">
  <result property="userId" column="user_id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
  <result property="orderId" column="order_id"/>
  <result property="totalPrice" column="total_price"/>
</resultMap>

 <!-- 可以定义多个 -->
<resultMap id="userOrderResultMapTest" type="UserOrderDTO">
  <result property="userId" column="user_id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
</resultMap>

这里,UserOrderDTO 是我们查询结果的封装对象。字段 user_id 映射到 userIdusername 映射到 username 等。通过这种方式,我们可以确保查询结果按照我们定义的字段正确映射到 Java 对象。

@Data
@EqualsAndHashCode
public class UserOrderDTO {
    private int userId;
    private String username;
    private String email;
    private int orderId;
    private BigDecimal totalPrice;
}

使用 sql 标签来引用公共 SQL 片段

首先,我们可以在 MyBatis 的 XML 配置文件中定义一个公共的 SQL 片段,比如查询 users 表的基本字段。使用 sql 标签,可以在多个查询中复用这个片段。

<!-- 定义公共的 SQL 片段 -->
<sql id="userColumns">
    id, username, email
</sql>

接下来,我们可以在查询中引用这个 SQL 片段,避免重复书写这些字段。

<!-- 查询所有用户的基本信息 -->
<select id="selectUsers" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users;
</select>

复用 SQL 片段(多次引用):如果需要查询其他表的数据,或者想要使用相同的列来进行其他操作,我们仍然可以引用同一个 SQL 片段。

<!-- 查询所有订单的用户信息 -->
<select id="selectUserOrders" resultType="UserOrderDTO">
    SELECT 
        <include refid="userColumns"/>, o.id AS order_id, o.total_price 
    FROM users u
    JOIN orders o ON u.id = o.user_id;
</select>

使用 sql 标签来处理复杂的查询条件: 如果查询条件复杂,常常需要复用某些 SQL 片段,可以使用 sql 标签来定义条件部分,方便引用。

使用 sql 标签来处理复杂的查询条件: 如果查询条件复杂,常常需要复用某些 SQL 片段,可以使用 sql 标签来定义条件部分,方便引用。

<!-- 定义一个查询条件 -->
<sql id="userWhereCondition">
    WHERE status = #{status} AND created_at > #{createdAt}
</sql>

<!-- 查询特定状态的用户 -->
<select id="selectUsersByStatus" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users
    <include refid="userWhereCondition"/>
</select>

动态 SQL 片段: 对于动态查询条件,可以结合 MyBatis 的动态 SQL 功能,如 ifwheretrim 等标签来生成灵活的查询条件。

<!-- 动态查询条件 -->
<select id="selectUsersByDynamicConditions" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users
    <where>
        <if test="username != null">
            AND username = #{username}
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
    </where>
</select>

在这个例子中,我们使用了 <where> 标签来动态生成 SQL 条件,只有在参数不为 null 时才会添加对应的条件。

4.4. 【强制】POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行 字段与属性之间的映射。

说明: 参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的。

假设你有一个 users 表,表中有一个布尔字段 is_active,表示用户是否激活。POJO 类中定义的布尔属性是 active,没有 is 前缀。我们需要在 resultMap 中明确将 is_active 字段映射到 active 属性。

数据库表结构:users 表:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

is_active

TINYINT(1)

POJO 类(User 类)

在 POJO 类中,布尔类型的属性不加 is 前缀,直接使用 active 属性名。

@Data
@EqualsAndHashCode
public class User {
    private int id;
    private String username;
    private String email;
    private boolean active; // 布尔属性名不加 "is"
}

resultMap 映射配置:在 MyBatis 的 XML 映射文件中,我们需要显式地使用 resultMap 来将数据库的 is_active 字段映射到 User 类的 active 属性。

<resultMap id="userResultMap" type="User">
  <id property="id" column="id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
  <!-- 映射数据库字段 is_active 到 POJO 类的 active 属性 -->
  <result property="active" column="is_active"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
  SELECT id, username, email, is_active
  FROM users
  WHERE id = #{id};
</select>

4.5. 【强制】不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>; 反过来,每一个表也必然有一个<resultMap>与之对应。

说明:配置映射关系,使字段与DO类解耦,方便维护。

为什么推荐使用 resultMap

  1. 明确的映射关系:使用 resultMap 明确指出数据库列和 Java 对象属性之间的关系,避免了 MyBatis 自动映射时可能出现的歧义,尤其是在属性名与列名不一致的情况下。
  2. 字段别名和处理resultMap 可以定义字段的别名,将数据库字段映射到 Java 类中的属性名,即使字段名不一致,也能做到灵活处理。
  3. 增强的映射控制resultMap 提供了更多的配置选项,如复杂映射(例如嵌套对象映射),以及对字段的额外处理(如日期格式化等)。
  4. 提升代码可读性与可维护性:通过显式的映射,可以提高代码的可读性,其他开发者在阅读代码时,能明确知道字段和属性之间的关系。

假设我们有一个 users 表和一个 User 类,users 表包含 idusernameemailpassword 等字段,User 类有对应的属性,但我们不直接使用 resultClass,而是通过 resultMap 显式定义映射关系。

表结构(users):

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

password

VARCHAR

@Data
@EqualsAndHashCode
public class User {
    private int id;
    private String username;
    private String email;
    private String password;
}

使用 resultMap 进行映射: 即使 User 类的属性和 users 表的字段一一对应,依然推荐使用 resultMap 进行明确映射。

<resultMap id="userResultMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <result property="password" column="password"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
    SELECT id, username, email, password
    FROM users
    WHERE id = #{id};
</select>

复杂类型映射(嵌套映射)

如果表中有外键关系,或者你需要返回嵌套对象(比如 User 类中的 Address 类属性),则可以使用 resultMap 定义嵌套映射。例如,假设 users 表有一个外键 address_id,引用 addresses 表。

@Data
@EqualsAndHashCode
public class Address {
    private int id;
    private String street;
    private String city;
}

users 表结构更新:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

password

VARCHAR

address_id

INT

Address 表结构:

Column

Type

id

INT

street

VARCHAR

city

VARCHAR

定义 resultMap 和嵌套映射:

<resultMap id="addressResultMap" type="Address">
    <id property="id" column="id"/>
    <result property="street" column="street"/>
    <result property="city" column="city"/>
</resultMap>

<resultMap id="userResultMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <result property="password" column="password"/>
    <!-- 嵌套映射 -->
    <association property="address" javaType="Address" resultMap="addressResultMap"/>
</resultMap>

<select id="selectUserWithAddressById" resultMap="userResultMap">
    SELECT u.id, u.username, u.email, u.password, a.id AS address_id, a.street, a.city
    FROM users u
    LEFT JOIN addresses a ON u.address_id = a.id
    WHERE u.id = #{id};
</select>

在这个例子中:

  • 嵌套映射: 我们通过 <association> 标签将 users 表中的 address_id 字段映射到 User 类中的 Address 对象。这意味着,当查询 users 表时,MyBatis 会使用 addressResultMapaddress_id 映射到 Address 类的相关属性。
  • <association> 标签: 用于处理嵌套的映射关系,指定嵌套对象的 resultMap 和映射的属性。

使用 resultMap 进行多表查询:假设我们还需要查询多个表(比如 orders 表),也可以使用类似的方式来映射结果。

查询用户和订单信息:

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="totalPrice" column="total_price"/>
    <result property="userId" column="user_id"/>
</resultMap>

<select id="selectUserWithOrdersById" resultMap="userResultMap">
    SELECT 
        u.id AS user_id, u.username, u.email, u.password,
        o.id AS order_id, o.total_price
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id};
</select>

在这个查询中,我们使用 LEFT JOINusers 表和 orders 表连接,并将 orders 表的字段映射到 Order 类。通过 resultMap,我们将查询结果映射到 UserOrder 对象。

复杂的嵌套对象关系设计总结

  • 即使字段和属性一一对应,也应该使用 resultMap。这种做法明确了映射关系,提升了代码的可读性和可维护性。
  • resultMap 提供了更多的灵活性,可以应对字段名不一致、嵌套对象、复杂查询等复杂情况。
  • 嵌套映射:使用 <association> 标签可以实现多表查询时的对象嵌套映射。
  • 避免使用 resultClass:直接使用 resultClass 可能会导致自动映射的问题,resultMap 让你更加精确地控制字段到属性的映射。

Insert 标签插入组合对象:在 MyBatis 中,如果你需要插入一个组合对象(如包含多个属性或嵌套对象的对象),你可以使用 insert 标签来实现。插入操作通常会使用 parameterType 来指定接收传入参数的 Java 类,然后在 SQL 语句中通过占位符来插入对象的属性值。

数据库表结构:假设我们有两个表:usersaddresses

users 表:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

address_id

INT

addresses 表:

Column

Type

id

INT

street

VARCHAR

city

VARCHAR

我们定义了 User 类,它包含 Address 类的引用,表示一个组合对象。

@Data
@EqualsAndHashCode
public class User {
    private int id;
    private String username;
    private String email;
    private Address address; // Address 类是嵌套的对象
}

@Data
@EqualsAndHashCode
public class Address {
    private int id;
    private String street;
    private String city;
}

insert 标签的配置:假设你要插入一个 User 对象,并且 User 对象中包含一个 Address 对象。你需要在 insert 标签中编写插入语句,同时使用 parameterType 来指明参数类型是 User

<insert id="insertUser" parameterType="User">
    <!-- 插入到 users 表 -->
    INSERT INTO users (username, email, address_id)
    VALUES (#{username}, #{email}, 
        (SELECT id FROM addresses WHERE street = #{address.street} AND city = #{address.city}));
</insert>

多表插入的情况:有时你可能需要先插入一个表(例如 addresses 表),然后将插入后的 id 作为外键插入到其他表(例如 users 表)。这是一个典型的多表插入场景。

假设我们需要先插入 address,然后将其 id 插入到 users 表中:

<insert id="insertAddressAndUser" parameterType="User">
    <!-- 插入地址 -->
    INSERT INTO addresses (street, city) 
    VALUES (#{address.street}, #{address.city});
    
    <!-- 获取插入后的 address_id -->
    <selectKey keyProperty="address.id" resultType="int" order="AFTER">
        SELECT LAST_INSERT_ID();
    </selectKey>

    <!-- 插入用户 -->
    INSERT INTO users (username, email, address_id) 
    VALUES (#{username}, #{email}, #{address.id});
</insert>

解析:

  • <selectKey> 标签: 用于获取刚插入的 addressid,并将其设置到 User 对象的 address.id 属性中。
  • LAST_INSERT_ID(): 用来获取上一次插入操作的自动生成的 ID。这里,我们通过 SELECT LAST_INSERT_ID() 来获取 addressid
  • 多表插入: 第一步插入 address,第二步通过获取插入后的 address.id 来将其插入到 users 表的 address_id 字段。

4.6. 【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。

使用 #{} 传递参数 (推荐做法):#{} 适用于所有的参数绑定,它能够安全地处理各种数据类型。

<select id="selectUserById" resultType="User">
    SELECT id, username, email
    FROM users
    WHERE id = #{id}
</select>

解释:使用 #{id},MyBatis 会将 id 作为一个预处理语句的参数,安全地传递给 SQL 引擎,防止 SQL 注入。

// 假设我们有一个 SqlSession 实例
SqlSession sqlSession = sqlSessionFactory.openSession();

// 查询用户
User user = sqlSession.selectOne("selectUserById", 1);

使用 ${} 存在 SQL 注入风险(避免使用)

#{} 是安全的,而 ${} 直接将参数嵌入到 SQL 语句中,容易导致 SQL 注入攻击。使用 ${} 只有在动态 SQL 中必须拼接表名、列名、排序字段等的时候才可以使用,但这类情况也需要小心处理。

<!-- 错误示范:直接拼接 SQL 语句 -->
<select id="selectUserByColumn" resultType="User">
    SELECT id, username, email
    FROM users
    WHERE ${columnName} = #{value}
</select>

在这个例子中,${columnName} 会将传入的 columnName 直接替换到 SQL 语句中,如果 columnName 的值来自用户输入,攻击者就可以利用这个特性进行 SQL 注入。

4.7. 【强制】iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用。

说明: 其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size 的子集合。

正例: Map<String, Object> map = new HashMap<>(16); map.put("start", start); map.put("size", size);	

4.8. 【强制】不允许直接拿HashMap与Hashtable作为查询结果集的输出。

反例: 某同学为避免写一个<resultMap>xxx</resultMap>,直接使用 HashTable 来接收数据库返回结 果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线 上问题。

问题原因:

  1. 类型不明确HashMapHashtable 的键和值是 Object 类型,无法在编译时进行类型检查,容易发生类型转换错误。
  2. 代码可读性差:直接使用 HashMapHashtable 返回结果时,不容易理解字段含义,也不方便重构和维护。
  3. 容易出错:如果查询结果的字段有变动,无法通过类型安全检查及时发现。

因此,推荐使用强类型的 POJO 类来映射查询结果,而不是直接使用 HashMapHashtable

不推荐的方式(使用 HashMap

<select id="selectUserById" resultType="java.util.HashMap">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

问题原因:

  • 查询结果直接映射到 HashMap,其中键是字段名(例如:idusernameemail),值是字段对应的值。HashMap 本身没有类型信息,这样做容易让程序员在处理数据时出错。
  • 如果数据库字段发生变化,或者字段的名字与 POJO 类属性不同,可能会导致错误。

推荐的方式(使用 POJO 类)

正确的做法是使用 POJO 类(即普通 Java 对象)来映射查询结果,这样可以确保类型安全、提高代码可读性,并且在字段变化时也能更容易地进行维护。

创建 POJO 类

假设我们查询的是用户信息,可以创建一个 User 类。

@Data
public class User {
    private int id;
    private String username;
    private String email;
}

使用 resultTyperesultMap 映射 POJO 类

在 MyBatis 的 XML 配置文件中,我们可以使用 resultTyperesultMap 将查询结果映射到 User 类。

方法一:使用 resultType

<select id="selectUserById" resultType="User">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

方法二:使用 resultMap(更灵活的方式)

使用 resultMap 更加灵活,尤其是当数据库字段与 POJO 属性不完全一致时,或者当我们需要处理复杂的映射关系时。

<resultMap id="userResultMap" type="User">
  <id property="id" column="id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

查询并使用结果

在 Java 代码中,通过 SqlSession 执行查询操作,MyBatis 会将结果映射到 User 对象中。

SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("selectUserById", 1);

// 输出查询结果
System.out.println("User ID: " + user.getId());
System.out.println("Username: " + user.getUsername());
System.out.println("Email: " + user.getEmail());

总结与最佳实践:

  1. 避免使用 HashMapHashtable 作为查询结果:直接将查询结果映射到 HashMapHashtable 会丧失类型安全性,导致代码可维护性差,容易出错。
  2. 使用 POJO 类来接收查询结果:通过强类型的 POJO 类进行映射,可以确保类型安全和更好的代码可读性。
  3. 使用 resultMapresultType 映射查询结果:当数据库字段与 POJO 类属性一致时,可以直接使用 resultType;如果需要更复杂的字段映射,则使用 resultMap 来显式指定字段与属性的映射关系。
  4. 类型安全的好处
    • 编译时检查:使用 POJO 类可以让编译器检查类型,避免运行时发生类型转换错误。
    • 代码可读性和可维护性:POJO 类使得字段更具语义,代码更容易理解和维护。
    • 数据库字段变化的可控性:当数据库字段发生变化时,修改 POJO 类中的属性名或者添加新字段可以更容易地保持同步。

4.9. 【强制】更新数据表记录时,必须同时更新记录对应的update_time字段值为当前时间。

在数据库表中更新记录时,通常会要求同时更新 update_time 字段,以记录数据的最后更新时间。这是一个常见的做法,可以通过触发器、应用逻辑或者在 SQL 语句中显式设置来实现。如下sql样例:

-- gmt_modify 字段会在记录更新时自动更新为当前时间。这是由于 ON UPDATE CURRENT_TIMESTAMP 属性的作用。
CREATE TABLE `address`
(
    `id`          bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `street`      varchar(128) NOT NULL COMMENT '街道',
    `city`        varchar(128) NOT NULL COMMENT '城市',
    `country`     varchar(128) NOT NULL COMMENT '国家',
    `gmt_create`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modify`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    `version`     varchar(50)           DEFAULT NULL COMMENT '版本号',
    `extra_info`  text COMMENT '扩展信息',
    `customer_id` varchar(100)          DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Address 表';

使用 SQL 语句进行更新

假设我们有一个 users 表,表结构如下:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

update_time

DATETIME

每次更新用户记录时,我们希望同时更新 update_time 字段为当前时间。

更新 SQL 语句

在 MyBatis 中,我们可以通过 UPDATE 语句来更新记录,并且使用 NOW()(MySQL 的当前时间函数)来更新 update_time 字段。

<update id="updateUser" parameterType="User">
  UPDATE users
  SET username = #{username},
  email = #{email},
  update_time = NOW()
  WHERE id = #{id}
</update>

解释:

  • usernameemail 是要更新的字段。
  • update_time = NOW() 会将 update_time 字段的值设置为当前时间。
  • WHERE id = #{id} 用于指定更新的记录。

使用触发器(Trigger)

如果你希望在数据库层面自动更新 update_time 字段,可以使用数据库触发器来实现。在每次更新某个记录时,触发器会自动更新 update_time 字段。

DELIMITER $$

CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    SET NEW.update_time = NOW();
END$$

DELIMITER ;

解释:

  • 这个触发器会在 users 表的每次 UPDATE 操作之前触发。
  • NEW.update_time = NOW() 将把 update_time 字段设置为当前时间。
  • 这样,每次更新 users 表中的数据时,update_time 字段都会自动更新。

推荐做法:应用层和数据库层的配合,即通过 SQL 显式更新 update_time 字段,同时也可以使用触发器作为补充。这样能确保数据一致性和准确性。

4.10. 【推荐】不要写一个大而全的数据更新接口。

传入为POJO类,不管是不是自己的目标更新字 段,都进行updatetablesetc1=value1,c2=value2,c3=value3; 这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。

你提到的“不要写一个大而全的数据更新接口”实际上是在强调 只更新实际发生改变的字段,这有助于避免更新无关字段,提高数据库操作的效率,减少出错的风险,并且避免增加不必要的日志存储(如 binlog)或数据传输负担。下面是对这个问题的详细解释和示例。

问题分析:

  • 易出错:如果你更新了所有字段(即使字段没有变化),可能会在无意中覆盖掉正确的值,或者导致字段值的错误。
  • 效率低:如果不必要的字段被更新,数据库需要进行不必要的操作,这会浪费资源。
  • 增加 binlog 存储:在 MySQL 等数据库中,所有更新操作都会被记录到 binlog 中。无必要的字段更新会导致更多的 binlog 记录,增加存储和网络负担。

推荐做法:

  1. 只更新变动的字段:动态检查每个字段是否发生变化,如果发生了变化才执行更新操作。
  2. 使用 UPDATE 语句时避免不必要的字段更新
  3. 提供灵活的更新接口:根据传入的参数只更新实际发生变化的字段。

手动检查字段变化(适用于简单的更新逻辑)

当 POJO 类的字段发生变化时,动态构建 UPDATE 语句,只有发生变化的字段才会被更新。

POJO 类示例:假设我们有一个 User 类,包含以下属性:

@Data
public class User {
    private Integer id;
    private String username;
    private String email;
    private Integer age;
    private Date updateTime;

    // Getters and Setters
}

更新接口:可以使用 MyBatis 的动态 SQL 标签 <set> 来仅更新实际发生变化的字段。

<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="username != null">username = #{username},</if>
    <if test="email != null">email = #{email},</if>
    <if test="age != null">age = #{age},</if>
    <if test="updateTime != null">update_time = #{updateTime},</if>
  </set>
  WHERE id = #{id}
</update>

解释:

  • <set> 标签:MyBatis 中的 <set> 标签会自动去掉末尾多余的逗号。
  • <if> 标签:用于判断字段是否为空或者是否发生变化,如果发生变化,则更新该字段。
  • 如果字段值发生变化,SQL 语句会更新该字段;如果没有变化(字段值为 null),则不会更新。

4.11. 【参考】@Transactional事务不要滥用。

事务会影响数据库的QPS,另外使用事务的地方需 要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。

4.12. 【参考】<isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时 带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;<isNotNull>表示不为 null 值 时执行。

在 MyBatis 中,<isEqual>, <isNotEmpty>, 和 <isNotNull> 是常用的动态 SQL 标签,用于根据不同条件生成 SQL 语句的不同部分。它们通常用于 WHERE 子句中,来控制 SQL 语句的生成。以下是每个标签的解释及其使用示例:

<isEqual>:用于判断字段是否等于某个常量(如 1),条件成立时才会生成对应的 SQL。
<isNotEmpty>:检查属性是否不为空且不为 null。如果属性的值不为空或 null,则会加入 SQL 条件。
<isNotNull>:检查属性是否不为 null。如果属性不为 null,则会加入 SQL 条件。
<select id="selectUserByCondition" resultType="User">
    SELECT id, username, email, is_active
    FROM users
    WHERE 1 = 1
    <isEqual property="isActive" compareValue="1">
        AND is_active = 1
    </isEqual>
    <isNotEmpty property="username">
        AND username = #{username}
    </isNotEmpty>
    <isNotNull property="email">
        AND email IS NOT NULL
    </isNotNull>
</select>

4.13. 【推荐】在mybatis中对于对象与对象之间存在一对多或者是多多的关系应该怎么设计

user与的Address 存在一对多情况,mybatis 编码中应该怎么设计?

@Data
@EqualsAndHashCode
public class User {
    private int id;
    private String username;
    private String email;
    private Address address; // Address 类是嵌套的对象
}

@Data
@EqualsAndHashCode
public class Address {
    private int id;
    private String street;
    private String city;
}

4.13.1. 使用 Address 对象作为嵌套属性

优点:

  • 面向对象思想:用户和地址是一个完整的对象关系,User 类中直接嵌套 Address 更符合领域模型设计。
  • 使用对象时更加直观,代码中的关联关系更清晰:
User user = new User();
user.setId(1);
user.setUsername("John");
user.setAddress(new Address(101, "Main Street", "New York"));
  • 便于序列化:如果需要将 User 序列化为 JSON 或其他格式(如 API 返回值),嵌套对象可以直接生成嵌套结构的 JSON:
{
  "id": 1,
  "username": "John",
  "email": "john@example.com",
  "address": {
    "id": 101,
    "street": "Main Street",
    "city": "New York"
  }
}
  • 减少多次查询:如果在业务逻辑中经常需要 UserAddress 的完整数据,这种设计可以通过关联查询一次性获取,避免额外查询 Address 表。

缺点:

  • 复杂性增加:每次查询用户时,都会额外加载整个地址对象。如果地址信息不常用,可能会浪费内存和处理时间。
  • 难以处理更新:如果需要更新 Address 的信息,修改 User 对象可能会导致逻辑不清晰。例如,更新地址时需要确保级联保存。
  • 潜在的性能问题:如果 User 表和 Address 表之间是一对多的关系(一个地址可能对应多个用户),直接嵌套 Address 对象可能会导致冗余数据。

嵌套对象设计中insert标签

在嵌套对象设计中,User 中包含一个嵌套的 Address 对象,因此在插入 User 时需要分别处理 AddressUser 的数据。以下是 MyBatis 中编写 insert 标签的方式:

分步插入

  • 先插入 Address,获取其主键(id)。
  • 再插入 User,将 Address.id 作为外键写入 User.address_id

注意主键回填

  • 数据库生成的 Address.id 主键需要通过 MyBatis 的主键回填机制传递到 User 对象中。
<mapper namespace="com.example.UserMapper">

    <!-- 插入 Address -->
    <insert id="insertAddress" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_address (street, city)
        VALUES (#{street}, #{city});
    </insert>

    <!-- 插入 User -->
    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_user (username, email, address_id)
        VALUES (#{username}, #{email}, #{address.id});
    </insert>

    <!-- 定义 Address 的 resultMap -->
    <resultMap id="addressResultMap" type="Address">
        <id column="id" property="id" />
        <result column="street" property="street" />
        <result column="city" property="city" />
    </resultMap>

    <!-- 定义 User 的 resultMap,包含 Address 的嵌套 -->
    <resultMap id="userResultMap" type="User">
        <id column="id" property="id" />
        <result column="username" property="username" />
        <result column="email" property="email" />
        <association property="address" javaType="Address" resultMap="addressResultMap" />
    </resultMap>

    <!-- 查询 User,关联 Address -->
    <select id="findUserById" resultMap="userResultMap">
        SELECT 
            u.id AS id,
            u.username AS username,
            u.email AS email,
            a.id AS address_id,
            a.street AS street,
            a.city AS city
        FROM t_user u
        LEFT JOIN t_address a ON u.address_id = a.id
        WHERE u.id = #{id};
    </select>
    
</mapper>
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void addUser(User user) {
        // 插入 Address,并回填 ID
        Address address = user.getAddress();
        userMapper.insertAddress(address);

        // 设置 Address.id 到 User 对象中
        user.setAddress(address);

        // 插入 User
        userMapper.insertUser(user);
    }

    public User findUserById(int id) {
        return userMapper.findUserById(id);
    }
}

4.13.2. 使用 addressId 外键方案

@Data
@EqualsAndHashCode
public class User {
    private int id;
    private String username;
    private String email;
    private String addressId; // AddressId 作为外键的关联Address
}

@Data
@EqualsAndHashCode
public class Address {
    private int id;
    private String street;
    private String city;
}

优点:

  • 更简洁的模型:User 中直接使用 addressId 作为外键,减少了对象嵌套的复杂性:
  • 更灵活的查询:如果业务场景中 Address 信息并非每次都需要,可以通过外键关联在需要时再查询地址数据,避免不必要的性能开销:
  • 降低内存占用:不需要每次加载完整的 Address 对象,只需在需要时查询,节省了内存和传输的开销。
  • 更方便的更新操作:更新 User 的地址时,只需要更新 addressId,而不需要修改完整的 Address 对象:

缺点:

  • 违背面向对象思想:数据库中的外键在代码层面表现为单一字段,而不是完整的对象,丢失了领域模型中对象之间的关系。
  • 额外查询需求:如果业务场景中需要频繁获取完整的 UserAddress 信息,就需要在代码中通过外键查询 Address
User user = userRepository.findById(userId);
Address address = addressRepository.findById(user.getAddressId());

4.13.3. 推荐方案

如果业务场景需要频繁访问完整的 UserAddress 信息:

  • 保持当前设计,使用嵌套的 Address 对象。
  • 可以通过 resultMap 配置 MyBatis 将查询结果直接映射为嵌套对象:
<resultMap id="userResultMap" type="User">
  <id column="id" property="id"/>
  <result column="username" property="username"/>
  <result column="email" property="email"/>
  <association property="address" javaType="Address">
    <id column="address_id" property="id"/>
    <result column="street" property="street"/>
    <result column="city" property="city"/>
  </association>
</resultMap>

如果业务场景中 Address 信息较少用到:

  • 使用 addressId 外键设计,延迟加载 Address 信息。
  • 可通过关联查询按需加载地址:
User user = userRepository.findById(userId);
if (user.getAddressId() != null) {
    Address address = addressRepository.findById(user.getAddressId());
    user.setAddress(address);
}

博文参考

  • 《mybtis实战》
  • 《阿里巴巴java规范》

http://www.kler.cn/a/500795.html

相关文章:

  • Qt监控系统远程网络登录/请求设备列表/服务器查看实时流/回放视频/验证码请求
  • 跳表和Mysql联合索引的最左原则和索引下推的优化
  • openCvSharp 计算机视觉图片找茬
  • 在 Flownex 网络中创建传热元件
  • excel VBA 基础教程
  • linux网络 | https前置知识 | 数据加密与解密、数据摘要
  • Vue 常用指令详解(附代码实例)
  • C++(10)—类和对象(上) ③this指针的详解
  • RAG技术:是将知识库的文档和问题共同输入到LLM中
  • 多媒体技术学习笔记
  • 饭搭难点亮点
  • 25/1/12 算法笔记 剖析Yolov8底层逻辑
  • 【2024年华为OD机试】(C卷,100分)- 单词加密(Java JS PythonC/C++)
  • 【学习笔记】理解深度学习的基础:机器学习
  • webpack打包要义
  • 什么是MVCC
  • 【ASP.NET学习】Web Pages 最简单的网页编程开发模型
  • 深入浅出Java Web开放平台:从API设计到安全保障的全方位探索
  • --- 多线程编程 基本用法 java ---
  • 从零开始开发纯血鸿蒙应用之多签名证书管理
  • A3. Springboot3.x集成LLama3.2实战
  • B+ 树的实现原理与应用场景
  • 20250112面试鸭特训营第20天
  • 移动端屏幕分辨率rem,less
  • 前端开发:HTML常见标签
  • 慧集通(DataLinkX)iPaaS集成平台-业务建模之业务对象(二)