Mybatis——Mybatis开发经验总结
摘要
本文主要介绍了MyBatis框架的设计与通用性,阐述了其作为Java持久化框架的亮点,包括精良的架构设计、丰富的扩展点以及易用性和可靠性。同时,对比了常见持久层框架,分析了MyBatis在关系型数据库交互中的优势。此外,还提供了订单系统持久层示例分析,涵盖从架包依赖到单元测试类的创建等步骤,并总结了MyBatis编码经验,给出了相关强制和推荐规范。
1. Mybatis框架的设计与通用性
MyBatis 是 Java 生态中非常著名的一款 ORM 框架。这是一款很值得你学习和研究的 Java 持久化框架。原因主要有两个:
- MyBatis 自身有很多亮点值得你深挖;
- MyBatis 在一线互联网大厂中应用广泛,已经成为你进入大厂的必备技能。
MyBatis 所具备的亮点可总结为如下三个方面:
- MyBatis 本身就是一款设计非常精良、架构设计非常清晰的持久层框架,并且 MyBatis 中还使用到了很多经典的设计模式,例如,工厂方法模式、适配器模式、装饰器模式、代理模式等。 在阅读 MyBatis 代码的时候,你也许会惊奇地发现:原来大师设计出来的代码真的是一种艺术。所以,从这个层面来讲,深入研究 MyBatis 原理,甚至阅读它的源码,不仅可以帮助你快速解决工作中遇到的 MyBatis 相关问题,还可以提高你的设计思维。
- MyBatis 提供了很多扩展点,例如,MyBatis 的插件机制、对第三方日志框架和第三方数据源的兼容等。 正由于这种可扩展的能力,让 MyBatis 的生命力非常旺盛,这也是很多 Java 开发人员将 MyBatis 作为自己首选 Java 持久化框架的原因之一,反过来促进了 MyBatis 用户的不断壮大。
- 开发人员使用 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 操作的核心步骤,具体如下:
- 注册数据库驱动类,指定数据库地址,其中包括 DB 的用户名、密码及其他连接信息;
- 调用 DriverManager.getConnection() 方法创建 Connection 连接到数据库;
- 调用 Connection 的 createStatement() 或 prepareStatement() 方法,创建 Statement 对象,此时会指定 SQL(或是 SQL 语句模板 + SQL 参数);
- 通过 Statement 对象执行 SQL 语句,得到 ResultSet 对象,也就是查询结果集;
- 遍历 ResultSet,从结果集中读取数据,并将每一行数据库记录转换成一个 JavaBean 对象;
- 关闭 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 definitionCREATE 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 definitionCREATE 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` definitionCREATE 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 definitionCREATE 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 definitionCREATE 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;@Overridepublic 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 # 数据库连接URLusername: 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 mapperPUBLIC "-//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">deletefrom productwhere id = #{id}</delete><!-- 通过id查询product --><select id="getProductById" resultMap="BaseResultMap">select<include refid="Base_Column_List"/>from productwhere 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 {@Autowiredprivate ProductMapper productMapper;private Product product;@Testpublic 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);}@Testpublic 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);}@Testpublic void testRemoveProduct() {product = new Product();product.setId(8L);int result = productMapper.removeProduct(product.getId());assertEquals(1, result);System.out.println(result);}@Testpublic void testGetProductById() {Product queryProduct = productMapper.getProductById(1L);assertNotNull(queryProduct);System.out.println(queryProduct);}@Testpublic void testGetAllProducts() {List<Product> allProducts = productMapper.getAllProducts();allProducts.stream().forEach(System.out::println);}
}
4. Mybatis编码经验总结
4.1. 【强制】数据库设计的不要使用级联关系,而是使用外键来关联
在数据库设计中,是否使用级联关系(Cascade)以及如何利用外键主要取决于业务需求和系统性能考虑。以下是不用级联关系、仅使用外键关联的优缺点及实践建议:
为什么不使用级联关系?
- 复杂性增加:级联操作(如级联删除或更新)可能导致复杂的联动操作,尤其是在关系较为复杂的数据库中,容易引发意外的连锁更新或删除。
- 性能问题:当涉及到大数据量时,级联更新或删除会占用大量数据库资源,导致性能下降。大量的级联操作可能导致事务锁表,从而影响数据库的并发性能。
- 业务逻辑不明确:级联操作将业务逻辑嵌入到数据库层,可能导致代码和数据库逻辑分离,增加调试难度。
- 控制力弱:数据库自动执行级联操作,程序员难以在操作中介入或控制,增加了意外数据丢失的风险。
使用外键关联的好处
- 数据一致性:外键确保了表与表之间的引用完整性(Referential Integrity),避免了孤立或无效的数据。
- 操作更灵活:手动控制关联数据的更新和删除,可以在代码层面清晰地定义和实现业务规则,增加灵活性。
- 提高可读性:业务逻辑在应用层实现,便于代码维护、调试和扩展。
设计与优化建议
- 使用外键但禁用级联:在数据库设计中可以定义外键关系,但禁止设置级联更新和级联删除(
ON DELETE CASCADE
、ON UPDATE CASCADE
)。 - 好处:避免复杂级联操作影响性能。外键约束仍然能帮助验证数据一致性。
- 在应用层手动实现级联逻辑:在需要删除或更新数据时,明确在代码中实现逻辑。比如,删除
Customer
时,先删除关联的Address
和Order
。
// 手动处理关联关系
void deleteCustomer(Long customerId) {// 删除地址addressRepository.deleteByCustomerId(customerId);// 删除订单及关联的OrderItemList<Order> orders = orderRepository.findByCustomerId(customerId);for (Order order : orders) {orderItemRepository.deleteByOrderId(order.getId());}orderRepository.deleteByCustomerId(customerId);// 最后删除CustomercustomerRepository.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
可以避免浮点数 (float
或double
) 存在的精度问题。 - 方便进行运算:金额字段通常需要进行加、减、乘、除等运算,
BigDecimal
提供了完善的 API 支持精确运算,而String
类型无法直接进行数学运算。
使用 String 的问题
- 不直观:
String
类型存储金额会让数据显得难以理解,需要手动解析为数值类型进行运算。 - 容易出错:
String
类型不能直接比较大小或计算,可能引入意外错误。 - 性能问题:每次操作都需要将
String
转换为数值类型,增加额外的性能开销。
注意: 避免使用 new BigDecimal(double)
,因为 double
会引入精度问题,推荐使用 String
或 BigDecimal.valueOf
:
假设你的数据库中 price 字段类型为 DECIMAL(10,2),对应的 Java 实体类字段声明为:import java.math.BigDecimal;public class Product {private BigDecimal price; // 高精度存储金额
}
总结
- 数据库字段建议使用
DECIMAL
或NUMERIC
类型。 - Java 实体类字段建议使用
BigDecimal
类型。 - 避免使用
String
类型存储金额或价格数据,以防数据不直观且难以运算和验证。
4.3. 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明: 增加查询分析器解析成本、增减字段容易与 resultMap 配置不一致、无用字段增加网络 消耗,尤其是 text 类型的字段。
假设我们需要查询 users
和 orders
两个表的数据,可以使用 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 uJOIN orders o ON u.id = o.user_id;
</select>
在这个例子中,我们通过 AS
为表 users
和 orders
中同名的 id
字段添加别名(user_id
和 order_id
),避免了字段冲突。这样做可以确保查询结果的字段名称清晰且不重复。
定义 resultMap
映射:为了将查询结果正确映射到 Java 对象,我们需要定义 resultMap
来映射字段到对象属性。例如,我们有 User
和 Order
类,分别表示 users
和 orders
表。
<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
映射到 userId
,username
映射到 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 uJOIN 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 功能,如 if
、where
、trim
等标签来生成灵活的查询条件。
<!-- 动态查询条件 -->
<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 |
| 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_activeFROM usersWHERE id = #{id};
</select>
4.5. 【强制】不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>; 反过来,每一个表也必然有一个<resultMap>与之对应。
说明:配置映射关系,使字段与DO类解耦,方便维护。
为什么推荐使用 resultMap
?
- 明确的映射关系:使用
resultMap
明确指出数据库列和 Java 对象属性之间的关系,避免了 MyBatis 自动映射时可能出现的歧义,尤其是在属性名与列名不一致的情况下。 - 字段别名和处理:
resultMap
可以定义字段的别名,将数据库字段映射到 Java 类中的属性名,即使字段名不一致,也能做到灵活处理。 - 增强的映射控制:
resultMap
提供了更多的配置选项,如复杂映射(例如嵌套对象映射),以及对字段的额外处理(如日期格式化等)。 - 提升代码可读性与可维护性:通过显式的映射,可以提高代码的可读性,其他开发者在阅读代码时,能明确知道字段和属性之间的关系。
假设我们有一个 users
表和一个 User
类,users
表包含 id
、username
、email
和 password
等字段,User
类有对应的属性,但我们不直接使用 resultClass
,而是通过 resultMap
显式定义映射关系。
表结构(users
):
Column | Type |
id | INT |
username | VARCHAR |
| 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, passwordFROM usersWHERE 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 |
| 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.cityFROM users uLEFT JOIN addresses a ON u.address_id = a.idWHERE u.id = #{id};
</select>
在这个例子中:
- 嵌套映射: 我们通过
<association>
标签将users
表中的address_id
字段映射到User
类中的Address
对象。这意味着,当查询users
表时,MyBatis 会使用addressResultMap
将address_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_priceFROM users uLEFT JOIN orders o ON u.id = o.user_idWHERE u.id = #{id};
</select>
在这个查询中,我们使用 LEFT JOIN
将 users
表和 orders
表连接,并将 orders
表的字段映射到 Order
类。通过 resultMap
,我们将查询结果映射到 User
和 Order
对象。
复杂的嵌套对象关系设计总结
- 即使字段和属性一一对应,也应该使用
resultMap
。这种做法明确了映射关系,提升了代码的可读性和可维护性。 resultMap
提供了更多的灵活性,可以应对字段名不一致、嵌套对象、复杂查询等复杂情况。- 嵌套映射:使用
<association>
标签可以实现多表查询时的对象嵌套映射。 - 避免使用
resultClass
:直接使用resultClass
可能会导致自动映射的问题,resultMap
让你更加精确地控制字段到属性的映射。
Insert 标签插入组合对象:在 MyBatis 中,如果你需要插入一个组合对象(如包含多个属性或嵌套对象的对象),你可以使用 insert
标签来实现。插入操作通常会使用 parameterType
来指定接收传入参数的 Java 类,然后在 SQL 语句中通过占位符来插入对象的属性值。
数据库表结构:假设我们有两个表:users
和 addresses
。
users
表:
Column | Type |
id | INT |
username | VARCHAR |
| 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>
标签: 用于获取刚插入的address
的id
,并将其设置到User
对象的address.id
属性中。LAST_INSERT_ID()
: 用来获取上一次插入操作的自动生成的 ID。这里,我们通过SELECT LAST_INSERT_ID()
来获取address
的id
。- 多表插入: 第一步插入
address
,第二步通过获取插入后的address.id
来将其插入到users
表的address_id
字段。
4.6. 【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
使用 #{}
传递参数 (推荐做法):#{}
适用于所有的参数绑定,它能够安全地处理各种数据类型。
<select id="selectUserById" resultType="User">SELECT id, username, emailFROM usersWHERE 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, emailFROM usersWHERE ${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,导致线 上问题。
问题原因:
- 类型不明确:
HashMap
和Hashtable
的键和值是Object
类型,无法在编译时进行类型检查,容易发生类型转换错误。 - 代码可读性差:直接使用
HashMap
或Hashtable
返回结果时,不容易理解字段含义,也不方便重构和维护。 - 容易出错:如果查询结果的字段有变动,无法通过类型安全检查及时发现。
因此,推荐使用强类型的 POJO 类来映射查询结果,而不是直接使用 HashMap
或 Hashtable
。
不推荐的方式(使用 HashMap
)
<select id="selectUserById" resultType="java.util.HashMap">SELECT id, username, emailFROM usersWHERE id = #{id}
</select>
问题原因:
- 查询结果直接映射到
HashMap
,其中键是字段名(例如:id
、username
、email
),值是字段对应的值。HashMap
本身没有类型信息,这样做容易让程序员在处理数据时出错。 - 如果数据库字段发生变化,或者字段的名字与 POJO 类属性不同,可能会导致错误。
推荐的方式(使用 POJO 类)
正确的做法是使用 POJO 类(即普通 Java 对象)来映射查询结果,这样可以确保类型安全、提高代码可读性,并且在字段变化时也能更容易地进行维护。
创建 POJO 类
假设我们查询的是用户信息,可以创建一个 User
类。
@Data
public class User {private int id;private String username;private String email;
}
使用 resultType
或 resultMap
映射 POJO 类
在 MyBatis 的 XML 配置文件中,我们可以使用 resultType
或 resultMap
将查询结果映射到 User
类。
方法一:使用 resultType
<select id="selectUserById" resultType="User">SELECT id, username, emailFROM usersWHERE 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, emailFROM usersWHERE 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());
总结与最佳实践:
- 避免使用
HashMap
或Hashtable
作为查询结果:直接将查询结果映射到HashMap
或Hashtable
会丧失类型安全性,导致代码可维护性差,容易出错。 - 使用 POJO 类来接收查询结果:通过强类型的 POJO 类进行映射,可以确保类型安全和更好的代码可读性。
- 使用
resultMap
或resultType
映射查询结果:当数据库字段与 POJO 类属性一致时,可以直接使用resultType
;如果需要更复杂的字段映射,则使用resultMap
来显式指定字段与属性的映射关系。 - 类型安全的好处:
-
- 编译时检查:使用 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 |
| VARCHAR |
update_time | DATETIME |
每次更新用户记录时,我们希望同时更新 update_time
字段为当前时间。
更新 SQL 语句
在 MyBatis 中,我们可以通过 UPDATE
语句来更新记录,并且使用 NOW()
(MySQL 的当前时间函数)来更新 update_time
字段。
<update id="updateUser" parameterType="User">UPDATE usersSET username = #{username},email = #{email},update_time = NOW()WHERE id = #{id}
</update>
解释:
username
和email
是要更新的字段。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
BEGINSET 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 记录,增加存储和网络负担。
推荐做法:
- 只更新变动的字段:动态检查每个字段是否发生变化,如果发生了变化才执行更新操作。
- 使用
UPDATE
语句时避免不必要的字段更新。 - 提供灵活的更新接口:根据传入的参数只更新实际发生变化的字段。
手动检查字段变化(适用于简单的更新逻辑)
当 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_activeFROM usersWHERE 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"}
}
- 减少多次查询:如果在业务逻辑中经常需要
User
和Address
的完整数据,这种设计可以通过关联查询一次性获取,避免额外查询Address
表。
缺点:
- 复杂性增加:每次查询用户时,都会额外加载整个地址对象。如果地址信息不常用,可能会浪费内存和处理时间。
- 难以处理更新:如果需要更新
Address
的信息,修改User
对象可能会导致逻辑不清晰。例如,更新地址时需要确保级联保存。 - 潜在的性能问题:如果
User
表和Address
表之间是一对多的关系(一个地址可能对应多个用户),直接嵌套Address
对象可能会导致冗余数据。
嵌套对象设计中insert标签
在嵌套对象设计中,User
中包含一个嵌套的 Address
对象,因此在插入 User
时需要分别处理 Address
和 User
的数据。以下是 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 cityFROM t_user uLEFT JOIN t_address a ON u.address_id = a.idWHERE u.id = #{id};</select></mapper>
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Transactionalpublic void addUser(User user) {// 插入 Address,并回填 IDAddress address = user.getAddress();userMapper.insertAddress(address);// 设置 Address.id 到 User 对象中user.setAddress(address);// 插入 UseruserMapper.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
对象:
缺点:
- 违背面向对象思想:数据库中的外键在代码层面表现为单一字段,而不是完整的对象,丢失了领域模型中对象之间的关系。
- 额外查询需求:如果业务场景中需要频繁获取完整的
User
和Address
信息,就需要在代码中通过外键查询Address
:
User user = userRepository.findById(userId);
Address address = addressRepository.findById(user.getAddressId());
4.13.3. 推荐方案
如果业务场景需要频繁访问完整的 User
和 Address
信息:
- 保持当前设计,使用嵌套的
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规范》
相关文章:
Mybatis——Mybatis开发经验总结
摘要 本文主要介绍了MyBatis框架的设计与通用性,阐述了其作为Java持久化框架的亮点,包括精良的架构设计、丰富的扩展点以及易用性和可靠性。同时,对比了常见持久层框架,分析了MyBatis在关系型数据库交互中的优势。此外࿰…...
013:深度学习之神经网络
本文为合集收录,欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请参考这里。 深度学习是机器学习中重要的一个学科分支,它的特点就在于需要构建多层且“深度”的神经网络。 人们在探索人工智能初期,就曾设想构建一个用数学方式…...
Java 模板变量替换——字符串替换器(思路Mybatis的GenericTokenParser)
Java 模板变量替换——字符串替换器(思路Mybatis的GenericTokenParser) 思路字符串替换器 思路 模板变量替换无非是寻找出字符串(模板)中的特殊标记,用对应的变量进行字符串替换。 提到变量替换,大家第一能…...
蓝桥杯备考:数据结构之栈 和 stack
目录 栈的概念以及栈的实现 STL 的stack 栈和stack的算法题 栈的模板题 栈的算法题之有效的括号 验证栈序列 后缀表达式 括号匹配 栈的概念以及栈的实现 栈是一种只允许在一端进行插入和删除的线性表 空栈:没有任何元素 入栈:插入元素消息 出…...
Lambda离线实时分治架构深度解析与实战
一、引言 在大数据技术日新月异的今天,Lambda架构作为一种经典的数据处理模型,在应对大规模数据应用方面展现出了强大的能力。它整合了离线批处理和实时流处理,为需要同时处理批量和实时数据的应用场景提供了成熟的解决方案。本文将对Lambda…...
Vue.js组件开发,AI时代的前端新玩法
AI可不只是写写小说、聊聊天,现在它的触角已经伸到了程序员的代码世界里。特别是前端开发,很多人都在尝试用ChatGPT或者类似的AI工具来写代码,甚至直接生成Vue.js组件。有些人感叹,"写代码的时代是不是要结束了?&…...
标定 3
标定场景与对应的方式 标定板标定主要应用场景: (1)无法获取到执行机构物理坐标值,比如相机固定,执行机构为传送带等 (2)相机存在畸变等非线性标定情况,需要进行畸变校正 (3)标定单像素精度 (4)获取两个相机之间的坐标系关系 标定板操作步骤: (1)确定好拍…...
电商项目-基于ElasticSearch实现商品搜索功能(三)
本系列文章主要介绍基于 Spring Data Elasticsearch 实现商品搜索的后端代码,介绍代码逻辑和代码实现。 主要实现功能:根据搜索关键字查询、条件筛选、规格过滤、价格区间搜索、搜索查询分页、搜索查询排序、高亮查询。 主要应用技术:canal,…...
【51单片机】03 蜂鸣器-播放音乐
蜂鸣器-播放音乐 一、原理介绍1.硬件电路 二、练习1.让蜂鸣器发声2.尝试演奏小星星 一、原理介绍 蜂鸣器分为有源蜂鸣器、无源蜂鸣器两种。 有源蜂鸣器:施加合适的电压之后就会发出特定频率的声音 无源蜂鸣器:需要提供特定频率的声音信号,才能…...
MySQL 架构
MySQL架构 MySQL8.0服务器是由连接池、服务管理⼯具和公共组件、NoSQL接⼝、SQL接⼝、解析器、优化 器、缓存、存储引擎、⽂件系统组成。MySQL还为各种编程语⾔提供了⼀套⽤于外部程序访问服务器的连接器。整体架构图如下所⽰: MySQL Connectors:为使⽤…...
XML 解析器:深入解析与高效应用
XML 解析器:深入解析与高效应用 引言 XML(可扩展标记语言)作为一种重要的数据交换格式,被广泛应用于各种系统和平台中。为了有效地处理和解析XML数据,XML解析器发挥着至关重要的作用。本文将深入探讨XML解析器的原理…...
LabVIEW设计 IIR 滤波器
这是一个设计 IIR 滤波器的 LabVIEW 程序框图,其功能主要是用于设计滤波器并计算其频率响应,但它并不直接对输入的波形进行实时滤波,而是提供一个滤波器的频率响应分析工具。 以下是框图中各部分的详细解释: 1. 主要模块功能说明 …...
基于改进粒子群优化的无人机最优能耗路径规划
目录 1. Introduction2. Preliminaries2.1. Particle Swarm Optimization Algorithm2.2. Deep Deterministic Policy Gradient2.3. Calculation of the Total Output Power of the Quadcopter Battery 3.OptimalEnergyConsumptionPathPlanningBasedonPSO-DDPG3.1.ProblemModell…...
AI刷题-数列推进计算任务、数组中的幸运数问题
目录 一、数列推进计算任务 问题描述 测试样例 解题思路: 问题理解 数据结构选择 算法步骤 优化思路 最终代码: 运行结果: 二、数组中的幸运数问题 问题描述 测试样例 解题思路: 问题理解 数据结构选择 算法步…...
微服务的配置共享
1.什么是微服务的配置共享 微服务架构中,配置共享是一个重要环节,它有助于提升服务间的协同效率和数据一致性。以下是对微服务配置共享的详细阐述: 1.1.配置共享的概念 配置共享是指在微服务架构中,将某些通用或全局的配置信息…...
【计算机网络】窥探计网全貌:说说计算机网络体系结构?
标签难度考察频率综合题⭐⭐⭐60% 这个问题在计算机网络知识体系中是一个比较重要的问题,只有完整地了解计算机网络的体系结构才能清晰地认识网络的运行原理。 在回答这个问题时,笔者认为有几个比较重要的点: 首先一定要分清楚前置条件&am…...
【MySQL】DATEDIFF()函数使用
DATEDIFF 函数用于计算两个日期之间的差值,以天为单位 DATEDIFF 函数返回一个整数,表示 date1 和 date2 之间的天数。如果 date1 在 date2 之前,结果为负数;如果在 date2 之后,结果为正数;如果相等…...
计算机网络学习笔记
第1课 绪论、传输介质 【知识点回顾】 两种导线可以减小电磁干扰: 双绞线(分为非屏蔽双绞线、屏蔽双绞线)(RJ-45用)同轴电缆(短距离使用)网络通信的基本单位:位(bit&…...
Spring Boot性能提升的核武器,速度提升500%!
虚拟线程是 Java 21 引入的一个新特性,用于简化并发编程。它与传统的操作系统线程相比,具有显著的优势: 轻量级:虚拟线程由 JVM 管理,而非操作系统,因此它们的内存占用和创建成本远低于传统线程。理论上&am…...
zig 安装,Hello World 示例
1. 安装 Zig 首先,你需要在你的计算机上安装 Zig 编译器。你可以从 Zig 官方网站 下载适合你操作系统的版本。 安装完成后,你可以在终端中运行以下命令来检查 Zig 是否安装成功: zig version如果一切正常,它会显示 Zig 的版本信…...
【数据库系统概论】第5章 数据库完整性【!触发器】
目录 5.1数据库完整性概述 5.2 实体完整性 5.3 参照完整性 5.4 用户定义的完整性 属性上的约束 1. 列值非空(NOT NULL) 2. 列值唯一(UNIQUE) 3. 检查列值是否满足条件(CHECK) 元组上的约束 5.5 完…...
Linux中通过frp实现内网穿透
1、准备工作 准备一台公网服务器(云服务器),推荐阿里云或者腾讯云都可以 需要下载好frp安装包Linux端的和Windows端的安装包 网址:Releases fatedier/frp (github.com)https://github.com/fatedier/frp/releases 2、下载frp_0…...
Vscode辅助编码AI神器continue插件
案例效果 1、安装或者更新vscode 有些版本的vscode不支持continue,最好更新到最新版,也可以直接官网下载 https://code.visualstudio.com/Download 2、安装continue插件 搜索continue,还未安装的,右下脚有个Install,点击安装即可 <...
上海亚商投顾:沪指探底回升微涨 机器人概念股午后爆发
上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 市场全天探底回升,沪指盘中跌超1.6%,创业板指一度跌逾3%,午后集体拉升翻红…...
LeetCode 3297.统计重新排列后包含另一个字符串的子字符串数目 I:滑动窗口
【LetMeFly】3297.统计重新排列后包含另一个字符串的子字符串数目 I:滑动窗口 力扣题目链接:https://leetcode.cn/problems/count-substrings-that-can-be-rearranged-to-contain-a-string-i/ 给你两个字符串 word1 和 word2 。 如果一个字符串 x 重新…...
ssm旅游攻略网站设计+jsp
系统包含:源码论文 所用技术:SpringBootVueSSMMybatisMysql 需要源码或者定制看文章最下面或看我的主页 目 录 目 录 III 1 绪论 1 1.1 研究背景 1 1.2 目的和意义 1 1.3 论文结构安排 2 2 相关技术 3 2.1 SSM框架介绍 3 2.2 B/S结构介绍 3 …...
前端学习-环境this对象以及回调函数(二十七)
目录 前言 目标 环境对象 作用 环境对象this是什么? 判断this指向的粗略规则是什么? 回调函数 目标 常见的使用场景 综合案例:Tab任务栏切换 总结 前言 男儿何不带吴钩,收取关山五十州 目标 能够分析判断函数运行在不…...
计算机网络-数据链路层(虚拟局域网VLAN)
2.6 虚拟局域 2.6.1 虚拟局域网概述 以太网交换机连接的各个网络同属于一个广播域,随着以太网的规模扩大,广播域也会相应的扩大,巨大的广播域会带来巨大的弊端。 广播风暴 难以治理 潜在的安全问题 TCP/IP协议下会进行广播的协议:…...
Python贪心
贪心 贪心:把整体问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直至所有步骤结束;每个步骤不会影响后续步骤核心性质:每次采用局部最优,最终结果就是全局最优如果题目满足上述核心性质…...
CSS 盒模型
盒模型 CSS盒模型是网页布局的核心概念之一,它描述了网页元素的物理结构和元素内容与周围元素之间的关系。根据W3C规范,每个HTML元素都被视为一个矩形盒子,这个盒子由以下四个部分组成: 内容区(Content areaÿ…...
【linux】vi编辑文件及readonly文件修改读写权限方法
板端vi修改文件: 1、vi 文件路径 vi mnt/eol/config/oem_eol.xml2、按 i进入修改状态,此时可以修改配置文件 3、按 esc退出修改状态,并按"wq!保存 问题:readonly文件无法直接vi修改 方案: 1、mount -o remoun…...
Git使用笔记
Git 版本控制 一、Git 介绍二、Git 使用1. 安装及配置2. 使用方法3. Git 命令3. 历史版本回退4. 分支 (Branch) 三、远程仓库1. SSH公钥连接Gitee2. 推送到远程仓库 一、Git 介绍 常见版本控制软件:集中式(CVS、SVN),分布式&#…...
mermaid大全(语法、流程图、时序图、甘特图、饼图、用户旅行图、类图)
⚠️ 有些网站的mermaid可能不完整,因此下面教程中可能有些语法是无效的。 😊亲测Typora软件均可以显示。 1. 介绍 Mermaid是一个基于JavaScript的图表绘制工具,它使用类似Markdown的语法来创建和修改各种类型的图表。以下是关于Mermaid的详…...
慧集通(DataLinkX)iPaaS集成平台-业务建模之业务对象(二)
3.UI模板 当我们选择一条已经建好的业务对象点击功能按钮【UI模板】进入该业务对象的UI显示配置界面。 右边填写的是UI模板的编码以及对应名称;菜单界面配置以业务对象UI模板编码获取显示界面。 3.1【列表-按钮】 展示的对应业务对象界面的功能按钮配置࿱…...
vue3+ts+element-plus 输入框el-input设置背景颜色
普通情况: 组件内容: <el-input v-model"applyBasicInfo.outerApplyId"/> 样式设置: ::v-deep .el-input__wrapper {background-color: pink; }// 也可以这样设置 ::v-deep(.el-input__wrapper) {background-color: pink…...
python迷宫寻宝 第6关 安全策略
地图: 1、体力不足去找终点,体力足则原地不动 import api## 判断是否需要离场的函数 # 体力足返回False,体力不足返回True def should_leave():# 拿到我离终点的距离e_row api.get.exit(what"row")e_col api.get.exit(what"…...
【计算机网络】lab7 TCP协议
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀计算机网络_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目录 1. 实验目的…...
Monorepo设置:新手指南
Monorepo是一种项目代码管理方法,指在单个代码仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署的复杂性,并提供更好的可重用性和协作性。 简单理解:所有项目都在一个代码仓库中 📦,但这并不意…...
HTTP 请求与响应的结构
一、引言 在当今数字化的时代,网络通信如同空气一般无处不在,而HTTP协议则是网络世界中最为重要的基石之一。当我们在浏览器中输入一个网址,轻松浏览网页、观看视频、下载文件或是进行在线购物等操作时,背后HTTP协议都在默默地发…...
计科高可用服务器架构实训(防火墙、双机热备,VRRP、MSTP、DHCP、OSPF)
一、项目介绍 需求分析: (1)总部和分部要求网络拓扑简单,方便维护,网络有扩展和冗余性; (2)总部分财务部,人事部,工程部,技术部,提供…...
Soildworks的学习【2025/1/12】
右键空白处,点击选项卡,即可看到所有已调用的选项卡: 点击机械小齿轮选项卡,选择文档属性,选择GB国标: 之后点击单位,选择MMGS毫米单位: 窗口右下角有MMGS,这里也可以选择…...
ORACLE-表空间和分区控制
--查询最后更新的统计信息时间 SELECT table_name, last_analyzed FROM dba_tables WHERE table_name 表名; --更新统计信息 -----按分区 BEGIN DBMS_STATS.GATHER_TABLE_STATS( ownname > XI_SF, -- 模式名称 tabname > 表名, -- 表名称 partnam…...
C# 与 Windows API 交互的“秘密武器”:结构体和联合体
一、引言 在 C# 的编程世界里,当我们想要深入挖掘 Windows 系统的底层功能,与 Windows API 打交道时,结构体和联合体就像是两把神奇的钥匙🔑 它们能够帮助我们精准地操控数据,实现一些高级且强大的功能。就好比搭建一…...
【数字化】华为-用变革的方法确保规划落地
导读:华为在数字化转型过程中,深刻认识到变革的必要性,并采用了一系列有效的方法确保转型规划的有效落地。华为认为,数字化转型不仅仅是技术层面的革新,更是企业运作模式、流程、组织、文化等深层次的变革。数字化转型…...
SpringData-Redis缓存
Spring Framework是领先的全堆栈Java/JEE应用程序框架。它提供了一个轻量级容器和一个通过使用依赖注入、AOP和可移植服务抽象实现的非侵入性编程模型。 NoSQL存储系统为传统RDBMS提供了一种横向可扩展性和速度的替代方案。就实现而言,键值存储代表NoSQL空间中最大…...
大语言模型兵马未动,数据准备粮草先行
从OpenAI正式发布ChatGPT开始,大型语言模型(LLM)就变得风靡一时。对业界和吃瓜群众来说,这种技术最大的吸引力来自于理解、解释和生成人类语言的能力,毕竟这曾被认为是人类独有的技能。类似CoPilot这样的工具正在迅速…...
跳表和Mysql联合索引的最左原则和索引下推的优化
文章目录 跳表(Skip List)关键特性跳表的结构示意图跳表的查询效率为什么 MySQL 不使用跳表而使用 B 树?跳表的实际应用场景 总结 MySQL 联合索引的最左匹配原则最左匹配原则的规则示例:创建联合索引查询示例及索引使用情况设计联…...
Android切换语言不退出App
1.需求 实现用户选择语言(未点击下一步),更新当前界面UI,点击下一步后,更新App的语言,并进行保存。 实现目标: 1.设置App的语言,本地进行保存 2.updateResources更新本地语言配置…...
Unity编程与游戏开发-编程与游戏开发的关系
游戏开发是一个复杂的多领域合作过程,涵盖了从创意构思到最终实现的多个方面。在这个过程中,技术、设计与美术三大核心要素相互交织,缺一不可。在游戏开发的过程中,Unity作为一款强大的跨平台游戏引擎,凭借其高效的开发工具和庞大的社区支持,成为了很多游戏开发者的首选工…...
【Git】问题汇总
在push的时候显示 protocol error: bad line length 8192 我在本地创建了一个gogs服务器,现在正在上传代码,但是出现了上述的这个问题。 解决方法 设置本地http.postBuffer(待验证) 方法一:全局配置 git config --g…...