bigywater.tech
Java · 420

AttributeConverter:实体字段与数据库值之间的转换

AttributeConverter 让实体字段与数据库列可以是不同的类型,但转换器的调用次数、null 处理、脏检查和查询里的行为都有细节。这篇用真实的数据库内容与调用计数量出这些行为。

3000 次
加载 1000 个含两个转换器字段的实体时 convertToEntityAttribute 的调用次数(convertToDatabaseColumn 1000 次)
空串 ≠ NULL
空集合经 CsvConverter 存进数据库的值与 null 集合存进数据库的值
整个查询失败
库里一行 archived = 'X' 时,读到它的 select d from Doc d 的结果(count(d) 不受影响)

转换后的数据库值

实测 · 本机执行输出
  原生 SQL 读到的列:java,jpa,sql(varchar) | 20261005(int4) | Y(varchar) | N(varchar);
  列类型:tags 是 varchar,day 是 integer(int4),archived 与 pinned 都是 varchar 的 Y/N——autoApply 的 Boolean 转换器也应用到了基本类型 boolean 的 pinned 上
  find 回来:tags = [java, jpa, sql],day = 2026-10-05,archived = true,pinned = false(autoApply 的 YN 转换器对 Boolean archived 与基本类型 boolean pinned 都生效)

null 与空值

实测 · 本机执行输出
  persist 一个 tags / day / archived 全为 null 的实体:convertToDatabaseColumn 2 次(其中参数为 null 2 次),convertToEntityAttribute 0 次(其中参数为 null 0 次)
  库里的值:null(varchar) | null(int4) | null(varchar);
  find 回来:tags = null,day = null,archived = null;convertToDatabaseColumn 0 次(其中参数为 null 0 次),convertToEntityAttribute 2 次(其中参数为 null 2 次)
  空集合 new HashSet<>() 存进去:库里的 tags = (varchar); (空串,不是 NULL)
  读回来:tags = [],tags == null 为 false,size = 0

脏检查

实测 · 本机执行输出
  find 之后直接 m.tags.add("new-tag")(原地修改)并 commit:新增 SQL 1 条;库里的 tags = java,jpa,new-tag,sql(varchar);
  换成一个内容完全相同的新 Set 对象并 commit:新增 SQL 0 条(按转换后的值比较,没有变化)
  换成一个多了 x2 的新 Set 对象并 commit:新增 SQL 1 条;库里的 tags = java,jpa,new-tag,sql,x2(varchar);
  LocalDate(不可变类型)赋成一个相等的新对象并 commit:新增 UPDATE 0 条(转换后的值相等,不更新)

调用次数

实测 · 本机执行输出
  加载 1000 个实体:convertToDatabaseColumn 1000 次(其中参数为 null 0 次),convertToEntityAttribute 3000 次(其中参数为 null 0 次)
  没有任何修改时 flush:convertToDatabaseColumn 0 次(其中参数为 null 0 次),convertToEntityAttribute 0 次(其中参数为 null 0 次)
  改了一个实体的 tags 之后 flush:convertToDatabaseColumn 3 次(其中参数为 null 0 次),convertToEntityAttribute 1 次(其中参数为 null 0 次)
  persist 100 个实体并 flush:convertToDatabaseColumn 300 次(其中参数为 null 0 次),convertToEntityAttribute 100 次(其中参数为 null 0 次)

查询里的转换

实测 · 本机执行输出
  参数 d.archived = :a(true):命中 1 行(参数被转换成 Y 再比较)
  字面量 d.archived = true:命中 1 行(字面量也被转换)
  参数 d.day = :day(LocalDate):命中 1 行(参数被转换成整数 20261005)
  范围比较 d.day >= :day and d.day < :day2:命中 1002 行(转换后的整数 yyyymmdd 保持了顺序,可以做范围查询)
  d.tags like :p:QueryArgumentException:Argument [%java%] of type [java.lang.String] did not match parameter type [java.util.Set (n/a)]
  投影 select d.day, d.archived:2026-10-05(LocalDate), true(Boolean)(投影结果也被转换回实体字段类型)

无法转换的值

实测 · 本机执行输出
  find 读到 archived = 'X' 的行:PersistenceException ← IllegalArgumentException:数据库里的 Y/N 列出现了无法识别的值:'X'
  不读取该列的聚合查询 count(d):1003 行(没有触发转换,不受影响)
  select d from Doc d(包含坏行):PersistenceException ← IllegalArgumentException:数据库里的 Y/N 列出现了无法识别的值:'X'

分步演示

AttributeConverter
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Hibernate ORM 6.6.4.Final、PostgreSQL JDBC 42.7.4、本机 PostgreSQL 16.15;两个转换器是自写的(计数用静态计数器);hbm2ddl 自动为 Y/N 列生成了检查约束(第 6 步先删除了它)
    • 没有验证:@Convert 用在集合元素 / Map 键、@Convert 与 @Enumerated 的冲突、Hibernate 的 @JavaType / @JdbcType 替代方案、转换器内注入 Spring Bean、其它数据库(Oracle / MySQL)上的 Boolean 列类型、JSON 列的转换
    京ICP备2026067502号-1京公网安备11010502063487号