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