Hibernate从3.6升级到5.2后用JoinColumnOrFormula触发ClassCastException
Hibernate 5.2.18迁移后ClassCastException异常分析与解决
数据库模型
TABLE_CONNECTION: +--+ |ID| +--+ |1 | |2 | +--+ TABLE_ENDPOINT: +--+------+-------------+ |ID|CON_ID|ENDPOINT_TYPE| +--+------+-------------+ |1 |1 |FROM | |2 |1 |TO | |3 |2 |FROM | |4 |2 |TO | +--+------+-------------+
Hibernate 3.6.10映射代码
Connection实体
@Entity @Table(name = "TABLE_CONNECTION") @Immutable public class Connection { @Id @Column(name = "ID", unique = true, nullable = false, precision = 38, scale = 0) private BigDecimal id; @OneToOne(fetch = FetchType.LAZY) @JoinColumnsOrFormulas({ @JoinColumnOrFormula(column = @JoinColumn(name = "ID", referencedColumnName = "CON_ID")) @JoinColumnOrFormula(formula = @JoinFormula(value = "'FROM'", referencedColumnName = "ENDPOINT_TYPE")) }) private Endpoint fromEndpoint; @OneToOne(fetch = FetchType.LAZY) @JoinColumnsOrFormulas({ @JoinColumnOrFormula(column = @JoinColumn(name = "ID", referencedColumnName = "CON_ID")) @JoinColumnOrFormula(formula = @JoinFormula(value = "'TO'", referencedColumnName = "ENDPOINT_TYPE")) }) private Endpoint toEndpoint; ... }
Endpoint实体
@Entity @Table(name = "TABLE_ENDPOINT") @Immutable public class Endpoint { @Id @Column(name = "ID", unique = true, nullable = false, precision = 38, scale = 0) private BigDecimal id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "CON_ID", nullable = false) private Connection connectionId; @Column(name = "ENDPOINT_TYPE", length = 4, nullable = false) @Enumerated(EnumType.STRING) private EndpointType endpointType; ... } enum EndpointType {FROM, TO}
迁移后抛出的异常
调用hibernateTemplate.get(Connection.class, BigDecimal.valueOf(1))时出现如下异常:
java.lang.ClassCastException: com.our.package.Endpoint cannot be cast to java.math.BigDecimal at org.hibernate.type.descriptor.java.BigDecimalTypeDescriptor.extractHashCode(BigDecimalTypeDescriptor.java:19) at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:204) at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:209) at org.hibernate.engine.spi.EntityUniqueKey.generateHashCode(EntityUniqueKey.java:67) at org.hibernate.engine.spi.EntityUniqueKey.<init>(EntityUniqueKey.java:48) at org.hibernate.type.ManyToOneType.resolve(ManyToOneType.java:250) at org.hibernate.engine.internal.TwoPhaseLoad.doInitializeEntity(TwoPhaseLoad.java:171) at org.hibernate.engine.internal.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:128) at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.performTwoPhaseLoad(AbstractRowReader.java:238) at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:209) at org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133) at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122) at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86) at org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load(AbstractLoadPlanBasedEntityLoader.java:185) at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4121) at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508) at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478) at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219) at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278) at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121) at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89) at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1240) at org.hibernate.internal.SessionImpl.access$1900(SessionImpl.java:204) at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.doLoad(SessionImpl.java:2842) at org.hibernate.internal.SessionImpl$IdentifierLoadAccessImpl.load(SessionImpl.java:2816) at org.hibernate.internal.SessionImpl.get(SessionImpl.java:1076) at org.springframework.orm.hibernate5.HibernateTemplate.lambda$get$0(HibernateTemplate.java:456) at org.springframework.orm.hibernate5.HibernateTemplate.doExecute(HibernateTemplate.java:367) at org.springframework.orm.hibernate5.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:334) at org.springframework.orm.hibernate5.HibernateTemplate.get(HibernateTemplate.java:451) at org.springframework.orm.hibernate5.HibernateTemplate.get(HibernateTemplate.java:445)
异常原因
Hibernate 5.x版本对@JoinColumnsOrFormulas的处理逻辑做了严格调整,尤其是在实体缓存的主键哈希计算环节。原来的映射中,Connection通过复合列+公式关联Endpoint,而Hibernate 5在处理Endpoint到Connection的反向ManyToOne关联时,错误地将Endpoint实体实例当作BigDecimal类型的主键传入哈希计算方法,导致类型转换失败。
简单来说,Hibernate 3对这种非常规关联的兼容性处理更宽松,而5.x版本的实体唯一键生成逻辑更严谨,混淆了关联键的类型识别。
解决方法
方案1:改用@Where注解替代公式关联(推荐)
放弃@JoinColumnsOrFormulas,使用@OneToOne配合@Where注解过滤Endpoint类型,这种方式更符合Hibernate 5的规范:
@Entity @Table(name = "TABLE_CONNECTION") @Immutable public class Connection { @Id @Column(name = "ID", unique = true, nullable = false, precision = 38, scale = 0) private BigDecimal id; @OneToOne(fetch = FetchType.LAZY, mappedBy = "connection") @Where(clause = "ENDPOINT_TYPE = 'FROM'") private Endpoint fromEndpoint; @OneToOne(fetch = FetchType.LAZY, mappedBy = "connection") @Where(clause = "ENDPOINT_TYPE = 'TO'") private Endpoint toEndpoint; // ...其他代码 }
同时把Endpoint中的connectionId字段名改为connection,避免命名歧义:
@ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "CON_ID", nullable = false) private Connection connection;
方案2:修正原有映射的细节
如果要保留@JoinColumnsOrFormulas,需要补充关联的非空约束,并确保字段命名无歧义:
- 在Connection的关联上添加
optional = false:
@OneToOne(fetch = FetchType.LAZY, optional = false) @JoinColumnsOrFormulas({ @JoinColumnOrFormula(column = @JoinColumn(name = "ID", referencedColumnName = "CON_ID", nullable = false)), @JoinColumnOrFormula(formula = @JoinFormula(value = "'FROM'", referencedColumnName = "ENDPOINT_TYPE")) }) private Endpoint fromEndpoint;
- 将Endpoint的
connectionId字段重命名为connection,避免和主键ID混淆。
方案3:升级Hibernate版本
该兼容性问题在Hibernate 5.4及以上版本中已被修复,升级到5.6.x等稳定版本可以直接解决问题,同时获得更多bug修复和性能优化。
内容的提问来源于stack exchange,提问作者Tomáš Záluský
相关产品推荐
相关产品推荐

