首页 / 资讯中心 / 文章详情

JPA/Hibernate映射MySQL JSON字段为结构化Java对象实战指南

JPA/Hibernate映射MySQL JSON字段为结构化Java对象实战指南 ★ FEATURED ARTICLE
1. 先搞清楚为什么不能直接拿 String 去接 JSON 字段先说个我自己的经历。之前在做一个信息采集类的后台管理系统MySQL 表里有一个extra_info字段存的是 JSON 对象里面包含来源渠道、采集时间、不同维度的扩展属性。一开始图省事实体类里直接private String extraInfo;映射反正 JPA 也能跑得通存进去没问题查出来也是一个 JSON 字符串用 Jackson 转一下就行了。等业务跑起来才发现问题不是“能不能用”而是“用起来有多难受”。最直观的痛点是类型校验完全丢失。JSON 字符串本质上是人写的不是机器约束的内部结构对不对、字段类型对不对全靠后端代码自觉。今天存进去的是{platform:web,tag:[a,b]}明天运营在管理后台误传了个{platform:123,tag:a}Jackon 反序列化的时候直接抛异常接口报 500你都不知道问题出在哪个脏数据上。更恶心的是如果业务侧依赖这个字段做批量查询比如WHERE JSON_EXTRACT(extra_info, $.source) apiHibernate 在生成 JPQL 时不会帮你做任何类型转换你只能写原生 SQL写完之后返回的还是Object[]裸数组配合 JPA 的实体映射感觉就像回到了 JDBC 时代。还有一类问题是Lob。有的团队会用Lob把 Java 对象映射成 MySQL 的LONGTEXT这种方案能解决字符串映射时的一部分类型问题但代价更大每次查询默认都会把这个大字段加载到内存哪怕你只需要业务主键和状态字段依然会被 JPA 的字段策略拖进来。虽然可以用Basic(fetch FetchType.LAZY)声明懒加载但 Hibernate 对懒加载基础类型的支持一直不太完善经常出现 session 关闭之后访问该字段抛LazyInitializationException的坑要么就得调字节码增强项目复杂度一下子上来了。所以现在我在新项目里遇到MySQL JSON 字段 JPA的组合基本不会再用 String 或 Lob 硬接。核心思路是引入一个专门的 JSON 类型映射器让实体类和数据库 JSON 列之间存在一个结构化的、可校验的数据管道。Hibernate 本身不支持 JSON 类型但借助第三方扩展可以做到像映射普通对象一样映射 JSON 字段查询、更新、类型转换都能获得和普通 Java 字段一致的体验。这也是本文想讲清楚的主线在 JPA 里把 MySQL 的 JSON 对象映射成真正的结构化 Java 对象而不是绕开它。下面动手之前先理清楚一个前提本文的示例基于 MySQL 8.0 和 Hibernate 6.x如果你还在用 Hibernate 5.x方案思路一致但具体依赖版本和 API 名称会有差异文末我会提一下如何做版本对应。2. 引入 JsonType 之前先把依赖和版本配对搞清楚JSON 映射器我用的是 hypersistence-utils 这个开源库作者是 Vlad Mihalcea也就是那本《High-Performance Java Persistence》的作者。这个库提供的JsonType能够把你的实体类字段序列化成 JSON 存入数据库读取时反序列化回 Java 对象查询时还支持在 JPQL / Criteria API 中作为参数传递。和直接手写String Jackson的方案相比它把繁琐的序列化反序列化逻辑收敛到了 Hibernate 的类型系统内部代码里只需要在字段上标注类型即可。版本配对是个很容易踩坑的环节。hypersistence-utils 的版本号和 Hibernate 的主版本是强绑定的不同大版本对应不同分支你用的 Hibernate 版本对应 hypersistence-utils 版本备注Hibernate 6.x含 6.4 / 6.5 / 6.6hypersistence-utils 6.12.0 及以上目前最常用的组合Hibernate 5.4 / 5.6hypersistence-utils 5.x老项目推荐Hibernate 6.0 早期hypersistence-utils 6.0.x建议升级到 6.12修复了一些 bugMaven 坐标如下dependency groupIdio.hypersistence/groupId artifactIdhypersistence-utils/artifactId version6.12.0/version /dependencyGradle 的话implementation io.hypersistence:hypersistence-utils:6.12.0这里有一点要提醒如果你的应用里同时存在多个 Hibernate 相关依赖比如hibernate-core和hibernate-entitymanager要用mvn dependency:tree检查一下实际生效的 Hibernate 版本以防依赖冲突导致版本匹配错位。另外如果你用了 Spring BootSpring Boot 3.x 默认自带 Hibernate 6.x不需要额外引入 HibernateSpring Boot 2.7 及以前自带 Hibernate 5.6对应 hypersistence-utils 5.x。博主我见过不少同事直接把 6.x 的 utils 塞进 Spring Boot 2.x 项目里启动就报类找不到本质上是版本分支不匹配。引入依赖之后还不能马上用。hypersistence-utils 在 Hibernate 6.x 下会自动注册类型贡献器但在某些老版本或自定义 SessionFactory 场景下需要手动注册。通常只要引入依赖就能工作如果发现类型解析失败可以在persistence.xml或application.properties里加上spring.jpa.properties.hibernate.type_contributorsio.hypersistence.utils.hibernate.type.internal.HypersistenceIntegrator其实在 Hibernate 6.x 下这个配置一般用不上写了也没副作用。真正要注意的是json列在数据库表里的列类型定义。MySQL 的 JSON 列和 TEXT/VARCHAR 在类型系统里完全是两码事如果你用ddl-auto: update让 Hibernate 自动建表默认可能生成一个json列但如果你用的是已有的表结构并且列类型是TEXT或LONGTEXT那么JsonType也能存只是 MySQL 不会对 JSON 内部结构做合法性校验。这个差别很关键——如果你希望数据库层面兜底 JSON 格式一定要保证列类型是json不要用longtext凑合。3. 实体类里 JSON 字段映射的具体写法与注意点前置工作做完接下来直接上一个完整的可运行示例。假设业务场景是记录每个用户在不同渠道上的扩展属性包括偏好设置和自定义标签这个信息天然适合存 JSON因为结构不固定且后续可能会加字段。建表语句CREATE TABLE user_profile ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(64) NOT NULL, extra_info JSON NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实体类Entity Table(name user_profile) TypeDef(name json, typeClass JsonType.class) public class UserProfile { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_name, nullable false, length 64) private String userName; Type(type json) Column(name extra_info, columnDefinition json) private ExtraInfo extraInfo; Column(name created_at, nullable false, updatable false) private LocalDateTime createdAt; // getter / setter / constructor 省略 }对应的ExtraInfo是一个普通 POJOpublic class ExtraInfo { private String source; private Integer level; private ListString tags new ArrayList(); private MapString, Object attributes new HashMap(); // getter / setter }这里有几个细节值得说明第一TypeDef写在类级别还是包级别如果项目里只有一两个地方用到 JSON 映射直接在实体上用Type(type json)就够了如果有多个实体都要用我建议在某个公共包下用TypeDef注解定义在package-info.java里全局统一生效避免每个实体重复声明。示例里写在类上是图省事实际大项目里我更喜欢包级别。第二Column(columnDefinition json)到底要不要写如果你依赖 Hibernate 自动建表建议写上确保生成的列是 MySQL 原生json类型如果你使用 Flyway / Liquibase 管理数据库结构那么这行可以去掉建表脚本由迁移工具负责实体类里只需要Type(type json)声明映射类型即可。要是不小心写了个columnDefinition text而数据里又有嵌套数组后续用JSON_EXTRACT/JSON_CONTAINS时会把一部分本应由数据库承担的 JSON 校验责任丢掉。第三关于getter和setter对 Jackson 序列化的影响。hypersistence-utils 默认用 Jackson 做 JSON 序列化反序列化时依赖ObjectMapper。如果ExtraInfo内部字段名和 JSON 键名不一致可以通过JsonProperty标注这里不展开。但我强烈建议为每一个 JSON 字段对应的 POJO 写单元测试序列化和反序列化各测一遍别以为简单 POJO 不会出错。我遇到过的真实情况是POJO 里有一个BigDecimal字段序列化后变成字符串反序列化时再转回 BigDecimal 看起来没问题但查询场景下做数值比较就翻车了。这个坑我放到第五节专门讲。第四写入数据时的用法非常直观ExtraInfo info new ExtraInfo(); info.setSource(wechat); info.setLevel(3); info.setTags(List.of(premium, active)); info.getAttributes().put(lastLoginDevice, iPhone); UserProfile profile new UserProfile(); profile.setUserName(u_10086); profile.setExtraInfo(info); profile.setCreatedAt(LocalDateTime.now()); userProfileRepository.save(profile);查看数据库extra_info列里存储的就是一个合法的 JSON 字符串比如{source:wechat,level:3,tags:[premium,active],attributes:{lastLoginDevice:iPhone}}。读取时UserProfile profile userProfileRepository.findById(1L).orElseThrow(); ExtraInfo info profile.getExtraInfo(); String source info.getSource(); // 直接强类型访问不需要手动调用ObjectMapper.readValue也不需要处理JsonProcessingException受检异常这就是自定义 Type 带来的核心价值——把类型映射的复杂性收敛到框架层业务代码清爽很多。4. 条件查询与函数下推JSON_EXTRACT 和方言函数怎么处理JSON 字段只做存取没什么难度真正有意思的是用 JSON 内部属性来过滤查询条件。比如你想查所有extra_info里source wechat的用户JPQL 直接写WHERE profile.extraInfo.source wechat是不行的因为 Hibernate 在解析 JPQL 时不知道extraInfo内部结构。常规做法有两种第一种原生 SQL JSON_EXTRACT。在 Repository 里用Query(nativeQuery true)public interface UserProfileRepository extends JpaRepositoryUserProfile, Long { Query(value SELECT * FROM user_profile WHERE JSON_EXTRACT(extra_info, $.source) :source, nativeQuery true) ListUserProfile findByExtraInfoSource(Param(source) String source); }这种写法简单直接但坏处也很明显返回值是ListUserProfile可以正常映射实体可如果你只需要查某个 JSON 内部字段的值比如想统计所有用户的level分布原生 SQL 返回的就是Object[]你又得手动转类型。而且一旦 SQL 里涉及动态拼接注入风险和可维护性都会变差。第二种用函数注册让 JPQL 也能用 JSON_EXTRACT。这是 hypersistence-utils 提供的一个亮点功能。它注册了json_extract允许你在 JPQL 或 Criteria 查询里直接写FUNCTION(json_extract, profile.extraInfo, $.source)。例如Query(SELECT p FROM UserProfile p WHERE FUNCTION(json_extract, p.extraInfo, $.source) :source) ListUserProfile findBySource(Param(source) String source);你别小看这层封装它比原生 SQL 优雅得多实体映射、分页、排序这些 JPA 特性都能正常使用而且 Hibernate 生成的 SQL 会包含json_extract(extra_info, $.source) ?数据库依然能利用函数索引MySQL 8.0 支持对 JSON 列创建函数索引。不过我要泼一盆冷水别指望所有 JSON 场景都能用 JPQL 优雅解决。比如JSON_CONTAINS/JSON_TABLE这类复杂函数hypersistence-utils 并没有全部注册硬要用的话还是得退回原生 SQL。我的经验是做好取舍场景推荐方式理由按 JSON 内部字段做等值/范围查询JPQL FUNCTION(json_extract)保持类型安全支持分页排序多条件 JSON 查询、嵌套数组匹配原生 SQL JSON_CONTAINS / JSON_TABLE函数能力更完整批量更新 JSON 内部字段JPA 实体加载后修改由 Hibernate 脏检查自动生成 update 语句只更新 JSON 某个内部属性不读取整行原生 SQL JSON_SET避免大对象全量读写另外补充一个我在实际项目里的做法如果查询过滤的对象确实是 JSON 内部的固定字段而且查询频率很高那就该考虑对这个字段建函数索引或者干脆升级成独立列。数据库的 JSON 能力再强也不该当成常规关系型查询的主力工具。JSON 适合存偶发读取、结构多变、不需要频繁按内部值关联的数据一旦你发现自己天天JSON_EXTRACT(...) xxx这往往说明这个字段其实应该抽出来做成列了。说到函数下推再提一个容易翻车的点MySQL 的json_extract返回的是JSON类型在WHERE json_extract(extra_info, $.level) 2这种场景下MySQL 会自动做类型转换但有时会因隐式转换规则导致索引失效。我建议在 SQL 层面显式写CAST(JSON_EXTRACT(...) AS SIGNED)或者JSON_UNQUOTE(JSON_EXTRACT(...))查询计划会更稳定。在 JPQL 里写FUNCTION(json_extract, p.extraInfo, $.level) 2时Hibernate 生成 SQL 不会自动加 CAST此时如果发现执行计划不对劲就退回原生 SQL 并显式 CAST别死磕 JPQL。5. 高阶方案让 Hibernate 方言支持 JSON 类型函数解决 ifnull 与类型推断问题第五节算是一个进阶技巧也是我踩过坑之后才搞明白的。先说背景hypersistence-utils 的JsonType解决了 Java 对象和数据库 JSON 字段的映射但在JPA 自动生成的 SQL中涉及 JSON 字段的表达式在方言层缺少对应的函数映射导致某些查询结果类型不符合预期。举个例子。假设我要统计每个source的用户数量并且使用 MySQL 的ifnull来保证没有任何记录时也输出 0。你可能会写原生 SQLSELECT ifnull(json_unquote(json_extract(extra_info, $.source)), unknown) AS source, COUNT(*) FROM user_profile GROUP BY source;这个 SQL 在 MySQL 客户端里执行没问题。但如果你试图把这个逻辑融入 JPA 的 Criteria API或者让 Hibernate 的方言在生成分页 Count 查询时正确识别 JSON 字段类型问题就来了——Hibernate 方言不知道json_extract的返回类型是什么。在某些 Hibernate 版本里遇到json_extract(...)作为 select 项时返回的类型会被推断成byte[]因为 MySQL JDBC 驱动默认把未知 JSON 类型映射为 byte[]。你在获取查询结果时如果没有强转类型就非常隐蔽。我当时遇到的现象很典型Hibernate 生成的 count 查询是select count(u.id) from user_profile u由于extra_info是 JSON 类型MySQL 8.0 驱动返回的 JDBC 类型是JSONHibernate 方言里没有json类型的注册信息于是直接抛异常或者返回Object[]。排查了半天最后的解决办法是在自定义方言里注册 JSON 相关的函数和类型码。具体做法是自定义一个方言类继承 MySQL 方言覆盖registerColumnTypes或直接注册函数public class CustomMySQLDialect extends MySQLDialect { public CustomMySQLDialect() { super(); registerFunction(json_extract, new SQLFunctionTemplate( StandardBasicTypes.STRING, json_extract(?1, ?2) )); registerFunction(json_unquote, new SQLFunctionTemplate( StandardBasicTypes.STRING, json_unquote(?1) )); } Override protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.registerColumnTypes(typeContributions, serviceRegistry); typeContributions.contributeColumn(Types.JAVA_OBJECT, json); typeContributions.contributeColumn(Types.VARCHAR, json); } }然后在配置文件里指定spring.jpa.database-platformcom.example.CustomMySQLDialect这里有几个细节要说清楚。第一为什么json_extract注册成返回StandardBasicTypes.STRING因为JSON_EXTRACT返回类型是 JSON而 JSON 在 Hibernate 类型系统里没有原生对应类型。直接注册为 STRING 之后Hibernate 生成 SQL 时会自动在结果集中按字符串处理。对于大多数读取 JSON 内部字符串值的场景这已经足够了。如果需要取数值用cast(? as signed)再配合 CAST 做一次强制转换。第二contributeColumn(Types.JAVA_OBJECT, json)的意思是告诉 Hibernate当某个 Java 类型对应Types.JAVA_OBJECTJDBC 类型时建表语句使用json列类型。这一步的好处是即使实体类里用了Map/List这类没有明确列类型映射的字段Hibernate 自动建表时也能正确生成 JSON 列而不是默认的varbinary或者blob。第三有朋友可能会问我注册成 STRING 之后读出来的 JSON 内部字符串值会不会带着引号确实会。MySQL 的JSON_EXTRACT返回wechat带双引号而JSON_UNQUOTE才会去掉引号。所以功能函数里我把json_unquote也注册了进去实际使用时要自行决定用哪个或者两者嵌套。这也是为什么建议在 SQL 里显式JSON_UNQUOTE而不是依赖 Hibernate 帮你做——Hibernate 不会帮你做语义级的 JSON 值剥离。那自定义方言之后原来的Type(type json)映射会不会被影响不会。自定义方言只是注册函数和列类型JsonType的序列化反序列化逻辑依然由 hypersistence-utils 负责两者是不同层面的东西。方言解决的是数据库 SQL 函数在 JPQL 里怎么暴露和自动建表/读结果集时类型怎么映射这两个问题而 JsonType 解决的是Java 对象与 JDBC 参数之间怎么转换的问题。这个方案的收获是涉及到 JSON 字段的聚合查询、group by 字段抽取、函数嵌套使用时不再因为 Hibernate 的方言盲区而被迫全部改写原生 SQL。对于团队里愿意用 Criteria API 做动态查询的人来说这个自定义方言是刚需。之前某公司项目组碰到的一个查询就是运营后台有一堆动态筛选条件其中会按 JSON 内部的channel字段做下拉筛选用 Criteria API 写的话如果不注册方言函数代码就得拼接原生的 SQL 片段一遍遍json_extract后期维护起来非常痛苦。自定义方言一次性解决之后动态查询的代码风格保持了统一值得花这半小时时间。6. 五个隐藏很深的坑从 BigDecimal 变成字符串到脏检查失效前面几节解决了怎么映射、怎么查的主要问题这一节专门聊我在实战里被坑过、也帮别人排查过的隐蔽问题。每一个都是网上文档很少提及、但一踩就够喝一壶的。坑一BigDecimal 字段被 Jackson 序列化成字符串这是我在一个订单系统里遇到的真实问题。订单表的扩展信息extra_info里有一个discountAmount字段类型是BigDecimal用来记录该订单在某个渠道下享受的特殊优惠。写入的时候没任何问题数据库里也存的是正常数字。但某次同事反馈某个报表查询返回的数据里discountAmount变成了字符串导致前端数字求和全错。排查过程是这样的先把数据库里的 JSON 拿出来看确认没有脏数据然后把实体类加载出来用调试器查看getExtraInfo()返回的对象发现discountAmount的运行时值已经变成了3.50这种带引号的字符串。再进一步定位问题出在Jackson 序列化 BigDecimal 时当数字的 scale 大于某个阈值或精度较高为避免精度丢失会转成字符串。具体来说ObjectMapper默认配置下对于BigDecimal如果小数点后位数过多比如 1E30 这种极值会序列化为1E30而不是数字对象但更常见的是在一些配置了WRITE_BIGDECIMAL_AS_PLAIN或特定JsonGenerator.Feature的环境下行为不一致。解决思路有三种在「序列化时」给ObjectMapper配置JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS或WRITE_BIGDECIMAL_AS_PLAIN全局统一 BigDecimal 的序列化策略。在实体类的 getter 上用JsonSerialize(using ToStringSerializer.class)强制 BigDecimal 字段转为字符串——但这只适合接收方明确要做字符串处理的场景。如果你希望 JSON 里存的是纯数字那么就别让 Jackson 插手改用JsonInclude(Include.NON_NULL)不是关键关键是要查ObjectMapper的WRITE_BIGDECIMAL_AS_PLAIN配置。那为什么 hypersistence-utils 默认会影响 BigDecimal 的表现因为 JsonType 内部使用的 ObjectMapper 如果没有显式配置用的是它自己创建的那个和你 Spring 容器里的 ObjectMapper 不是同一个实例。所以你在 Spring 里配置的那一堆 Jackson 规则对 JsonType 默认不生效。这是最坑的一点你在 Spring 里调好了序列化行为测试通过但 JPA 读取时走的却是另一个 ObjectMapper 实例行为不一致。后来我在项目里直接通过配置把 hypersistence-utils 的 ObjectMapper 换成 Spring 容器里的那个才彻底解决。具体做法是重写 JsonType 的序列化器或看 hypersistence 的文档里有没有提供设置入口不同版本做法不同但这篇文章里重点是把根因讲清楚。坑二实体类 getter/setter 引发的脏检查失效JsonType和 JPA 的实体生命周期配合时有个很隐晦的问题如果 JSON 字段的 POJO 内部包含Map或List你修改了这个 Map 里的某个值之后Hibernate 可能不会认为实体变脏dirty导致更新 SQL 不被生成。举个例子ExtraInfo info profile.getExtraInfo(); info.getAttributes().put(device, iphone); // 没有调用 profile.setExtraInfo(info) // 事务提交后数据库不会变原因很简单Hibernate 的脏检查通过比对实体属性的快照来判断是否更新而profile.extraInfo属性引用没有变还是同一个ExtraInfo对象Hibernate 不知道对象内部发生了变化。在常规字段上你改一下setUserName()就会有反应但集合/Map作为内部字段时这种隐式修改常常被漏掉。解决办法有两个方向在修改完 Json 对象内部后显式调用profile.setExtraInfo(info)或entityManager.flush()让 Hibernate 感知到赋值行为。虽然引用没有变但 setter 的调用会触发属性加载策略使实体被标记为 dirty。重写ExtraInfo的equals/hashCode让 Hibernate 的快照比对能够识别内容变化。这个方案更彻底但如果 POJO 内部结构复杂equals 写起来很费心而且容易踩到hashCode缓存坑。我自己在项目中采用了「初始化后复制对象」策略每次从数据库取出实体后第一时间把extraInfo复制一份放到一个临时变量里所有修改都在复制的对象上操作最后再set回实体。虽然多一次对象复制但脏检查逻辑非常明确不会因为对象复用产生疑问。这个习惯以后你在任何需要修改集合字段时都用得上。坑三HashMap 的乱序问题ExtraInfo里的attributes用的是HashMapJackson 序列化时按 HashMap 的默认迭代顺序输出这会导致一个很不起眼的体验问题数据库里存着的 JSON 键顺序每次可能都不一样前端展示时某些字段顺序不稳定排查问题的时候翻数据库也特别费眼睛。排序本身不影响 JSON 合法性但如果你有团队规范要求 diff JSON 内容比如对比两次更新差异乱序会给排查增加噪音。解决办法是在 POJO 的attributes字段上声明JsonType使用带顺序的 Map或者直接把类型改成LinkedHashMap。实际开发里我早把attributes改成了MapString, Object但运行时用LinkedHashMap初始化既保证序列化顺序稳定又保留接口兼容性。坑四Query里使用 json_extract 返回值类型不匹配原生 SQL 查询SELECT JSON_EXTRACT(extra_info, $.source) FROM ...时MySQL 返回的类型是 JSONJDBC 驱动可能会把它映射成byte[]。你用String去接直接报 ClassCastException。这个坑我调试了很久才发现原因。Hibernate 对原生 SQL 的返回类型映射依赖 SQL 列的 JDBC 类型而 MySQL 8.0 的驱动对 JSON 类型返回的是byte[]。解决办法有三个查询时显式CAST(JSON_EXTRACT(...) AS CHAR)强制返回字符串。查询时JSON_UNQUOTE(JSON_EXTRACT(...))返回值变成字符串。对于非要返回 JSON 类型的场景用JsonNode或byte[]接收再手动转换。我的建议是把前面提到的自定义方言注册json_unquote函数结合使用不管在 JPQL 还是原生查询里都统一返回字符串从源头上消灭类型隐坑。坑五大 JSON 字段导致的加载性能问题不算复杂的技术点但影响范围大。JsonType字段属于基本类型映射字段加载策略默认是EAGER哪怕你只查一列也把整个 JSON 读出来。之前有个业务表extra_info里存了将近 100KB 的嵌套 JSON列表页查询一次 1000 条数据光这个字段的网络传输就占了几 MB。我建议在实体上配合Basic(fetch FetchType.LAZY)使用让 JSON 字段延迟加载只在需要时读取。如果项目规模不大或查询列表允许加载大字段那至少要避免把大 JSON 和经常展示的字段混在同一张表里。可以在设计时拆表把大 JSON 放到xxx_extra副表主表只存主键和稳定字段需要扩展信息时再关联读取。很多人在表设计初期为了省事把能塞的都塞进去等性能出问题再拆成本高得多。7. 用了一段 JsonType 之后的取舍心得写到最后说点个人使用感受吧。hypersistence-utils 的 JsonType 确实是 JPA 处理 MySQL JSON 字段的最省心方案但它不是银弹。用了半年多之后我的一个体会是JSON 字段在 JPA 里映射得越透明越容易让人产生一种它在关系型数据库里就和普通字段一样的错觉于是过度使用最终在性能和一致性上付出代价。JSON 字段本质上应该用来承载结构经常会变、查询条件很少基于内部字段的数据比如扩展属性、第三方回调的原始报文、配置快照。如果是业务核心字段哪怕设计时多建几张表、多做几次 join也比后期在 JSON 里翻山越岭地维护强。另一个体会是映射方案要尽早统一。团队里一旦有人手写了 String Jackson 的方式处理某个 JSON 字段另一个人又在用 JsonType那么查代码的时候会非常混乱。建议一开始就规定好规范凡是 MySQL JSON 列实体类必须用 JsonType 映射推荐使用包级别的TypeDef并且为所有的 JSON POJO 提供单元测试防止序列化策略调整导致线上数据爆炸。规范越早定后面擦屁股的成本越低。最后给一个小的 tips如果你在使用ddl-auto: validate校验表结构注意json类型的列在 MySQL 8.0 和 MariaDB 之间的差异。MariaDB 的JSON是LONGTEXT的别名Hibernate 方言不同校验结果也不同。如果生产环境是 MariaDB但本地开发用 MySQL很容易出现环境不一致导致的隐性问题。跨数据库兼容的项目尽量在 CI 里跑一次真实的数据库实例来验证别依赖内存数据库内存数据库对 JSON 列的支持和线上完全不是一回事。这篇文章的路线是先明确 String 和 Lob 有什么问题再介绍 JsonType 的依赖和实体写法然后讲条件查询和自定义方言最后集中讲隐蔽的坑。按这个顺序走一遍JPA 映射 MySQL JSON 对象这条路上大部分常见的石头你基本都能提前避开了。
阅读完成 · 觉得有帮助?
咨询建站