Hibernate Envers审计表主键异常致PostgreSQL索引过大问题排查
问题描述
使用Hibernate Envers 6.3.0执行审计操作时,插入数据触发PostgreSQL报错:
Caused by: org.postgresql.util.PSQLException: ERROR: index row size 4624 exceeds btree version 4 maximum 2704 for index "cm_cm_property_name_values_aud_pkey" Detail: Index row references tuple (0,17) in relation "cm_cm_property_name_values_aud". Hint: Values larger than 1/3 of a buffer page cannot be indexed. Consider a function index of an MD5 hash of the value, or use full text indexing.
经排查,问题源于审计表主键包含大文本字段cm_cm_value,导致索引长度超出PostgreSQL限制。
原表结构:
create table cm_property_name_values ( cm_property_id uuid not null, cm_value text, cm_name varchar(255) not null, primary key (cm_property_id, cm_name) );
Envers生成的审计表结构:
create table cm_cm_property_name_values_aud ( cm_rev integer not null, cm_cm_property_id uuid not null, cm_cm_value text not null, cm_cm_name varchar(255) not null, cm_revtype smallint, primary key (cm_cm_property_id, cm_rev, cm_cm_value, cm_cm_name) );
原因分析
你的原表cm_property_name_values对应的是无独立主键的集合映射(如@ElementCollection)或嵌入式组件。Envers针对这类没有自身唯一标识符的实体/集合,会将原表所有字段加上版本号rev组合成审计表主键,以此保证每条审计记录的唯一性。这就导致大文本字段被纳入主键,触发PostgreSQL的索引长度限制。
官方文档提到的「原实体ID与版本号的组合」主键,仅针对拥有独立主键的普通实体,这类实体的审计表主键才是原ID + rev。
解决方案
方案1:调整实体映射,给集合/组件添加独立主键
这是最规范的解决方式,将原集合映射改为独立实体关联:
- 创建对应
cm_property_name_values的实体类,新增独立主键:
@Entity @Table(name = "cm_property_name_values") @Audited public class PropertyNameValue { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 新增独立主键 @ManyToOne @JoinColumn(name = "cm_property_id") private Property property; @Column(name = "cm_value") private String value; @Column(name = "cm_name", length = 255) private String name; // getter、setter }
- 主实体中用
@OneToMany替代原@ElementCollection:
@Entity @Audited public class Property { @Id private UUID id; @OneToMany(mappedBy = "property", cascade = CascadeType.ALL) private List<PropertyNameValue> nameValues; // 其他字段、getter、setter }
调整后,Envers生成的审计表主键会是实体主键id + 版本号rev,避免大文本字段进入主键。
方案2:自定义审计表主键策略(针对无法修改实体映射的场景)
如果无法调整原实体结构,可通过@AuditTable和@PrimaryKeyJoinColumn自定义审计表主键,需保证唯一性:
- 编写自定义审计实体:
@Entity @Table(name = "cm_cm_property_name_values_aud") public class PropertyNameValueAud { @EmbeddedId private PropertyNameValueAudId id; @Column(name = "cm_revtype") private Short revType; @Column(name = "cm_cm_value") private String value; // getter、setter @Embeddable public static class PropertyNameValueAudId implements Serializable { @Column(name = "cm_cm_property_id") private UUID propertyId; @Column(name = "cm_rev") private Integer rev; @Column(name = "cm_cm_name") private String name; // 重写equals、hashCode方法 } }
- 在原集合映射上指定自定义审计实体:
@ElementCollection @CollectionTable(name = "cm_property_name_values", joinColumns = @JoinColumn(name = "cm_property_id")) @Audited(targetAuditMode = RelationTargetAuditMode.AUDIT) @AuditOverride(forClass = PropertyNameValue.class, auditEntityClass = PropertyNameValueAud.class) private Map<String, String> nameValues;
此方式需手动维护审计表结构,确保Envers能正确写入和查询审计数据。
方案3:数据库层面临时规避(不推荐)
如果以上方案均无法实施,可按PostgreSQL提示将审计表主键索引改为哈希索引:
DROP INDEX cm_cm_property_name_values_aud_pkey; CREATE UNIQUE INDEX cm_cm_property_name_values_aud_pkey ON cm_cm_property_name_values_aud USING hash (cm_cm_property_id, cm_rev, cm_cm_value, cm_cm_name);
但该方法可能影响Envers查询性能,且哈希索引不支持范围查询,仅作为临时应急方案。
内容的提问来源于stack exchange,提问作者martijnbrinkers

