Hibernate插入DB2时排除自动生成标识列的报错问题
最近我在用Hibernate向DB2数据库插入一个带@EmbeddedId的实体时,直接踩了坑,报了一连串错误:
SQL Error: -798, SQLState: 428C9 DB2 SQL Error: SQLCODE=-798, SQLSTATE=428C9, SQLERRMC=ID, DRIVER=4.9.78
SQL Error: -727, SQLState: 56098 DB2 SQL Error: SQLCODE=-727, SQLSTATE=56098, SQLERRMC=2;-798;428C9;ID, DRIVER=4.9.78
On release of batch it still contained JDBC statements
Error Refeshing Firmware Level Sources :org.springframework.dao.InvalidDataAccessResourceUsageException: could not perform addBatch; SQL [insert into Db2Schema.table_entity ( ID, KEY1, KEY2, KEY3, .....) values (?, ?, ?, ?, ......)]; nested exception is org.hibernate.exception.SQLGrammarException: could not perform addBatch
先贴一下我当时的实体代码,方便大家对照:
原实体类
@Entity @Table(name="table_entity", schema="Db2Schema") @DynamicUpdate @DynamicInsert public class TableEntity implements Serializable { private static final long serialVersionUID = 1L; @EmbeddedId private TableEntityKey tableEntityKey; // 其余实体字段省略 }
原可嵌入主键类
@Embeddable public class TableEntityKey implements Serializable{ @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="ID", insertable = false, updatable = false) // 之前试过加@Transient,但也没解决 private Integer id; @Column(name="key1", length=254) // 其余主键字段省略 }
问题根源分析
折腾了半天,终于搞懂了:DB2的IDENTITY自增列有个硬性规则——绝对不允许在INSERT语句里显式指定该列的值,哪怕传NULL都不行。
而我的问题出在两个地方:
- 虽然我给
id字段加了insertable=false,但因为它是@EmbeddedId复合主键的一部分,Hibernate的处理逻辑比较特殊,生成INSERT语句时还是把ID列塞进了字段列表里,直接触发了DB2的-798错误。 @GeneratedValue放在@Embeddable类里本身就不是Hibernate推荐的用法,复合主键的设计初衷是用业务上的唯一组合键,自增列是数据库自动生成的,两者语义不匹配,自然容易出问题。
可行的解决方案
方案一:把自增ID从复合主键中剥离(最推荐)
这是最符合设计规范的做法,把自增ID设为独立主键,原来的复合主键部分改成嵌入的业务键,还能给业务键加唯一约束保证业务唯一性。修改后的代码如下:
修改后的实体类
@Entity @Table(name="table_entity", schema="Db2Schema") @DynamicUpdate @DynamicInsert public class TableEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="ID", insertable = false, updatable = false) private Integer id; // 原来的复合主键去掉id后,做成业务键嵌入 @Embedded private TableEntityBusinessKey businessKey; // 其余实体字段省略 }
新增业务键类
@Embeddable public class TableEntityBusinessKey implements Serializable{ @Column(name="key1", length=254) private String key1; // 其余原来的主键字段省略 // 记得重写equals和hashCode方法,这是嵌入类的要求 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TableEntityBusinessKey that = (TableEntityBusinessKey) o; return Objects.equals(key1, that.key1); // 其他字段的equals判断 } @Override public int hashCode() { return Objects.hash(key1); // 其他字段的hashCode计算 } }
这样调整后,Hibernate生成INSERT语句时会自动忽略ID列,DB2会自动生成自增值,完美解决报错,同时结构也更清晰。
方案二:强制Hibernate不生成ID列的插入语句(不推荐)
如果实在不想改结构,可以试试以下调整,但兼容性不好,容易踩坑:
- 给
id字段加上@Transient,但这样Hibernate不会映射这个字段,插入后你得手动查询获取生成的ID,非常麻烦。 - 用Hibernate的
@Generated注解替代@GeneratedValue,明确指定插入后生成:
@Embeddable public class TableEntityKey implements Serializable{ @Generated(GenerationTime.INSERT) @Column(name="ID", insertable = false, updatable = false) private Integer id; @Column(name="key1", length=254) private String key1; // 其余字段省略 }
不过这种方式在不同版本的Hibernate里可能有不同的表现,而且复合主键的处理逻辑本来就复杂,不建议用。
方案三:改用DB2序列生成ID(需修改数据库)
如果允许修改数据库表结构,可以把IDENTITY列改成用序列生成,然后在Hibernate里配置序列生成器:
@Embeddable public class TableEntityKey implements Serializable{ @GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "table_id_seq") @SequenceGenerator(name = "table_id_seq", sequenceName = "DB2SCHEMA.TABLE_ID_SEQ", allocationSize = 1) @Column(name="ID") private Integer id; // 其余字段省略 }
这种方式下,Hibernate会先从序列获取ID值,再插入到数据库,DB2不会报错,但同样,复合主键里放序列生成的ID还是不太符合设计逻辑,仅作为备选方案。
内容的提问来源于stack exchange,提问作者Muhammad Salman Farooq

