MyBatis
快来分享你的内容吧~
- 2025-12-06·Java后端本文对 Java 的持久层历史做一个简单梳理,解释了为什么会有 MyBatis 和 JPA 两种技术, 并介绍 JPA 和 Spring Data JPA 基本概念和用法。查看全文加油鸭:梳理得这么清晰透彻,真是技术分享的典范!对Java持久层历史的深入解读特别有帮助,感谢你的用心整理!18321分享
- 2025-09-15·Java后端
- 2025-05-16·Java后端
- 2024-12-08·GitHub:https://githu
- 2024-12-02·Java后端大家没有遇到过这种问题,安装MybatisX插件后,mapper图标确实变了但是,连接好数据库后右键数据库表没有MybatisX-Generator选项查看全文Asensio:我也一样 还不知道怎么解决 楼主解决了吗😇580分享
MyBatis-Plus 分页查询报错 BadSqlGrammarException:SELECT COUNT() 异常排查与解决
### 1. 现象描述 在进行优惠券业务开发时,调用分页查询接口 `/coupons/page` 突然抛出 `BadSqlGrammarException`。查看后台日志,发现 MyBatis-Plus 自动生成的 `COUNT` 语句出现了明显的语法错误: **报错核心信息:** ```text 根本原因:SQL 语法错误:SELECT COUNT() FROM coupon 中 COUNT() 缺少参数 数据库报错位置:near ') FROM coupon' at line 1 技术栈:Spring Boot + MyBatis-Plus (3.4.3) + MySQL ``` 正常的统计语句应该是 `SELECT COUNT(*)` 或 `SELECT COUNT(1)`,但程序生成的 SQL 括号内空空如也,导致 MySQL 解析失败。 --- ### 2. 原因分析 经过对代码和 MyBatis-Plus 源码逻辑的排查,该问题主要由以下两个因素共同导致: #### 2.1 MyBatis-Plus Count SQL 优化失败 MyBatis-Plus 在进行分页查询时,默认会开启 `optimizeCountSql`(Count 优化)。它会尝试通过 `jsqlparser` 解析原始 SQL,将其转化为更高效的统计语句。 然而,在 **MyBatis-Plus 3.4.3** 版本中,当实体类(PO)中包含特殊的字段映射(如使用了反引号包裹的保留字 `` `name` ``、`` `specific` ``)时,优化器可能无法正确推断出统计参数,从而生成了非法的 `COUNT()`。 #### 2.2 实体类中的保留字冲突 在项目的Coupon类po中可以看到,表中多个字段使用了 MySQL 关键字: ```java @TableField("`name`") private String name; @TableField("`specific`") private Boolean specific; ``` 虽然在 SQL 中使用了反引号规避,但在复杂的自动生成场景下,这增加了解析器出错的概率。 --- ### 3. 解决方案 #### 禁用 Count 优化 最直接且稳妥的解决办法是针对该分页请求禁用 Count 优化。禁用后,MyBatis-Plus 将不再尝试简化统计 SQL,而是使用最通用的 `SELECT COUNT(*) FROM (原查询SQL) AS total` 方式进行统计。 --- ### 4. 代码实现 以下是重构后的分页查询逻辑: ```java @Override public PageDTO<CouponPageVO> pageQuery(CouponQuery query) { // 1. 构建分页对象 Page<Coupon> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 2. 关键修复:针对某些场景下 COUNT() 生成非法 SQL 的问题,手动禁用 count 优化 page.setOptimizeCountSql(false); // 3. 构建查询条件 String name = query.getName(); Integer type = query.getType(); Integer status = query.getStatus(); LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), Coupon::getName, name) .eq(type != null, Coupon::getDiscountType, type) .eq(status != null, Coupon::getStatus, status); // 4. 执行分页查询 this.page(page, wrapper); // 5. 封装结果返回 if (CollUtils.isEmpty(page.getRecords())) { return PageDTO.empty(page); } return PageDTO.of(page, CouponPageVO.class); } ``` --- ### 5. 总结 1. **谨慎对待关键字**:数据库设计时应尽量避开 `name`, `status`, `specific` 等关键字。若必须使用,务必在实体类映射时加上反引号。 2. **版本避坑**:MyBatis-Plus 3.4.x 系列的 Count 优化在某些复杂场景下确实存在不稳定性。如果遇到 `BadSqlGrammarException` 且指向 Count 语句,优先考虑通过 `page.setOptimizeCountSql(false)` 绕过。
Spring进阶 - JPA持久层
本文对 Java 的持久层历史做一个简单梳理,解释了为什么会有 **MyBatis 和 JPA 两种技术,** 并介绍 JPA 和 Spring Data JPA 基本概念和用法。 下面让我们回到什么框架都没有的 JDBC 时代,从大的时间线 **快速回顾** Java持久层历史演变。 Java 的数据库访问层技术演变 ================ 在 Java 世界里,对象(Object)生活在内存中,关系(Relation)生活在数据库里。它们就像两个语言不通的物种,而持久层框架,就是那个尽力让双方听懂彼此的翻译官 。  核心的持久层技术有: 1. **JDBC:**最低抽象层,所有 SQL 与资源管理由开发者手工处理。 2. **MyBatis:**将 SQL 控制权交给开发者,通过映射机制减少大量模板代码。 3. **JPA(以 Hibernate 为代表):**由框架自动推断 SQL,通过 ORM 屏蔽表结构细节。 这三类技术并不是彼此替代,而是**在不同业务复杂度、团队偏好和性能要求下共存**。 #### 1、JDBC - 让Java能够访问任何第三方数据库 在 JDBC 诞生之前,不同的数据库厂商协议不一致 ,Java程序无法使用一套接口(同一套代码)统一操作各种第三方数据库,于是在1997年, Sun 公司提出 JDBC 规范,由 Java 提供一套接口,第三方数据库自己写驱动,实现接口对接 Java 的 JDBC 。 JDBC 的缺点非常多: * 连接很麻烦,样板代码多。 * 硬编码 SQL ,业务代码和 SQL 语句混合。 * 从数据库查出来的数据,需要手动转成 Java 列表。 那个年代有聪明的程序员为了减少样板代码写了多种工具类,但并没有达到理想的状态,重复工作依然存在。 #### 2、 Hibernate 自动化的持久层框架 2001 年,Gavin King 抛出了 **Hibernate, 它的核心理念是:程序员应该操作对象,而不是操作数据库表。** **Hibernate** 让开发者仅编写 Java 代码,可以根据对象自动生成 SQL ,简化了对数据库的操作,减少了开发者的重复劳动。 缺点: * 自动生成的 SQL 往往性能不可控(著名的 N+1 问题) * 由于是自动生成SQL, 对于复杂的查询显得力不从心。 #### 3、 iBATIS 崛起 后来改名 MyBatis Hibernate 让开发者尝到了甜头,掀起一场革命,但它的缺点也是致命的, 在处理复杂 SQL 时不够灵活 ,于是另一个持久层框架 iBATIS 便诞生(大约是2002年),跟 Hibernate 设计理念不同,iBATIS 并不是要在代码中消灭所有的 SQL 语句,而是认为**SQL 是数据库的灵魂,不应该被隐藏,而应该被管理。** 所以它在业界被称为 **半自动化ORM**, 它不负责帮你生成 SQL(虽然现在也可以),它只负责**把 SQL 和 Java 代码解耦。** 代码语法来到了我们熟悉的样子 Mapper接口: ```java public interface UserMapper { User selectUserById(Long id); } ``` XML 配置 : ```java <select id="selectUserById" resultType="com.example.User"> SELECT id, name, age FROM users WHERE id = #{id} </select> ``` 半自动化胜在灵活可控,SQL 语言全由开发者手动编写,并且与 Java业务分离,独立存在 XML 配置中。 ##### 防止SQL注入的原理 **#{} 可以防止SQL注入。** 原理是 XML 中写的: ```java SELECT * FROM user WHERE name = #{name} ``` MyBatis 会生成: ```java SELECT * FROM user WHERE name = ? ``` 然后立刻给数据库编译,所有主流数据库(MySQL、PostgreSQL、Oracle、SQL Server)都支持带 `?` 的预编译语句 之后 MyBatis 调用 JDBC 的 API ,发送参数给数据库: ```java preparedStatement.setString(1, name); ``` 也就是说,分为两阶段发送: * **数据库预编译**:SQL 模板先传给数据库编译 * **数据库执行阶段**:JDBC 发送参数 这就是 **JDBC PreparedStatement** 的防注入原理。 这样处理之后,随便用户传:name = "xxx' OR '1'='1", 最终SQL语句是: ```java SELECT * FROM user WHERE name = "xxx' OR '1'='1" ``` 不会把 OR 解析成关键字。 #### 4、 JPA - Java 官方的 ORM 规范 全自动框架不止 Hibernate 一个,从2002年到2006年发展了多个全自动持久层框架,包括: Hibernate、 EclipseLink、Java 官方的 EJB 框架(做的很失败),让持久层框架的使用变得混乱(但跟 Mybatis 无关,Mybatis并非 ORM 框架,自成一派)。 于是,在2006年,Java官方(Sun 公司)提出 JPA 规范。 **JPA 的本质不是一个框架,而是一套接口规范** JPA的出现是为了统一 Hibernate、EclipseLink 的使用方式,并带来了语法便利。也就是说 JPA 本身是规范,底层实现可以选用 Hibernate 或 EclipseLink 实现具体能力。 #### 5、 当下的 Java 持久层框架的两大派系 * MyBatis 发展到如今,有 MyBatis-plus , MyBatis-flex 被主流使用,**这一类属于SQL映射框架** * JPA 发展到如今, **Spring Data JPA** 被主流使用,**这一类属于 ORM 框架** 对这两个技术的定位: **Spring Data JPA** : 基于 JPA 封装的规范,兼容 Spring 生态,其底层默认由 Hibernate 实现 ORM 功能。 **JPA/Hibernate 的核心原理是:** 根据开发者定义的实体类、字段映射、关联关系以及方法命名规则,由框架自动推导并生成 SQL。 同时,JPA 还支持 JPQL、Criteria API、Native SQL 等方式,让开发者在需要时可以手动编写 SQL。 本质上,JPA/Hibernate 是一个完整的 **ORM 引擎**,以“对象模型”为中心,负责对象到关系数据库之间的自动转换。 **MyBatis-Plus** :MyBatis-Plus 通过预置的模板 SQL、Wrapper 条件构造器、CRUD 自动生成器等,一定程度上减少了 MyBatis 中手写 SQL 的工作量。 但其核心依然是 **模板化 SQL + 映射**,框架并不理解实体关系,也不会根据关联规则自动生成 SQL,复杂查询仍需开发者手写 SQL。 两者在功能上是非常像的,但 MyBatis 不是 ORM 框架。 在开发的体验上,能够明显感受到 JPA 以对象为中心,MyBatis 以 「SQL 构造」和「条件注入」为中心。 在**全球范围**,Spring Data JPA 是绝对的主流; 在**国内**,则是MyBatis-Plus 主流。 JPA === 介绍 -- JPA(Java Persistence API) 是 Java 官方发布的一套 ORM 规范 ,负责 * 要怎么把 Java 对象映射成数据库表 * 怎么维护外键关系(1对多、多对多) * 怎么查询 * 怎么管理增删改查生命周期 但只负责一套约束,底层的具体实现代码由三个主流框架提供: * **Hibernate(最主流)** * EclipseLink * OpenJPA 在 Spring Boot 环境中,**默认的 JPA 实现就是 Hibernate**。 **核心理念**: 把面向对象世界(对象、引用、继承、聚合)的模型,映射到关系数据库世界(表、行、列、外键、连接)的技术与模式集合。 也就是说你只需要写: ```java User user = new User(); user.setName("张三"); entityManager.persist(user); ``` 底层会自动生成 SQL,例如: ```sql insert into user (name) values ('张三'); ``` 映射具体包含了: * **类 <——>表** * **标识(Identity)映射** : Java 对象的唯一标识(主键)要和数据库的主键对应 (@Id) * **值类型映射** : Java 的数据类型到数据库的数据类型映射,如 String, int, LocalDateTime 映射为 varchat 、int 、DateTime。 * **事务边界与一致性** EntityManager 机制 ---------------- ### 介绍 Java 对象拥有复杂的生命周期(新建、托管、游离、删除),而关系型数据库只有简单的 CRUD 操作。两者之间缺乏自动的映射机制,这在电路中叫做 阻抗失配 ,即 两个电路连接时阻抗不匹配,会导致信号反射或能量损耗 。 简单来说,java对象有生命周期(创建,更新,销毁),而在 MySQL 等数据库的 「记录」没有生命周期的概念,对象是"活的"(动态)数据载体,数据库是"死的"(静态)数据载体。 **Hibernate 的解决方案:** 引入 `EntityManager` 作为 **ORM 的核心调度器**, 作用是充当生命周期的过渡适配,Java对象更新后,理论上Java对象和数据库记录不一致,此时 Java对象的数据处于脏状态,`EntityManager`会在合适的时机自动刷新到数据库,保证数据一致性。 在 Java 代码层面,它是一个接口 (`javax.persistence.EntityManager`)。它的职责类似“缓存”: 1. **状态管理**:它负责维护实体对象在内存中的四种状态(New, Managed, Detached, Removed)。 2. **脏检查(Dirty Checking)**:它利用内部的一级缓存机制,自动比对对象的前后状态。 3. **持久化上下文**: 持久化上下文是一块“一级缓存” , 在同一事务内重复读取不会重复查询数据库 ,它负责决定在何时、以何种顺序将内存中的变化生成 SQL 语句并发送给数据库,所以具有 自动脏检查(dirty check) 能力。 ```java @Transactional public void test() { User u1 = em.find(User.class, 1L); // SQL 1 次 User u2 = em.find(User.class, 1L); // 不触发 SQL(一级缓存) } ``` ### EntityManager 具体作用 #### 1、状态管理(对象生命周期管理) **四种状态:新建(new) 、托管(Managed)、游离(Detached)、删除态 (Removed)** 1. **新建:** Java 对象刚刚是 `new` 出来,没有 ID (主键是空),此时 EntityManager 根本不管。 2. **托管**: 有 ID , EntityManager 正在监控它 ,会触发藏检查, 只要你改了 Java 对象的属性(`setName`),就算你不喊保存,事务提交时,EntityManager 也会**自动**向数据库发 `UPDATE` 语句。 以下三种场景从 新建 变成 托管 ```java // 场景 A:通过 persist 让 New 变成 Managed em.persist(user); // 场景 B:通过 find 从数据库查出来的,天生就是 Managed User user = em.find(User.class, 1L); // 【关键】修改属性,自动同步数据库!不需要写 em.update(user) user.setName("李四"); ``` 3. **游离**:还是有ID, **但EntityManager 不再监控它了**(可能是 Session 关闭了,或者被踢出了缓存)。 此时 你改了对象的属性,EntityManager 假装看不见,数据库**不会**发生任何变化。 简单来说,就是拿到数据,连接关闭,此时对象就是纯粹的 **数据载体。** ```java User user = em.find(User.class, 1L); // 此时是 Managed em.clear(); // 清空上下文,把所有对象踢出管理 // 或者 em.close(); // 关闭连接 // 此时 user 变成了 Detached。 // 虽然你改了名字,但数据库里还是“李四”,不会变。 user.setName("王五"); ``` 4. **删除态**: 有 ID ,**EntityManager 标记它为“待删除”**。 事务提交时,物理删除数据库记录。 ```java User user = em.find(User.class, 1L); em.remove(user); // 标记删除 // 此时 user 对象在 Java 内存里其实还在!你可以打印它。 // 但事务提交后,数据库里就没了。 ``` **那逻辑删除怎么实现?** JPA 默认没有逻辑删除,可以由 Hibernate 偷梁换柱实现。代码如下: ```java import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; @Entity @Table(name = "t_user") // 1. 拦截删除:告诉 Hibernate,当有人调用 remove 时,别执行 DELETE,执行这句 UPDATE @SQLDelete(sql = "UPDATE t_user SET deleted = 1 WHERE id = ?") // 2. 拦截查询:告诉 Hibernate,无论查什么,都在 WHERE 后面自动拼上这句条件 @Where(clause = "deleted = 0") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 逻辑删除标识:0-未删,1-已删 @Column(name = "deleted") private Integer deleted = 0; } ``` #### 2、事务控制和延迟操作 * 它可以管理事务边界,确保一系列对象操作要么全部成功,要么全部回滚。 * 支持延迟加载(Lazy Loading),比如一对多关联对象,访问时才查询数据库,提高性能。 ```java @Service public class UserServiceManual { @PersistenceContext private EntityManager entityManager; public void createUser(User user) { // 获取事务 EntityTransaction transaction = entityManager.getTransaction(); try { // 手动开启事务 transaction.begin(); // 执行数据库操作, 这个user准备被持久化到数据库 // JPA 会在事务提交时生成对应的 INSERT SQL entityManager.persist(user); // 提交事务 transaction.commit(); } catch (Exception e) { // 异常回滚 if (transaction.isActive()) { transaction.rollback(); } throw e; } } } ``` 事务可以替换为 Spring 注解事务: ```java @Service public class UserServiceTransactional { @Autowired private UserRepository userRepository; // Spring Data JPA Repository @Transactional public void createUser(User user) { // 直接操作对象即可 userRepository.save(user); // 如果这里抛出异常,事务会自动回滚 // 无需手动管理 commit 或 rollback } } ``` ### EntityManager 四大核心方法 1. persist() 把 new 的对象变成 managed(持久化态) ```java User user = new User(); user.setName("Tom"); em.persist(user); ``` 2. merge() : merge 用于将 **detached(脱管态)** 的实体重新合并回持久化上下文。 * merge 不会让传入对象变成 managed * merge 会复制一个新的对象并返回新的 managed 实体 ```java User user = em.find(User.class, 1L); transaction.commit(); user.setName("Tom2"); // u2 变成 managed User u2 = em.merge(user); ``` 3. remove() : 标记实体为 removed, 不是立即 DELETE , 在事务提交时删除记录 . ```java User user = em.find(User.class, 1L); // 不会删除 Java 内存中的对象,但一般在方法内创建的对象, // 线程离开方法后,对象是【不可达】状态,下一次GC会回收 em.remove(user); ``` remove 必须传入一个 managed 实体,否则报错。 4. refresh(): 把实体从数据库的最新数据“重置”回来 (覆盖内存中的属性) ```java User user = em.find(User.class, 1L); user.setName("Wrong"); em.refresh(user); ``` ### JPQL 对象化查询语言 JPQL(Java Persistence Query Language)是 JPA 的查询语言,用**实体类和属性名**写查询,而不是数据库表名和字段名。 * `User` → 实体类名,而不是 `user` * `u.name` → 实体字段名,而不是数据库 column 名 SQL 语句和 JPQL 语句对比 | 概念 | SQL | JPQL | | --- | --- | --- | | 查询对象 | 表 t_user | 实体 User 字段 | u_age | age(实体属性) 表别名 | u(可选) | u(必须) | **JPQL 语言规范本身要求“实体必须带别名”** 1. String 拼接的 JPQL , 最基础、最常用 ```java String jpql = "SELECT u FROM User u WHERE u.age > :age"; List<User> result = em.createQuery(jpql, User.class) .setParameter("age", 20) .getResultList(); ``` `:age`解释: * 这里 `:age` 意味着 ,这个值之后由 Java **动态赋值** * `:age` 的名字是 `"age"` * `.setParameter("age", 20)` 表示把 20 绑定进去 **2. 在实体类上定义查询** ```java @Entity @NamedQuery( name = "User.findByAge", query = "SELECT u FROM User u WHERE u.age > :age" ) public class User { ... } ``` ```java List<User> users = em.createNamedQuery("User.findByAge", User.class) .setParameter("age", 20) .getResultList(); ``` 3. **Criteria API(类型安全 Query)** 不写字符串 JPQL,**类型安全**, 多用于复杂查询、动态查询 , 用得最多的是大企业项目或动态查询场景, ```java CriteriaBuilder cb = em.getCriteriaBuilder(); // CriteriaQuery 作用和 MyBatis-Plus 的 QueryWrapper 几乎一样 // 都是查询构造器 CriteriaQuery<User> cq = cb.createQuery(User.class); Root<User> root = cq.from(User.class); cq.select(root).where(cb.gt(root.get("age"), 20)); List<User> list = em.createQuery(cq).getResultList(); ``` 等价于: ```java SELECT u FROM User u WHERE u.age > 20 ``` 4. **Native SQL(完全手写 SQL)** 完全手搓SQL, 不受 JPQL 语法限制 ,**有SQL 注入风险** ```java // 风险操作 String sql = "SELECT * FROM user WHERE name = '" + input + "'"; em.createNativeQuery(sql).getResultList(); ``` 安全操作 setPoarameter ```java String sql = "SELECT * FROM user WHERE age > ?"; List<User> users = em.createNativeQuery(sql, User.class) .setParameter(1, 20) .getResultList(); ``` 后续会讲到Spring Data JPA 的语法,这里一块汇总了,不怕赘述,就怕学不会。 5. **Spring Data JPA 的 JPQL 方式** Spring 生态绝对主流,非常常见,没有SQL注入的风险,第一种方式 JPQL 字符串有可能导致 SQL 注入风险。 ```java @Query("SELECT u FROM User u WHERE u.age > :age") List<User> findOlderThan(@Param("age") int age); ``` 6. **通过方法名自动推断查询** 不用写 JPQL,Spring 根据方法名自动生成,所以可能方法名很长。 ```java List<User> findByAgeGreaterThan(int age); ``` JPA 注解体系 -------- 在 JPA 规范中,实体类的行为是完全由注解驱动的。注解控制实体如何映射到表、字段、主键策略、枚举、二进制数据存储等。 ### 基本注解 核心注解列表 : * **@Entity** * **@Table** * **@Id** * **@GeneratedValue** * **@Column** * **@Transient** * **@Enumerated** * **@Lob** 1. `**@Entity**` (必须) , 告诉 JPA “这是一个实体,请帮我管理” , 类必须有无参构造方法。 ```java @Entity public class User { } ``` 2. `**@Table**` (可选,但推荐) , 如果类名叫 `User`,不加这个注解,默认表名也是 `User`。数据库通常用下划线命名(`t_user`),所以需要它。 ```java @Entity @Table(name = "t_user") public class User { } ``` 3. `**@Id**` (必须) , 指定谁是主键。 4. `**@GeneratedValue**` (核心) , 指定主键的生成策略,常见策略: * `GenerationType.IDENTITY`:数据库自增(MySQL 常用) * `GenerationType.SEQUENCE`:序列(PostgreSQL / Oracle 使用) * `GenerationType.AUTO`:根据数据库自动选择 * `GenerationType.UUID`(在新版 Hibernate 中已支持) ```java @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; ``` 5. `**@Column**` (最常用) , 细粒度控制列的属性。 * `name`:列名 * `nullable`:是否允许为 NULL(默认 true) * `length`:VARCHAR 长度(默认 255) * `unique`:是否创建唯一约束 ```java @Column(name = "username", nullable = false, length = 50, unique = true) private String username; ``` 6. `**@Transient**` (重要) , 指定字段不写入数据库,不参与 ORM 映射。 **使用场景:** * DTO 生成的临时字段 * 缓存某些计算结果 * 不需要被数据库持久化的业务属性(如 token) 7. `**@Enumerated**` , 指定枚举在数据库中的存储方式。 **两种模式:** * `EnumType.STRING`:把枚举值转成字符串 优点:可读、安全 缺点:占用空间稍大 * `EnumType.ORDINAL`:(禁用,全是坑)存储枚举的 ordinal(序号) 缺点:绝不能乱改枚举定义顺序!否则全表数据错位 ```java @Enumerated(EnumType.STRING) private Status status; ``` 8. @Lob ,告诉 JPA 将字段映射为大对象类型(LOB) * 字符串 → CLOB(长文本) * 字节数组 → BLOB(二进制) ```java @Lob private String largeText; @Lob private byte[] fileContent; ``` ### 进阶注解 做项目不可能只有一张表,只要有表关联,就逃不掉下面这些。这也是 JPA 比 MyBatis 难的地方。 1. **关系映射:**`**@OneToMany**` **/** `**@ManyToOne**` * **关系的“拥有方(owning side)”决定外键在哪张表上**。在一对多关系中,通常由“多”的一方(`Order`)持有外键 `user_id`,因此 `Order` 是 owning side。 * 在 JPA 的双向关系中,**只能一个 side 为 owning side**。另一个 side 使用 `mappedBy` 指向 owning side 的属性名(不是列名)。 有两个关键属性: **FetchType.LAZY** : 懒加载 查询主实体时,不查关联。只有你 **第一次访问关联字段时** 才去查。用来解决表关系太多的 SQL 操作导致性能极低问题, 不需要的数据不要查,访问才查。 **最好所有关联默认用懒加载** **FetchType. EAGER** : 立即把关联实体一起查出来 **数据库表创建了外键才需要这个注解,否则不用。** ```java @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 100) private String name; // mappedBy 指向 Order 类中定义的 user 字段名 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); // helper method 保持双向一致性 public void addOrder(Order o) { orders.add(o); o.setUser(this); } public void removeOrder(Order o) { orders.remove(o); o.setUser(null); } // getters/setters... } ``` ```java @Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) // owning side, 指定外键列 private User user; private BigDecimal amount; // getters/setters... } ``` 2. **并发控制 乐观锁 @Version** 在表上维护一个 `version` 列(`int` / `long` / `timestamp`),每次更新时 JPA 在 `WHERE` 子句中带上 `version = ?`,并在更新成功后把 `version` 自增(或更新为新值) 如果更新时数据库的 `version` 与实体中持有的 `version` 不一致(说明其他事务已提交修改),则更新影响行数为 0,JPA 抛出乐观锁异常(`OptimisticLockException`,在 Spring 中常被包装为 `ObjectOptimisticLockingFailureException` / `OptimisticLockingFailureException`)。 ```java @Entity public class Order { @Id @GeneratedValue private Long id; @Version private Integer version; private BigDecimal amount; // ... } ``` Spring Data JPA =============== 介绍 -- Spring Data JPA **不是** JPA 的实现(它不是 Hibernate 的替代品)。 它的作用是**管理** JPA。它让你连 JPA 的标准代码(如 `EntityManager`)都不用写了,只需要写一个**接口**,剩下的交给它。 Spring Data JPA 的核心理念是:**约定优于配置** 如果直接用 Hibernate/JPA,你可能还需要写很多通用的 DAO(数据访问对象)代码,比如增删改查。 比如: ```java // 你需要手动管理 EntityManager,还要写 SQL 或 JPQL public User findByName(String name) { return entityManager.createQuery("SELECT u FROM User u WHERE u.name = :name", User.class) .setParameter("name", name) .getSingleResult(); } ``` Spring Data JPA 写法: ```java // 你只需要定义一个接口,不需要写实现类! public interface UserRepository extends JpaRepository<User, Long> { // 只要你的方法名符合规范,SQL 会自动生成 User findByName(String name); } ``` **你看,连实现类都不用写!** Spring Data JPA 会在程序启动时,利用**动态代理**技术,自动帮你生成这个接口的实现代码。 **核心特性** Spring Data JPA 是 JPA 的封装,对外提供了几个大杀器: * **Repository 抽象**: 它提供了 `JpaRepository` 等顶层接口,直接继承它,你就立刻拥有了标准的 CRUD(增删改查)、分页、排序功能,一行代码都不用写。 * **方法名即查询 (Query Methods)**: 这是最神奇的地方。你按照语义写方法名,它自动翻译成 SQL。 * `findByAge(int age)` -> `select * from user where age = ?` * `findByNameAndAddress(String name, String addr)` -> `select * from ... where name = ? and address = ?` * **极简的分页与排序**: 只需要在方法参数中加入 `Pageable` 或 `Sort` 参数,它自动帮你拼接 `LIMIT` 等分页语句。 Repository 接口 ------------- Spring Data JPA 最核心的 4 个 Repository: 1. **CrudRepository** 2. **PagingAndSortingRepository** 3. **JpaRepository** 4. **JpaSpecificationExecutor** 看看接口的集成关系图: ```java CrudRepository<T, ID> ↑ PagingAndSortingRepository<T, ID> ↑ JpaRepository<T, ID> ``` 而: ```plain JpaSpecificationExecutor<T> (完全独立,不在继承链上) ``` 每一个接口的职责接下来展开讲。 ### CrudRepository —— 最基础的 CRUD 提供最基本的数据操作: * `save()` * `findById()` * `existsById()` * `findAll()` * `deleteById()` * `deleteAll()` 典型用法: ```plain userRepository.save(user); userRepository.findById(1L); ``` **适合:简单项目基础 CRUD。** ### PagingAndSortingRepository —— 增加分页与排序 在 CrudRepository 基础上,扩展: * `findAll(Sort sort)` * `findAll(Pageable pageable)` 用法: ```plain Page<User> page = userRepository.findAll(PageRequest.of(0, 10)); ``` **适合:需要分页、排序,但不需要复杂 SQL 的项目。** ### JpaRepository —— 最强的默认 Repository 这是 Spring Data JPA 项目 最常用的接口,生产环境几乎都用它。 它扩展了: * 批处理增强:`saveAll()` * 刷新:`flush()` * 批量删除:`deleteInBatch()` * 去重查询:`findAll(Sort)`、`findAll(Pageable)` * 实体管理更丰富 **示例:** ```plain List<User> users = userRepository.findAll(Sort.by("age")); ``` **适合:主流企业开发,99% 项目从它开始。** ### JpaSpecificationExecutor —— 完全动态查询(Criteria API) 它让你可以构建类似 MyBatis-Plus QueryWrapper 的动态条件。 提供方法: * `findAll(Specification spec)` * `findAll(Specification spec, Sort sort)` * `findAll(Specification spec, Pageable pageable)` * `count(Specification spec)` 示例(查询 age > 20 且 name = "Tom"): ```plain userRepository.findAll((root, query, cb) -> cb.and( cb.gt(root.get("age"), 20), cb.equal(root.get("name"), "Tom") ) ); ``` 非常像 MyBatis Plus 的构造器: ```plain wrapper.gt("age", 20).eq("name", "Tom"); ``` **适合:复杂动态查询、企业级系统。** ### 一个 DEMO 学会使用 Repository 接口 1. 实体类定义 ```java @Entity @Data // Lombok 注解,生成 Getter/Setter 等 @Table(name = "t_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Integer age; private String email; private Date createTime; } ``` 2. 核心接口 (Repository) 这是最重要的一步。我们要同时继承两个接口 , 实现“双剑合璧”。 ```java import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; // 继承 JpaRepository 获得基础 CRUD + 分页 // 继承 JpaSpecificationExecutor 获得动态查询能力 public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 这里暂时不需要写任何代码,两个父接口已经够强大了! } ``` 3. 业务层实战使用 这里我模拟一个 Service 方法,展示如何构建复杂的动态查询。 ```java @Service public class UserService { @Autowired private UserRepository userRepository; /** * 模拟前端传入的搜索条件对象 * 如果字段为 null,说明前端没输这个条件 */ public Page<User> searchUsers(String nameKeyword, Integer minAge, String exactEmail, int page, int size) { // 1. 构建动态查询条件 (Specification) // root: 代表 User 表(也就是 SQL 中的 from t_user) // query: 顶层查询对象(主要用于组合 Order By 等,平时用得少) // cb (CriteriaBuilder): 构建 SQL 谓词的工厂(也就是 =, like, >, and, or 这些工具) Specification<User> spec = (root, query, cb) -> { // 用于存放所有的 WHERE 条件 List<Predicate> predicates = new ArrayList<>(); // --- 动态拼装条件 --- // 1. 姓名模糊查询 (对应 SQL: name LIKE %keyword%) if (nameKeyword != null && !nameKeyword.isEmpty()) { // equal 是相等,like 是模糊 predicates.add(cb.like(root.get("name"), "%" + nameKeyword + "%")); } // 2. 年龄大于某值 (对应 SQL: age > minAge) if (minAge != null) { // gt = greater than predicates.add(cb.gt(root.get("age"), minAge)); } // 3. 邮箱精确匹配 (对应 SQL: email = 'xxx') if (exactEmail != null) { predicates.add(cb.equal(root.get("email"), exactEmail)); } // --- 组合所有条件 --- // 将 List 里的条件用 AND 连接起来 return cb.and(predicates.toArray(new Predicate[0])); }; // 2. 构建分页与排序 (Pageable) // page 从 0 开始,Sort.Direction.DESC 代表倒序 Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createTime")); // 3. 发起查询 (双剑合璧调用) // findAll(Specification, Pageable) 是 JpaSpecificationExecutor 提供的 return userRepository.findAll(spec, pageable); } } ``` Spring Data 的查询机制 ----------------- ### 语义化查询 这是 Spring Data JPA 最“黑科技”的地方。它像一个翻译官,把你的 Java 方法名“翻译”成 SQL。 **翻译规则:** Spring 会剥离方法名的前缀(如 `find`, `read`, `query`, `count`),解析剩下的部分。 * **关键字映射:** * `And` / `Or` → `WHERE ... AND ...` * `Is`, `Equals` → `WHERE col = ?` * `Between` → `WHERE col BETWEEN ? AND ?` * `LessThan`, `GreaterThan` → `<` , `>` * `Like`, `Containing` → `LIKE %?%` * `OrderBy[Property][Asc/Desc]` → `ORDER BY ...` ```java List<User> findByEmailAndStatus(String email, Status status); ``` SQL: ```java SELECT * FROM user WHERE email = ? AND status = ? ``` 模糊匹配: ```java List<User> findByNameLike(String name); // 传参:“%Tom%” List<User> findByNameContaining(String name); // 自动拼接成 %name% List<User> findByNameStartingWith(String prefix); // prefix% ``` 排序: ```java List<User> findByStatusOrderByAgeDesc(Status status); ``` ```java SELECT u.id, u.name, u.age, u.email, u.status FROM user u WHERE u.status = ? ORDER BY u.age DESC; ``` 取 TopN: ```java User findTop1ByOrderByIdDesc(); ``` ```java SELECT u.id, u.name, u.age, u.email, u.status FROM user u ORDER BY u.id DESC LIMIT 1; ``` ### 手动指定查询 (`@Query` 注解) 当方法名太长,或者你需要用到很复杂的 SQL 特性(如 Join 优化、子查询)时,这层机制就派上用场了。优先级**高于**第一层。 使用 JPQL , **操作的是类和属性,不是表和字段** ```java // ?1 代表第一个参数 @Query("select u from User u where u.email = ?1 and u.age > 18") User findAdultUserByEmail(String email); ``` 也可以使用SQL 如果你必须用数据库特有的函数(比如 MySQL 的 `DATE_FORMAT` 或 Oracle 的窗口函数),就必须开启 `nativeQuery = true`。**这时写的是纯正的 SQL。** ```java // 这里的 t_user 是表名,email 是数据库字段名 @Query(value = "select * from t_user where email = ?1", nativeQuery = true) User findByEmailNative(String email); ``` 默认 `@Query` 只能查。如果你要 UPDATE 或 DELETE,必须加 `@Modifying` 并配合事务。 ```java @Modifying @Query("update User u set u.name = ?1 where u.id = ?2") int updateName(String name, Long id); ``` ### 动态查询 Specification 当查询条件是不固定的(用户可能填了名字,也可能没填),前两种方法就“死”了。 **什么是动态查询?** 动态 = **根据用户输入的参数数量不同,自动拼不同的 SQL 条件** 比如搜索用户页面一般有这些筛选项: * name(可填可不填) * minAge(可填可不填) * maxAge(可填可不填) * status(可选可空) * email(可空) 用户可能输入: | name | minAge | maxAge | status | email | | --- | --- | --- | --- | --- | | Tom | null | null | null | null null | 20 | 30 | ENABLED | null null | null | null | null | null Lily | 30 | null | DISABLED | aaa@bb.com | 每次输入不一样 → SQL 就不一样,这就是“动态查询”。 方法名的语义化查询和 JPQL 不适合动态查询,因为那两种方式都要提前写死 SQL , 不适合灵活生成。 Specification 就是用 Java 代码动态构造 WHERE 条件的工具,类似 MyBatis-Plus 的 QueryWrapper。 必要条件:**你的 Repository 必须实现 JpaSpecificationExecutor<T>** ```java public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { } ``` 只有实现了这个接口,Repository 才能执行: ```plain findAll(Specification spec) findAll(Specification spec, Pageable pageable) findOne(Specification spec) ``` 动态查询案例: 搜索条件 ```java public List<User> search(UserQuery query) { Specification<User> spec = (root, q, cb) -> { List<Predicate> list = new ArrayList<>(); if (query.getName() != null) { list.add(cb.like(root.get("name"), "%" + query.getName() + "%")); } if (query.getMinAge() != null) { list.add(cb.ge(root.get("age"), query.getMinAge())); } if (query.getMaxAge() != null) { list.add(cb.le(root.get("age"), query.getMaxAge())); } if (query.getStatus() != null) { list.add(cb.equal(root.get("status"), query.getStatus())); } if (query.getEmail() != null) { list.add(cb.equal(root.get("email"), query.getEmail())); } return cb.and(list.toArray(new Predicate[0])); }; return userRepository.findAll(spec); } ``` 如果用户输入name , 自动生成 SQL: ```java SELECT * FROM user WHERE name LIKE '%Tom%'; ``` 用户输入: ```java minAge = 20 maxAge = 30 status = 'ENABLED' ``` 生成SQL: ```java SELECT * FROM user WHERE age >= 20 AND age <= 30 AND status = 'ENABLED'; ``` 用户全都不输入: ```java SELECT * FROM user; ``` 这叫 “全表查询”,因为没有条件。 ### 分页与排序:`Page`, `Pageable`, `Sort` Sort 是排序,不管你是否分页,可能都要排序(查多条数据)。 ```java // 写法 1:按 age 倒序 (ORDER BY age DESC) Sort sort1 = Sort.by(Sort.Direction.DESC, "age"); // 写法 2:更流式的写法 (推荐) Sort sort2 = Sort.by("age").descending(); ``` Sort 对象可以用到下面分页条件中。 Spring Data 提供统一的分页接口,Pageable 分页条件,Page 分页查询结果,包含 List + TotalCount ```plain Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "age")); Page<User> page = userRepository.findAll(pageable); ``` 🔹 对应 SQL(第一页,5 条,按 age 倒序): ```sql SELECT * FROM user ORDER BY age DESC LIMIT 5 OFFSET 0; ``` * * * 以上是本文的全部内容,介绍了ORM框架的演进,MyBatis 与 Hibernate 等框架的设计理念不同延申出来两大派系,并介绍了 JPA 和 Spring Data JPA 的基本用法,实战中基本上使用 Spring Data JPA。
MyBatisPlus插件IPage引入错误
我在配置mp的IPage配置的时候,发现MybatisPlusInterceptor一直显示找不到这个类,我其他代码块都正常使用Mp的LambdaQuery功能,但就是分页功能一直不生效。 **MybatisConfig** `package xlin.com.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // 1.初始化核心插件 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 2.添加分页插件 PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); pageInterceptor.setMaxLimit(1000L); // 设置分页上限 interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }` **service/pom.xml** 以下是service层的pom.xml ```<?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> <parent> <groupId>XLin.com</groupId> <artifactId>main</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service</name> <description>service</description> <properties> <java.version>17</java.version> <mybatis-plus.version>3.5.10.1</mybatis-plus.version> </properties> <dependencies> <!-- 内部模块依赖 --> <dependency> <groupId>XLin.com</groupId> <artifactId>model</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>XLin.com</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--连接池依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 修改 MyBatis-Plus 依赖,排除冲突的 mybatis-spring --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- 显式添加兼容的 mybatis-spring 版本 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!--websocket--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>${mybatis-plus.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>17</source> <target>17</target> <compilerArgs>--enable-preview</compilerArgs> </configuration> </plugin> </plugins> </build> </project> ``` 有没有机智聪明的鱼友帮我解决这个问题?
mybatisplus的逻辑删除
<img src="https://pic.code-nav.cn/post_picture/1920840467615395842/yAHVefWNvPwjWdtt.webp" alt="image.png" width="100%" />
使用 MyBatis-Plus 用户登录异常
数据库有对应的用户但是查询 DB 却无法正常查询出来 1、首先排查逻辑删除 2、其次通过 MyBatis-Plus 输出执行 SQL 然后在 console 执行看是否有数据 3、注册是否使用注册接口进行注册,因为存储到 DB 里面的都是加密后的用户密码 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/8HrjchcmeUxwtXrp.webp" alt="image.png" width="100%" /> # 排查是否逻辑删除 1、首先确保逻辑删除相关的配置 ```yml mybatis-plus: global-config: db-config: logic-delete-field: isDelete # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) ``` 2、对应的字段是否有 @TableLogic ```java /** * 是否删除 */ @TableLogic private Integer isDelete; ``` 3、查看数据库里面的数据 isDelete 字段是否有数据 和 是否是 0 (配置文件的 `logic-not-delete-value`) <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/FYn0w1tZeu9QIaFS.webp" alt="image-20250322095802663" width="100%" /> # 输出日志执行 SQL 1、添加 MyBatis-Plus 日志 ```yml mybatis-plus: configuration: # 仅在开发环境打印日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` 2、日志 ``` Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8824ddb] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@213764115 wrapping com.mysql.cj.jdbc.ConnectionImpl@2d160394] will not be managed by Spring ==> Preparing: SELECT id,username,userAccount,avatarUrl,gender,userPassword,phone,email,userStatus,createTime,updateTime,isDelete,userRole,planetCode FROM user WHERE isDelete=0 AND (userAccount = ? AND userPassword = ?) ==> Parameters: leikooo(String), 7a364d20730ddeeea1a78b6b6c680c72(String) <== Total: 0 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8824ddb] ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/NcgxbxutWEk4tgWr.webp" alt="image-20250322101509441" width="100%" /> 3、把执行的 SQL 在 DB 的 把对应的 ? 替换成 parameters 的参数 ```java SELECT id,username,userAccount,avatarUrl,gender,userPassword,phone,email,userStatus,createTime,updateTime,isDelete,userRole,planetCode FROM user WHERE isDelete=0 AND (userAccount = ? AND userPassword = ?) ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/O7zwGe3JONtvicgm.webp" alt="image-20250322102653633" width="100%" /> 最终效果 ``` SELECT id,username,userAccount,avatarUrl,gender,userPassword,phone,email,userStatus,createTime,updateTime,isDelete,userRole,planetCode FROM user WHERE isDelete=0 AND (userAccount = 'leikooo' AND userPassword = '7a364d20730ddeeea1a78b6b6c680c72') ``` 4、执行 SQL <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/mq679V5tYlw115iP.webp" alt="image-20250322103128346" width="472px" /> 执行 SQL <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/xgpMxtjFpgWctqTZ.webp" alt="image-20250322103337351" width="100%" /> # 查看 DB 数据 1、首先确认一下是否是用户名或者密码错误为了确保不是这个问题,**重新注册一个账号** 然后尝试登录一下 2、如果还是不行,就检查一下 逻辑删除 3、还是不行??应该有点离谱了,如果有报错信息就分析一下其他报错信息
踩坑MybatisPlus映射JSON字段
了解Mysql中text和json数据格式 --------------------- 在MySQL中,`TEXT` 和 `JSON` 是两种不同的数据类型,它们的主要区别在于存储的内容和使用方式。 ### 1. TEXT 类型 * **存储内容**:`TEXT` 类型用于存储纯文本数据,可以是任意长度的字符串。 * **用途**:适用于存储大段的文本内容,如文章、日志等。 * **操作**:`TEXT` 类型的数据可以通过字符串函数进行操作,如 `SUBSTRING`、`CONCAT` 等。 * **索引**:`TEXT` 类型的列不能直接创建索引,但可以创建前缀索引。 ### 2. JSON 类型 * **存储内容**:`JSON` 类型用于存储 JSON 格式的数据,JSON 是一种轻量级的数据交换格式,支持复杂的数据结构,如对象、数组等。 * **用途**:适用于存储结构化数据,如配置信息、API 响应等。 * **操作**:`JSON` 类型的数据可以通过专门的 JSON 函数进行操作,如 `JSON_EXTRACT`、 `JSON_SET`、`JSON_ARRAY` 等。 * **索引**:`JSON` 类型的列可以创建虚拟列,并在虚拟列上创建索引,以提高查询性能。 ### JSON 类型的使用示例 #### 创建表 ```sql CREATE TABLE example ( id INT PRIMARY KEY, data JSON ); ``` #### 插入 JSON 数据 ```sql INSERT INTO example (id, data) VALUES (1, '{"name": "Alice", "age": 25, "hobbies": ["reading", "traveling"]}'); ``` #### 查询 JSON 数据 ```sql -- 查询整个 JSON 字段 SELECT data FROM example WHERE id = 1; -- 查询 JSON 字段中的某个键值 SELECT JSON_EXTRACT(data, '$.name') AS name FROM example WHERE id = 1; -- 查询 JSON 字段中的数组 SELECT JSON_EXTRACT(data, '$.hobbies') AS hobbies FROM example WHERE id = 1; ``` #### 更新 JSON 数据 ```plsql -- 更新 JSON 字段中的某个键值 UPDATE example SET data = JSON_SET(data, '$.age', 26) WHERE id = 1; -- 向 JSON 字段中的数组添加元素 UPDATE example SET data = JSON_ARRAY_APPEND(data, '$.hobbies', 'cooking') WHERE id = 1; ``` #### 创建虚拟列并创建索引 ```sql -- 创建虚拟列 ALTER TABLE example ADD COLUMN name VARCHAR(255) AS (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) VIRTUAL; -- 在虚拟列上创建索引 CREATE INDEX idx_name ON example (name); ``` ### 补充: MySQL 中,**虚拟列(Generated Column)** 是一种特殊的列,它的值是通过表达式计算得出的,而不是直接存储数据。虚拟列可以分为两种类型: 1. **VIRTUAL**:虚拟列的值在查询时动态计算,不占用存储空间。 2. **STORED**:虚拟列的值在插入或更新时计算并存储,占用存储空间。 虚拟列的主要用途是简化查询、优化性能以及支持对 JSON 或其他复杂数据类型的索引。以下是虚拟列的优缺点: #### 优点 ##### 1. 简化查询 * 虚拟列可以将复杂的计算逻辑封装在列定义中,查询时可以直接使用虚拟列,而不需要重复编写复杂的表达式。 * 例如,从 JSON 数据中提取某个字段并创建虚拟列后,可以直接查询虚拟列,而不需要每次都使用 `JSON_EXTRACT`。 ##### 2. 支持索引 * 虚拟列可以创建索引,从而显著提高查询性能。 * 对于 JSON 数据类型,MySQL 不支持直接对 JSON 字段创建索引,但可以通过虚拟列提取 JSON 中的某个字段并创建索引。 ##### 3. 数据一致性 * 虚拟列的值是通过表达式计算得出的,因此可以保证数据的一致性。 * 例如,如果虚拟列是基于其他列计算的,那么当基础列的值发生变化时,虚拟列的值会自动更新。 ##### 4. 节省存储空间(VIRTUAL 类型) * `VIRTUAL` 类型的虚拟列不存储实际数据,只在查询时动态计算,因此不会占用额外的存储空间。 ##### 5. 灵活性 * 虚拟列可以基于复杂的表达式创建,支持数学运算、字符串操作、JSON 函数等。 #### 缺点 ##### 1. 性能开销(VIRTUAL 类型)virtual * `VIRTUAL` 类型的虚拟列在每次查询时都需要动态计算,如果表达式复杂或数据量较大,可能会影响查询性能。 * 对于频繁查询的场景,`STORED` 类型的虚拟列可能更适合,因为它会预先计算并存储结果。 ##### 2. 存储开销(STORED 类型)stored * `STORED` 类型的虚拟列会占用存储空间,因为它的值是预先计算并存储的。 * 如果虚拟列的数据量较大,可能会增加表的存储需求。 ##### 3. 不支持所有表达式 * 虚拟列的表达式有一些限制,例如不能使用子查询、存储过程、用户定义函数等。 * 某些复杂的计算可能无法通过虚拟列实现。 ##### 4. 维护复杂性 * 如果虚拟列的表达式依赖于其他列,那么当表结构发生变化时(例如修改列名或删除列),可能需要同步更新虚拟列的定义。 * 虚拟列的表达式逻辑需要谨慎设计,避免出现性能瓶颈或逻辑错误。 ##### 5. 兼容性 * 虚拟列是 MySQL 5.7 及以上版本引入的功能,旧版本的 MySQL 不支持虚拟列。 #### 使用场景 ##### 1. JSON 数据索引 * 从 JSON 字段中提取某个键值并创建虚拟列,然后在该列上创建索引。 例如: ```sql ALTER TABLE example ADD COLUMN name VARCHAR(255) AS (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name'))) VIRTUAL; CREATE INDEX idx_name ON example (name); ``` ##### 2. 计算字段 例如,基于价格和数量计算总金额: ```sql ALTER TABLE orders ADD COLUMN total_amount DECIMAL(10, 2) AS (price * quantity) STORED; ``` ##### 3. 数据格式化 例如,将日期字段格式化为字符串: ```sql ALTER TABLE events ADD COLUMN event_date_str VARCHAR(10) AS (DATE_FORMAT(event_date, '%Y-%m-%d')) VIRTUAL; ``` #### 总结 | 特性 | VIRTUAL 虚拟列 | STORED 虚拟列 | | --- | --- | --- | | 存储空间 | 不占用存储空间 | 占用存储空间 计算时机 | 每次查询时动态计算 | 插入或更新时计算并存储 性能 | 查询时可能有性能开销 | 查询性能较好,但写入时可能有开销 适用场景 | 数据变化频繁、存储空间有限 | 查询频繁、计算复杂 | 根据具体需求选择合适的虚拟列类型,可以显著提升数据库的性能和灵活性。 ### 总结 * `TEXT` 类型适合存储纯文本数据,而 `JSON` 类型适合存储结构化数据。 * `JSON` 类型提供了丰富的函数来操作 JSON 数据,并且可以通过虚拟列和索引来优化查询性能。 MybatisPlus如何使用JSON类型 --------------------- ### 数据库实体 <img src="https://pic.code-nav.cn/post_picture/1848659043884322817/i2tB2mn0oefaTDyc.webp" alt="" width="100%" /> ### 引入依赖 ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> ``` ### 创建实体 ```java @Data // 注意这里一定要开启注解映射 @TableName(autoResultMap = true) public class CaseInfo implements Serializable { private static final long serialVersionUID = 1L; /** * 配置id */ @TableId(type = IdType.ASSIGN_ID) private Long id; /** * 房间id */ @TableField("roomId") private Long roomId; /** * 配置 */ // 配置类型处理器可以自己写,也可以用其他的 JacksonTypeHandler.class 看自己 // 如果没有在类开头添加注解映射 ,当然你也可以用显示注解 看自己 @TableField(value = "caseInfo",typeHandler = HutoolJsonTypeHandler.class) @TableField(typeHandler = HutoolJsonTypeHandler.class) private CaseInfos caseInfo; } ``` ```java @Data public class CaseInfos implements Serializable { private static final long serialVersionUID = 1L; private String size; private String tag; } ``` ### 自己实现类型处理器 ```java public class HutoolJsonTypeHandler extends AbstractJsonTypeHandler<CaseInfos> { @Override public CaseInfos parse(String json) { // 将JSON字符串转换为CaseInfo对象 if (json == null || json.isEmpty()) { return null; } try { return JSONUtil.toBean(json, CaseInfos.class); } catch (Exception e) { System.err.println("JSON解析异常: " + e.getMessage()); return null; } } @Override public String toJson(CaseInfos obj) { // 将CaseInfo对象转换为JSON字符串 if (obj == null) { return null; } try { return JSONUtil.toJsonStr(obj); } catch (Exception e) { System.err.println("JSON转换异常: " + e.getMessage()); return null; } } } ``` ### 增删改查 ```java @SpringBootTest class CaseServiceTest { @Resource private CaseService caseService; // 生成增删改查 @Test void save() { CaseInfo aCase = new CaseInfo(); aCase.setRoomId(1L); CaseInfos caseInfo = new CaseInfos(); caseInfo.setSize("big"); caseInfo.setTag("恐怖"); aCase.setCaseInfo(caseInfo); caseService.save(aCase); } @Test public void updateById() { Long caseId = 1902930311949942786L; String newSizeValue = "big"; // 使用 LambdaUpdateWrapper 和 MySQL JSON 函数更新单个字段 LambdaUpdateWrapper<CaseInfo> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(CaseInfo::getId, caseId) .setSql(String.format("caseInfo = JSON_SET(caseInfo, '$.size', '%s')", newSizeValue)); // 执行更新操作 boolean updateResult = caseService.update(updateWrapper); if (updateResult) { System.out.println("Update successful."); } else { System.out.println("Update failed."); } } /** * 更新JSON对象中的单个字段 * 示例1:更新size字段 */ @Test public void updateJsonField_Size() { // 1. 指定要更新的记录ID Long caseId = 1902930311949942786L; // 2. 设置新的size值 String newSizeValue = "medium"; // 3. 使用LambdaUpdateWrapper和MySQL的JSON_SET函数 LambdaUpdateWrapper<CaseInfo> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(CaseInfo::getId, caseId) .setSql(String.format("caseInfo = JSON_SET(caseInfo, '$.size', '%s')", newSizeValue)); // 4. 执行更新操作 boolean updateResult = caseService.update(updateWrapper); System.out.println("更新size字段 " + (updateResult ? "成功" : "失败")); // 5. 验证更新结果 if (updateResult) { CaseInfo updatedCase = caseService.getById(caseId); System.out.println("更新后的记录: " + updatedCase); System.out.println("size字段新值: " + (updatedCase.getCaseInfo() != null ? updatedCase.getCaseInfo().getSize() : "null")); } } /** * 使用MySQL ->> 操作符更新JSON字段 * 这是一种更简洁的方式 */ @Test public void updateJsonField_UsingArrowOperator() { // 1. 指定要更新的记录ID Long caseId = 1902930311949942786L; // 2. 设置新的值 String newSizeValue = "超大"; // 3. 使用LambdaUpdateWrapper和MySQL的 ->> 操作符 LambdaUpdateWrapper<CaseInfo> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(CaseInfo::getId, caseId) .setSql("JSON_SET(caseInfo, '$.size', '" + newSizeValue + "') -> '$'"); // 4. 执行更新操作 boolean updateResult = caseService.update(null, updateWrapper); System.out.println("使用 ->> 操作符更新字段 " + (updateResult ? "成功" : "失败")); // 5. 验证更新结果 if (updateResult) { CaseInfo updatedCase = caseService.getById(caseId); System.out.println("更新后的记录: " + updatedCase); System.out.println("size字段新值: " + (updatedCase.getCaseInfo() != null ? updatedCase.getCaseInfo().getSize() : "null")); } } /** * 查询JSON字段中特定值的记录 * 使用MySQL ->> 操作符 */ @Test public void queryUsingJsonOperator() { // 使用原生SQL进行查询 String sql = "SELECT * FROM case_info WHERE caseInfo->>'$.size' = 'medium'"; System.out.println("执行SQL: " + sql); // 获取所有记录并过滤 List<CaseInfo> allCases = caseService.list(); System.out.println("找到符合条件的记录:"); for (CaseInfo aCase : allCases) { if (aCase.getCaseInfo() != null && "medium".equals(aCase.getCaseInfo().getSize())) { System.out.println(aCase); } } } /** * 更新JSON对象中的单个字段 * 示例2:更新tag字段 */ @Test public void updateJsonField_Tag() { // 1. 指定要更新的记录ID Long caseId = 1902930311949942786L; // 2. 设置新的tag值 String newTagValue = "科幻"; // 3. 使用LambdaUpdateWrapper和MySQL的JSON_SET函数 LambdaUpdateWrapper<CaseInfo> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(CaseInfo::getId, caseId) .setSql(String.format("caseInfo = JSON_SET(caseInfo, '$.tag', '%s')", newTagValue)); // 4. 执行更新操作 boolean updateResult = caseService.update(updateWrapper); System.out.println("更新tag字段 " + (updateResult ? "成功" : "失败")); // 5. 验证更新结果 if (updateResult) { CaseInfo updatedCase = caseService.getById(caseId); System.out.println("更新后的记录: " + updatedCase); System.out.println("tag字段新值: " + (updatedCase.getCaseInfo() != null ? updatedCase.getCaseInfo().getTag() : "null")); } } /** * 同时更新JSON对象中的多个字段 */ @Test public void updateMultipleJsonFields() { // 1. 指定要更新的记录ID Long caseId = 1902930311949942786L; // 2. 设置新的值 String newSizeValue = "small"; String newTagValue = "推理"; // 3. 使用LambdaUpdateWrapper和MySQL的JSON_SET函数同时更新多个字段 LambdaUpdateWrapper<CaseInfo> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(CaseInfo::getId, caseId) .setSql(String.format("caseInfo = JSON_SET(caseInfo, '$.size', '%s', '$.tag', '%s')", newSizeValue, newTagValue)); // 4. 执行更新操作 boolean updateResult = caseService.update(updateWrapper); System.out.println("同时更新多个字段 " + (updateResult ? "成功" : "失败")); // 5. 验证更新结果 if (updateResult) { CaseInfo updatedCase = caseService.getById(caseId); System.out.println("更新后的记录: " + updatedCase); if (updatedCase.getCaseInfo() != null) { System.out.println("size字段新值: " + updatedCase.getCaseInfo().getSize()); System.out.println("tag字段新值: " + updatedCase.getCaseInfo().getTag()); } } } @Test void removeById() { caseService.removeById(1L); } @Test void list() { List<CaseInfo> list = caseService.list(); for (CaseInfo aCase : list) { System.out.println(aCase); } } } ``` 到这你就应该学会了该如何在OJ项目中使用JSON格式来处理判题信息等等JSON类型的字段了把 如果还有问题 那你有没好好看我的文档 **回答我!!! look in my eyes tell me why why baby why**
mybatis-plus遇到的坑
有时候分页不起作用,驼峰式命名自动转下划线,一个配置就能解决 ```java /** * MyBatis Plus 配置 * * @author https://github.com/liqcdfz */ @Configuration @MapperScan("com.qcdfz.qcdfzhaiguitangbackend.mapper") public class MyBatisPlusConfig { /** * 拦截器配置 * * @return */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer() { /** * 创建并返回一个ConfigurationCustomizer的Lambda表达式 * 该表达式接收一个Configuration对象,并设置其mapUnderscoreToCamelCase属性为false * 这样做是为了确保数据库表字段名不会自动转换为驼峰命名法 * * @param configuration MyBatis的配置对象,通过它来调整MyBatis的行为 */ return configuration -> configuration.setMapUnderscoreToCamelCase(false); } } ```
MybatisPlus3.3.2分页插件无效?
MybatisPlus3.3.2还没到分页插件升级,也就是用的PaginationInterceptor,然后失效了 配置: <img src="https://pic.code-nav.cn/post_picture/1868580085084782594/XcvN0ZskFLURfQxQ.png" alt="QQ_1734339324284.png" width="100%" /> 调用: <img src="https://pic.code-nav.cn/post_picture/1868580085084782594/H80Lfuw5uVwhjaM3.webp" alt="QQ_1734339306575.png" width="100%" /> xml: <img src="https://pic.code-nav.cn/post_picture/1868580085084782594/UAeGiS1xD8Xkwxo4.webp" alt="QQ_1734339273539.png" width="100%" /> 因为数据库内这个表有2w条数据导致每次调用这个接口都要等8s才有数据,sql语句后面没有limit每次都全表查询,断点之后总是执行完sql语句才进拦截器,那位大佬可以跟小弟说说这是为啥嘛
MyBatis自定义JSON类型处理器
# 一、使用场景 | 实体类 | 数据库 | | :---------------------: | :-------------------------------------: | | `List<String>` | `["user", "admin"]` | | `List<List<List<ADT>>>` | `{"ADT":[[{"BookingCode":["N","N"]}]]}` | 复杂的Bean的定义如下(包含泛型) ```java @Data public class ADT { private List<String> BookingCode; } @Data public class Price { private List<List<ADT>> ADT; } ``` --- # 二、步骤 **UserMapper.xml** ~~~xml <resultMap id="UserResult" type="User"> <result property="userRole" column="user_role" typeHandler="cn.luoyan.elitecode.common.utils.JacksonTypeHandler"/> </resultMap> <insert id="insertUser" useGeneratedKeys="true" keyProperty="userId"> insert into user( <if test="userRole != null and userRole != ''">user_role,</if> ) values( <!-- PS:这里typeHandler无需用引号包围 --> <if test="userRole != null and userRole != ''">#{userRole, typeHandler=cn.luoyan.elitecode.common.utils.JacksonTypeHandler},</if> ) </insert> ~~~ **添加类型处理器** ~~~java package cn.luoyan.elitecode.common.utils; import cn.hutool.core.lang.TypeReference; import cn.hutool.json.JSONUtil; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class JacksonTypeHandler<T> extends BaseTypeHandler<T> { private Class<T> type; public JacksonTypeHandler(Class<T> type) { this.type = type; } /** * 将非空参数设置到 PreparedStatement 中的指定位置 * * @param preparedStatement 要设置参数的 PreparedStatement 对象 * @param i 参数的位置索引,从 1 开始 * @param parameter 要设置的参数值,类型为 String * @param jdbcType 参数的 JDBC 类型,用于正确转换 Java 类型到数据库类型 * @throws SQLException 如果设置参数时发生 SQL 异常 */ @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, T parameter, JdbcType jdbcType) throws SQLException { preparedStatement.setString(i, this.toJSON(parameter)); } /** * 从 ResultSet 中获取指定列名的可为空结果 * * @param resultSet 要从中获取结果的 ResultSet 对象 * @param columnName 列名 * @return 指定列名的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException { String json = resultSet.getString(columnName); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 从 ResultSet 中获取指定列索引的可为空结果 * * @param resultSet 要从中获取结果的 ResultSet 对象 * @param columnIndex 列的索引,从 1 开始 * @return 指定列索引的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException { String json = resultSet.getString(columnIndex); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 从 CallableStatement 中获取指定列索引的可为空结果 * * @param callableStatement 要从中获取结果的 CallableStatement 对象 * @param columnIndex 列的索引,从 1 开始 * @return 指定列索引的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException { String json = callableStatement.getString(columnIndex); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 字符串转JSON * @param parameter * @return */ private String toJSON(T parameter) { return JSONUtil.toJsonStr(parameter); } /** * JSON转字符串 * @param json * @return */ private T parse(String json) { // 使用 TypeReference 来保留完整的泛型信息,避免因类型擦除导致的问题 return JSONUtil.toBean(json, new TypeReference<T>() {}, false); } } ~~~ --- # 三、为什么使用 `JSONUtil.toBean(json, new TypeReference<T>() {}, false)` 而不是直接使用 `JSONUtil.toBean(json, type)`? **1)`JSONUtil.toBean(json, type)`** 这个方法主要用于将 JSON 对象转换为指定的 Java 类型。它的局限性在于: - 它接受一个 `Class` 对象作为参数,这个 `Class` 对象在运行时不包含泛型信息。 - 由于 Java 的类型擦除机制,`List.class` 在运行时只代表原始的 `List` 类型,不包含元素类型信息。 - 对于 `["user", "admin"]` 这样的 JSON 数组,该方法无法正确识别元素应该是什么类型。 - 结果可能是一个包含 `Object` 类型元素的 `List`,而不是 `String` 类型。 示例: ```java String json = "[\"user\", \"admin\"]"; List<String> list = JSONUtil.toBean(json, List.class); // 这不会正确工作 ``` **2)`JSONUtil.toBean(json, new TypeReference<T>() {}, false)`** 这个方法更加灵活,能够处理复杂的泛型类型: - Hutool 的 `TypeReference` 是一个抽象类,通过创建匿名内部类的方式来捕获完整的泛型类型信息。 - 当你使用 `new TypeReference<List<String>>() {}` 时,Hutool 可以通过反射获取到完整的泛型类型信息,包括元素类型。 - 这使得 Hutool 能够正确地将 JSON 数组中的元素解析为 `String` 类型。 示例: ```java String json = "[\"user\", \"admin\"]"; List<String> list = JSONUtil.toBean(json, new TypeReference<List<String>>() {}, false); // 这可以正确工作 ``` # 一、使用场景 | 实体类 | 数据库 | | :---------------------: | :-------------------------------------: | | `List<String>` | `["user", "admin"]` | | `List<List<List<ADT>>>` | `{"ADT":[[{"BookingCode":["N","N"]}]]}` | 复杂的Bean的定义如下(包含泛型) ```java @Data public class ADT { private List<String> BookingCode; } @Data public class Price { private List<List<ADT>> ADT; } ``` --- # 二、步骤 **UserMapper.xml** ~~~xml <resultMap id="UserResult" type="User"> <result property="userRole" column="user_role" typeHandler="cn.luoyan.elitecode.common.utils.JacksonTypeHandler"/> </resultMap> <insert id="insertUser" useGeneratedKeys="true" keyProperty="userId"> insert into user( <if test="userRole != null and userRole != ''">user_role,</if> ) values( <!-- PS:这里typeHandler无需用引号包围 --> <if test="userRole != null and userRole != ''">#{userRole, typeHandler=cn.luoyan.elitecode.common.utils.JacksonTypeHandler},</if> ) </insert> ~~~ **添加类型处理器** ~~~java package cn.luoyan.elitecode.common.utils; import cn.hutool.core.lang.TypeReference; import cn.hutool.json.JSONUtil; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class JacksonTypeHandler<T> extends BaseTypeHandler<T> { private Class<T> type; public JacksonTypeHandler(Class<T> type) { this.type = type; } /** * 将非空参数设置到 PreparedStatement 中的指定位置 * * @param preparedStatement 要设置参数的 PreparedStatement 对象 * @param i 参数的位置索引,从 1 开始 * @param parameter 要设置的参数值,类型为 String * @param jdbcType 参数的 JDBC 类型,用于正确转换 Java 类型到数据库类型 * @throws SQLException 如果设置参数时发生 SQL 异常 */ @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, T parameter, JdbcType jdbcType) throws SQLException { preparedStatement.setString(i, this.toJSON(parameter)); } /** * 从 ResultSet 中获取指定列名的可为空结果 * * @param resultSet 要从中获取结果的 ResultSet 对象 * @param columnName 列名 * @return 指定列名的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException { String json = resultSet.getString(columnName); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 从 ResultSet 中获取指定列索引的可为空结果 * * @param resultSet 要从中获取结果的 ResultSet 对象 * @param columnIndex 列的索引,从 1 开始 * @return 指定列索引的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException { String json = resultSet.getString(columnIndex); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 从 CallableStatement 中获取指定列索引的可为空结果 * * @param callableStatement 要从中获取结果的 CallableStatement 对象 * @param columnIndex 列的索引,从 1 开始 * @return 指定列索引的结果,如果为空则返回 null * @throws SQLException 如果获取结果时发生 SQL 异常 */ @Override public T getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException { String json = callableStatement.getString(columnIndex); return StringUtils.isEmpty(json) ? null : this.parse(json); } /** * 字符串转JSON * @param parameter * @return */ private String toJSON(T parameter) { return JSONUtil.toJsonStr(parameter); } /** * JSON转字符串 * @param json * @return */ private T parse(String json) { // 使用 TypeReference 来保留完整的泛型信息,避免因类型擦除导致的问题 return JSONUtil.toBean(json, new TypeReference<T>() {}, false); } } ~~~ --- # 三、为什么使用 `JSONUtil.toBean(json, new TypeReference<T>() {}, false)` 而不是直接使用 `JSONUtil.toBean(json, type)`? **1)`JSONUtil.toBean(json, type)`** 这个方法主要用于将 JSON 对象转换为指定的 Java 类型。它的局限性在于: - 它接受一个 `Class` 对象作为参数,这个 `Class` 对象在运行时不包含泛型信息。 - 由于 Java 的类型擦除机制,`List.class` 在运行时只代表原始的 `List` 类型,不包含元素类型信息。 - 对于 `["user", "admin"]` 这样的 JSON 数组,该方法无法正确识别元素应该是什么类型。 - 结果可能是一个包含 `Object` 类型元素的 `List`,而不是 `String` 类型。 示例: ```java String json = "[\"user\", \"admin\"]"; List<String> list = JSONUtil.toBean(json, List.class); // 这不会正确工作 ``` **2)`JSONUtil.toBean(json, new TypeReference<T>() {}, false)`** 这个方法更加灵活,能够处理复杂的泛型类型: - Hutool 的 `TypeReference` 是一个抽象类,通过创建匿名内部类的方式来捕获完整的泛型类型信息。 - 当你使用 `new TypeReference<List<String>>() {}` 时,Hutool 可以通过反射获取到完整的泛型类型信息,包括元素类型。 - 这使得 Hutool 能够正确地将 JSON 数组中的元素解析为 `String` 类型。 示例: ```java String json = "[\"user\", \"admin\"]"; List<String> list = JSONUtil.toBean(json, new TypeReference<List<String>>() {}, false); // 这可以正确工作 ```
关于安装MybatisX插件后用不了
大家没有遇到过这种问题,安装MybatisX插件后,mapper图标确实变了 <img src="https://pic.code-nav.cn/post_picture/1806978145014042626/K4gwn99whWAT9Psr.webp" alt="image.png" width="100%" /> 但是,连接好数据库后右键数据库表没有MybatisX-Generator选项 <img src="https://pic.code-nav.cn/post_picture/1806978145014042626/8CPl1Cv2iijOu4L1.webp" alt="image.png" width="361px" />
