资讯动态

PostgreSQL JSONB与MyBatis-Plus自动注入方案详解

发布时间:2026/10/17 21:22:41 来源:尧图企业网站定制
先亮个结论PostgreSQL 里的 JSONB 是真好用但放到 Java 生态尤其是我平时用得最多的 MyBatis-Plus 里你会发现默认行为经常“对不上号”。实体里一个 Map 或自定义对象数据库里一个 jsonb 列插件默认的自动映射根本不知道该把两端怎么接起来更麻烦的是插入数据时希望自动带上的 create_time、update_time 这类系统字段一旦和扩展字段放一起各种“该写没写”、“写了类型不对”的毛病全冒出来。这篇文章不绕弯子直接围绕“PostgreSQL 中使用 JSONB 类型并结合 MyBatis-Plus 实现自动注入”这个场景把我在生产环境里用过、踩过坑、最后稳定跑着的几种方案挨个讲清楚。会讲原理、贴代码、给配置最后把常见的坑和排查思路也整理成清单。适合正在搞 Spring Boot MyBatis-Plus PostgreSQL 这套组合的 Java 开发如果你刚被 JSONB 映射和自动填充折腾过那这篇就是给你准备的。1. 先弄明白JSONB 与自动注入到底要解决什么问题1.1 JSONB 的典型用途和 Java 侧的真实痛点PostgreSQL 从 9.4 开始提供 JSONB 类型相比普通 JSON 类型它的优势是二进制存储、支持索引GIN、支持丰富的 jsonb 运算符和函数。实际项目里通常拿它存三类数据一类是业务上的“扩展字段”比如订单表里不同渠道带过来的不同属性一类是高度动态的配置快照比如某个规则引擎跑出来的完整参数还有一类是日志、审计信息这类结构不固定的数据。Java 侧的问题从来都不在数据库而在于 ORM 框架对 jsonb 的支持。MyBatis 本身有 TypeHandler 机制但默认并不认识 PostgreSQL 的 jsonb 类型。MyBatis-Plus下面简称 MP虽然封装了很多默认行为但在 JSONB 面前也只是“能用默认映射硬闯”结果通常就是读出来是一个 String 或 PGobject没法直接当对象用写进去变成 varchar 的字符串列的 jsonb 类型校验直接报错查询条件里想用 jsonb 的-、-、运算符Wrapper 很难直接表达。这些问题叠加 MP 的自动填充功能之后场景就更复杂了。所谓“自动注入”在 MP 语境里通常指两件事一是MetaObjectHandler实现的字段自动填充insert 时自动带 create_timeupdate 时自动带 update_time 这类操作二是通过 SQL 注入器动态给 Mapper 注入自定义方法。而一旦实体里同时存在 jsonb 字段和需要自动填充的普通字段顺序、类型、序列化方式任何一个环节出错数据就写不进去。1.2 自动注入在 MyBatis-Plus 里的两层含义第一层是“自动填充”Auto Fill对应TableField(fill FieldFill.INSERT / UPDATE / INSERT_UPDATE)注解加MetaObjectHandler实现类。它的执行时机是 MP 在生成 INSERT 或 UPDATE 语句之前通过反射把 handler 里设置的值注入实体字段再拼进 SQL。这个过程发生在应用层不经过数据库触发器所以“填充什么值、填充到哪些字段”完全由 Java 代码控制。第二层是“自动注入器”ISqlInjector对应DefaultSqlInjector的子类或自定义AbstractMethod。它的执行时机更早在 Mapper 接口加载阶段就把自定义方法比如insertWithJsonb、updateJsonbField的逻辑注入进去。如果你需要一些 MP 标准方法表达不好的 SQL 片段比如INSERT INTO ... (extra) VALUES ({}::jsonb)、或者WHERE extra {key:value}::jsonb就可以走这条路。理解这两层区别很重要因为很多教程把“自动注入”和“自动填充”混为一谈。在 JSONB 场景里你可能两个都需要用自动填充解决 create_time、create_by 这类审计字段的默认值用 TypeHandler 或自定义 SQL 注入解决 jsonb 列的正确读写。2. 准备工作和基础选型别在第一步就埋坑2.1 依赖与环境的推荐组合先说清楚我验证过的组合能让后面几种方案跑得最顺PostgreSQL 12 以上实际上 9.4 都支持 jsonb但 12 以上对 jsonb 的函数和 GIN 索引支持更完整Spring Boot 2.7.xJava 8 以上都可以Java 11/17 也没问题mybatis-plus-boot-starter 3.5.x驱动用 PostgreSQL JDBC 42.x序列化工具推荐 Jacksonspring-boot-starter-json 自带后面 TypeHandler 默认就用 ObjectMapper 处理。Maven 核心依赖大概是这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency有一个比较容易忽略的细节如果你用的连接池是 HikariCPSpring Boot 默认建议给数据源配置加一个参数dataSource-properties: stringtypeunspecified。这个参数不能乱加它只建议在你已经用 TypeHandler 或 PGobject 方案时不用加因为驱动默认会把字符串参数当作 varchar 发送而 jsonb 列要求接收 jsonb 类型输入两边类型不一致就容易报“column is of type jsonb but expression is of type character varying”。我们后面用 PGobject 或者::jsonb转换来规避这个报错并不依赖它。2.2 表结构和实体模型的初始化用一个线上最常见的“扩展字段”场景来做演示一张业务订单表除了固定字段还有一列extra用来存渠道定制属性。建表 SQLCREATE TABLE biz_order ( id BIGSERIAL PRIMARY KEY, order_no VARCHAR(64) NOT NULL, amount NUMERIC(10,2) NOT NULL, status VARCHAR(32) NOT NULL, extra JSONB NOT NULL DEFAULT {}::jsonb, create_time TIMESTAMP NOT NULL DEFAULT now(), update_time TIMESTAMP NOT NULL DEFAULT now(), created_by VARCHAR(64) );对应的 Java 实体Data TableName(biz_order) public class BizOrder { TableId(type IdType.AUTO) private Long id; private String orderNo; private BigDecimal amount; private String status; /** * 扩展字段数据库类型为 jsonb */ private MapString, Object extra; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableField(fill FieldFill.INSERT) private String createdBy; }这里extra用MapString, Object接收是最通用的做法既能覆盖大部分动态结构又不至于让你为每个形态单独建类。如果你们公司有规范的数据字典约束更推荐用具体的 POJO 类比如ExtraInfo可读性和校验性都更好。需要特别强调一个容易踩的坑建表时用了DEFAULT {}::jsonb但 MP 的 INSERT 语句是列出所有字段的如果实体里extra是 nullMP 仍然会把null写进 SQL这时列默认值不会生效最终写入的是一个null而不是空对象{}。如果你希望数据库兜底就必须在 Java 侧保证extra至少是一个空 Map或者用后面的自定义注入方案让 SQL 里显式写{}::jsonb。2.3 为什么常规配置解决不了 jsonb 映射MP 默认的自动映射做两件事根据TableField和属性名生成 SQL 列再用ResultSet按类型取数。Integer、String、LocalDateTime这些都有内置 TypeHandler不会出问题。但Map走的是默认的UnknownTypeHandler而 PostgreSQL 驱动把一个 jsonb 列读出来时返回的类型是PGobjectgetObject拿到的是一个类型为“jsonb”的对象不是 Map更不是 String。也就是说你不告诉 MP“这个字段怎么读、怎么写”它就只能瞎猜。TypeHandler 存在的意义就是从 JDBC 层接管这个字段的读写让 MP 在 set 参数时调用setParameter在读结果时调用getNullableResult。3. 方案一自定义 TypeHandler从根上解决 Java 对象和 jsonb 的双向转换3.1 BaseTypeHandler 子类的实现思路TypeHandler 的原理不复杂MyBatis 在执行 SQL 参数填充时会按字段找到对应 TypeHandler调用它的setNonNullParameter执行完查询后再调用它的三个getNullableResult重载把 JDBC 列值转成 Java 类型。我们要做的事就两件写的时候把 Java 的Map/ POJO通过 Jackson 序列化成 JSON 字符串再包一层PGobject显式指定数据库类型为jsonb读的时候把PGobject拿到的字符串通过 Jackson 反序列化成 Java 类型。其中关键点就是用PGobject包一层并设置setType(jsonb)。这样 PostgreSQL 驱动会按 jsonb 类型发送参数而不是按普通字符串发送避免类型不一致的报错。这也是为什么我不建议直接用setString(ps, jsonString)硬写的原因——那本质上是在让 jsonb 列接收 varchar绝大多数情况下都会撞上类型校验。实现代码public class JsonbTypeHandlerE extends BaseTypeHandlerE { private static final ObjectMapper MAPPER new ObjectMapper(); private final ClassE clazz; public JsonbTypeHandler(ClassE clazz) { this.clazz clazz; } Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { try { String json MAPPER.writeValueAsString(parameter); PGobject pgObject new PGobject(); pgObject.setType(jsonb); pgObject.setValue(json); ps.setObject(i, pgObject); } catch (JsonProcessingException e) { throw new SQLException(Failed to convert parameter to jsonb, e); } } Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private E parse(String json) { if (json null || json.isEmpty()) { return null; } try { return MAPPER.readValue(json, clazz); } catch (IOException e) { throw new RuntimeException(Failed to parse jsonb column, e); } } }几个实现细节非常关键我用了泛型ClassE这样同一个 Handler 既能处理Map又能处理 POJO不用每个类写一个。MP 里通过TableField(typeHandler ...)指定时会直接实例化并传入字段类型。rs.getString(columnName)能拿到 jsonb 的原始文本是因为 PostgreSQL 驱动对PGobject的toString()就是它的原始 JSON 字符串。parse里如果用MAPPER.readValue(json, clazz)注意如果 json 是{}而 clazz 是Map.class结果是空 Map不会报错但如果 json 是数组而你给它一个 POJO 类会抛异常。这个在后面排错里会再提。3.2 在实体字段上使用以及 XML 和 Wrapper 的配合方式在BizOrder实体中标注TableField(typeHandler JsonbTypeHandler.class) private MapString, Object extra;这样 MP 生成的 INSERT、UPDATE、SELECT 语句都会自动应用这个 TypeHandler。需要注意MP 默认生成的updateById只更新非 null 字段而extra这个字段更新逻辑要看你业务需要如果允许把扩展字段整个覆盖那保持默认就可以如果希望只更新 extra 里的某个子 key就不能靠普通的updateById了得用自定义 SQL 或 XML。还有一种情况如果你用了Select注解或 XML 里的 resultMap也需要手动标注 TypeHandler。例如 XML 写法resultMap idorderMap typecom.example.entity.BizOrder id propertyid columnid/ result propertyorderNo columnorder_no/ result propertyextra columnextra typeHandlercom.example.handler.JsonbTypeHandler/ /resultMap否则查询结果会因为 UnknownTypeHandler 直接返回 PGobject赋值给 Map 字段时报反序列化错误。Wrapper 的查询条件如果要用 jsonb 的运算符建议直接用apply方法比如QueryWrapperBizOrder wrapper new QueryWrapper(); wrapper.apply(extra {0}, {\channel\:\app\}::jsonb); ListBizOrder list baseMapper.selectList(wrapper);这里用{0}占位而不是直接字符串拼接是为了让 MP 走参数占位符避免 SQL 注入问题同时让::jsonb留在 SQL 片段里正常生效。这是我在实际项目里最常写的一行代码。4. 方案二用 MetaObjectHandler 实现字段自动填充并和 JSONB 字段共存4.1 标准自动填充的配置方式MP 的自动填充并不神秘核心就两步第一步实体字段上用TableField(fill FieldFill.INSERT)或FieldFill.INSERT_UPDATE标注告诉 MP 哪些字段需要自动处理。第二步写一个MetaObjectHandler的实现类在insertFill和updateFill两个方法里给字段赋值。“自动填充”的触发时机有两个特点需要记住只针对 MP 自带的方法insert、insertBatch、updateById、update等你自定义的 SQL 不会触发它而且插入时它修改的是实体对象本身不是数据库的默认值机制。这意味着如果一次批量插入几百条数据填充逻辑会被反复执行所以 handler 里逻辑要轻不要做数据库查询。一个最常用的实现Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, createdBy, String.class, getCurrentUser()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } private String getCurrentUser() { // 从上下文取当前登录人比如 RequestContextHolder 或自定义上下文 return system; } }这里我用的是strictInsertFill而不是老的setFieldValByName。两者的差别在于strictInsertFill会先检查实体里这个字段是不是已经有值有值就不覆盖而setFieldValByName是不管三七二十一直接覆盖。默认用strictInsertFill更安全比如你在代码里显式给某个订单设置了createTime结果自动填充把它改成当前时间那可就要查半天了。4.2 自动填充与 JSONB 字段共存的三条经验这个方案本身不直接处理 jsonb但在同时有 jsonb 字段和审计字段的表上有几条经验是踩过坑才换来的第一不要在insertFill里给 jsonb 字段填充默认值。比如有人试图在 handler 里写this.setFieldValByName(extra, new HashMap(), metaObject)看起来能解决空值问题但实际上会让所有 INSERT 语句都多一次 Map 序列化而且如果后续 SQL 改成显式写{}::jsonb这里又会重复覆盖。更干净的方案是让实体字段初始化为空 Map或者在插入前统一处理。第二updateFill只在 MP 的 update 方法中生效但如果你写了自定义 SQL 直接从 jsonb 里取一个子字段去更新比如SET extra jsonb_set(extra, {stock}, 10)这个 SQL 是感知不到MetaObjectHandler的。最容易被坑到的场景是“我明明配了自动填充但更新个 jsonb 子字段后 update_time 没变”。原因就在于你走的是Update注解或 XML不是 MP 的 updateById。解决方法是把 update_time 也显式写进 SQL或者用后面要讲的 SQL 注入器方案把“更新 jsonb 子字段 更新 update_time”封装成一个原子操作。第三使用insertBatch/saveBatch时MP 对批量插入的自动填充有性能损耗吗有但没那么夸张。一次插入几千条每个对象反射塞两三个字段实测影响不大。真正损耗大的是你给 jsonb 字段配置的 TypeHandler 在批量模式下的反复序列化这后面会专门讲。4.3 什么时候适合用“数据库默认值 自动填充”的组合如果你们的 DBA 喜欢把约束都放到数据库层也可以让 create_time、update_time 用数据库默认值兜底Java 侧只依赖MetaObjectHandler做应用层的填充。两边的差异在于数据库默认值对任何 SQL 都生效包括别人用 navicat 手插的数据应用层填充只对走 MP 的代码生效。最稳的组合是两层都做create_time 数据库默认值now() 应用层自动填充二者结果一致不会冲突。我个人的建议是别把 jsonb 字段的默认值交给数据库兜底因为空 Map 和 null 的语义差异太大。数据库DEFAULT {}::jsonb只能防止裸 SQL 插 null但 Java 实体里如果不处理MP 还是会把 null 写进去。所以要么实体里初始化private MapString, Object extra new HashMap()要么在插入前统一把 null 改成空 Map两条路选一条走通就行。5. 方案三自定义 SQL 注入器把 JSONB 操作封装成 Mapper 方法5.1 为什么要用自定义 SQL 注入器而不是手写 XML方案一和方案二合起来已经解决了“jsonb 字段读写”和“审计字段自动填充”两个问题。但遇到 “更新 jsonb 里的某个 key”“按 jsonb 内部条件批量更新”“插入时动态构造 jsonb 字段”这类高级操作Wrapper 和注解都写不利索每次都要手写 XML 又很累。自定义 SQL 注入器就是 MP 提供的“让 Mapper 拥有新方法”的官方通道。和直接在Update注解里写 SQL 相比注入器方案的优势有三个一是方法名和 Mapper 接口绑定调用风格和普通 MP 方法一致二是可以复用 MP 的TableInfo机制自动拼表名、列名减少硬编码三是可以在同一个方法定义里插入MetaObjectHandler的自定义逻辑比如注入当前用户到 jsonb 里。缺点是学习曲线略陡需要对 MP 的AbstractMethod和ISqlInjector有一定了解。但对于需要把 jsonb 操作沉淀为团队通用能力的场景一次封装长期受益。5.2 从 AbstractMethod 继承实现“更新 jsonb 子字段”的通用方法以“按主键更新 extra 中某个 key”为例我希望 Mapper 里能有一个方法int updateJsonbField(Param(id) Long id, Param(field) String field, Param(value) String value);它会生成如下 SQL 并执行UPDATE biz_order SET extra jsonb_set(extra, ARRAY[field], value::jsonb), update_time now() WHERE id #{id}这样不仅能更新子字段而且顺带把 update_time 一起维护不需要依赖updateFill彻底解决 4.2 里提到的自定义 SQL 不走自动填充的问题。实现一个自定义方法public class UpdateJsonbField extends AbstractMethod { Override public MappedStatement injectMappedStatement(Class? mapperClass, Class? modelClass, TableInfo tableInfo) { String sql String.format( UPDATE %s SET %s jsonb_set(%s, ARRAY[#{field}], CAST(#{value} AS jsonb)), update_time now() WHERE id #{id}, tableInfo.getTableName(), tableInfo.getFieldList().stream() .filter(f - extra.equals(f.getProperty())) .findFirst() .map(TableFieldInfo::getColumn) .orElse(extra), tableInfo.getFieldList().stream() .filter(f - extra.equals(f.getProperty())) .findFirst() .map(TableFieldInfo::getColumn) .orElse(extra) ); SqlSource sqlSource languageDriver.createSqlSource(configuration, sql, modelClass); return addInsertMappedStatement(mapperClass, modelClass, updateJsonbField, sqlSource); } }这里要注意addInsertMappedStatement这个方法名有点误导但它其实同时支持 insert 之外的语句类型。MP 内部会按你的 SQL 模板来判断返回类型所以方法名保持和业务语义一致即可。然后在SqlInjector子类里注册Component public class MySqlInjector extends DefaultSqlInjector { Override public ListAbstractMethod getMethodList(Class? mapperClass, TableInfo tableInfo) { ListAbstractMethod methods super.getMethodList(mapperClass, tableInfo); methods.add(new UpdateJsonbField()); return methods; } }再在 Mapper 接口里声明public interface BizOrderMapper extends BaseMapperBizOrder { int updateJsonbField(Param(id) Long id, Param(field) String field, Param(value) String value); }这样bizOrderMapper.updateJsonbField(...)就能直接调用。实际执行时PostgreSQL 的jsonb_set函数会返回新的 jsonb 值并覆盖extra而update_time now()是数据库层的时间不会依赖应用服务器时钟这在多实例部署时更可靠。5.3 扩展思路把“插入时自动构造 jsonb”也做成注入方法同样的思路还可以做一个insertWithJsonb方法解决“实体里 extra 为 null但数据库列不允许 null需要显式写入空对象”的情况。SQL 模板核心片段INSERT INTO biz_order (order_no, amount, status, extra, create_time, update_time) VALUES (#{orderNo}, #{amount}, #{status}, COALESCE(CAST(#{extra} AS jsonb), {}::jsonb), now(), now())注意这里CAST(#{extra} AS jsonb)把参数转成 jsonb如果extra为 null就用COALESCE兜底为空对象。如果你在实体里初始化了new HashMap()其实也可以不这么写但 COALESCE 这对“从外部传 null”的情况更稳因为实体初始化的空 Map 照样会被序列化成{}还能正常写入 jsonb。我更推荐的是另一个做法完全放弃 MP 的自动填充来处理 jsonb把“补充默认元数据”放到注入器 SQL 里写成比如COALESCE(#\{extra\}, {created_by:system}::jsonb)这样一个插入方法就能同时解决 jsonb 空值和审计字段注入两个需求。但前提是你们团队对这种“SQL 里写业务元数据”的风格不反感。如果希望保持业务逻辑在 Java 层建议还是使用MetaObjectHandler TypeHandler 的组合更直观。6. 三种方案怎么选对比和适用边界三种方案并不是互斥关系它们解决的问题各有侧重。我见过最稳的组合方案是TypeHandler 做字段映射方案一 MetaObjectHandler 做审计字段方案二 自定义 SQL 注入器做 jsonb 高级操作方案三。但如果你想最小化依赖也可以只选其中一两条。从功能覆盖度上看需求方案一 TypeHandler方案二 MetaObjectHandler方案三 SQL注入器实体读 jsonb 并转成 Map/POJO支持不支持需要配合 TypeHandler写入时自动塞 create_time 等字段不支持支持可在 SQL 里写 now()更新 jsonb 的子 key不支持不支持原生支持按 jsonb 字段条件查询可通过 Wrapper apply不支持原生支持映射到 Java 的类型错误规避核心能力无关需要在 SQL 里显式转团队上手成本中低较高从性能角度说三种方案在写 jsonb 字段时都需要把 Java 对象序列化成 JSON 字符串。方案一和方案三其实最终都要走 String 或 PGobject 参数差异不大真正影响性能的是你序列化的数据本身。我实测过一个 20 层嵌套、含数组和长文本的扩展字段单次序列化大约在 0.2~0.5ms单条插入的感受不明显但如果批量插入 1 万条且每条都有大 JSON序列化时间会累积到秒级。这时候优先考虑要不要把这么大的数据拆到单独的表而不是塞进 jsonb。维护成本上方案一需要维护一个 TypeHandler 和一个序列化器方案二维护一个 Handler 和实体注解方案三维护一个注入器和若干 Method 类成本最高。一个很现实的问题如果你的项目里 jsonb 字段不止一个表实体也不止一个方案三里的方法名和 SQL 模板一旦写死某列名多表时容易踩坑。我的建议是方案三只给“高频、跨表统一”的操作使用比如统一的updateJsonbField通过TableFieldInfo动态找列名这个处理能大幅提高复用性。从可靠性角度说方案二完全依赖应用层时间在服务器时钟和数据库时钟不一致时写入的时间会有偏差。虽然这个偏差一般可以忽略但如果你的规范要求“以数据库时间为准”方案三里写now()更契合需求。反过来如果你们做了逻辑删除、多租户字段、乐观锁这些 MP 扩展功能方案三需要手工处理这些字段复杂性就上去了这也是为什么不建议把所有 SQL 都换成自定义注入器。7. 常见问题与排查技巧实录7.1 写入报错column extra is of type jsonb but expression is of type character varying这个报错是最典型的基本对应两种可能。第一种是用了TableField(typeHandler ...)但 TypeHandler 里setNonNullParameter直接调了ps.setString而不是包PGobject驱动就把参数当 varchar 发过去了数据库类型不匹配。第二种是 XML 里写了#{extra}但没有指定jdbcType或 TypeHandler用了默认处理器。解决办法方案一的实现里用PGobject.setType(jsonb)是正路。如果你不想每个字段都写 TypeHandler还有一个替代做法是把实体里 jsonb 字段类型改成String然后数据库列用jsonb配合驱动配置stringtypeunspecified也能跑通代价是 Java 侧多一次手动序列化和反序列化。7.2 读出来为什么是 {} 字符串而不是一个 Map这个问题的根源往往是查询语句没有走 TypeHandler。比如你用Select(select * from biz_order where id #{id})返回实体MP 在处理这种注解查询时如果实体字段上的TableField(typeHandler ...)没有被识别它就不会应用 TypeHandler结果返回 PGobject 的字符串表示赋值给 Map 字段时直接类型转换失败或者在某些宽松配置下赋成了一个字符串。排查方法是先在数据库客户端里查一下这个字段真实返回什么确认没问题后在 XML 的 resultMap 里显式标注typeHandler。另外实体字段如果是Object类型MyBatis 是不允许直接映射的必须明确类型。最省事的方式还是全程使用 MP 的selectById/selectList这样实体上的注解才会生效。7.3 自动填充的 create_time 没有写进去先确认三件事实体字段是不是加了TableField(fill ...)MetaObjectHandler有没有被 Spring 管理组件扫描是否覆盖handler 里的字段名和实体字段名是否一致。注意strictInsertFill只在字段值为 null 时才填充如果你在代码里先 new 了一个实体set 了 createTime又调 insertcreateTime 会保留你设置的而不是被覆盖。还有一个很少人注意的坑MetaObjectHandler在执行时实体里的List、Map等复杂字段如果被初始化过反射取值的开销会比普通字段大一些但不会导致填充失败。真正导致“没写进去”的原因八成是 SQL 里传的字段名和数据库列名对不上比如数据库列是create_time实体属性是createTimeMP 正常情况下会做驼峰映射但如果你全局配置里关闭了map-underscore-to-camel-case那就要检查实体上有没有TableField(create_time)。7.4 批量插入时 jsonb 性能突然变差saveBatch默认是分批执行每批 1000 条左右如果每条都经过 TypeHandler 的 Jackson 序列化堆内存里会同时存在挺多 JSON 字符串。我遇到过一个极端情况一个 50 个字段的 POJO 序列化出来有 10KB批量插入 5 万条结果 GC 频繁插入时间慢得离谱。优化方向有三个一是给 ObjectMapper 配置复用单例不要在 TypeHandler 里每次 new二是批处理时把rewriteBatchedStatementstrue加到 JDBC URL 上让驱动真正走批量三是如果 JSON 数据确实很大考虑列级压缩或拆表。经验值是单条 JSON 超过 2KB 时就要开始关注批量插入的耗时了不能只在小数据量下测过就上线。7.5 jsonb 查询条件用不上 GIN 索引这个不算 bug但经常被误判为 bug。比如WHERE extra - key value这种写法即使建了 GIN 索引也未必会走因为-返回的是 jsonb索引优化器不一定能用上。要走 GIN 索引标准写法是用包含运算符WHERE extra {key: value}::jsonb所以在 Java 侧用 Wrapper 时尽量用apply(extra {0}, {\key\:\value\}::jsonb)而不是apply(extra - key {0}, ...)。后者在数据量大的时候全表扫描体验很差。8. 我个人实操中的体会半年前第一次在项目里混用 jsonb 和 MP 时我走了不少弯路最深的体会是方案选型之前先别急着写代码要去确认你们的项目里 jsonb 到底是“存”还是“查”。如果只是把一段结构化数据塞进去偶尔整读整写方案一 方案二就够用了整那些 SQL 注入器反而是过度设计。如果高频地按 jsonb 内部字段筛选、更新子字段那方案三不是可有可无的加分项而是必需品。还有一个小经验数据库里的 jsonb 字段命名尽量在所有表里保持一致统一叫extra或者attributes不要一张表叫extra、另一张表叫ext_info。这样写自定义 AbstractMethod 的时候可以通过统一的字段名在TableInfo里找到列名一个复用方法就能服务所有表。如果各表的 jsonb 列名不一致注入器方法就得给每张表传一个列名参数或者写多个实现维护成本立刻上来了。最后关于自动注入的时机我现在的习惯是能用数据库函数解决的比如now()、jsonb_set绝不放到应用层做能用 MP 标准机制解决的比如 create_time 填充绝不手写 SQL只能自己写 SQL 的才引入自定义注入器。这样一套下来代码里 SQL 硬编码最少TypeHandler 一个类通用Handler 一个类管审计字段真正需要手写 SQL 的场景少之又少项目的可维护性能好不少。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑