optional=false导致的问题之Hibernate源码分析

8月初,帮助同事接手了一个hibernate实体保存出错的问题。解决过程比较有意思,最终还是需要分析hibnate源码来解决,现记录如下:

实体类定义如下:

@Entity

@Proxy(lazy=true)

@DiscriminatorValue("1")

@SecondaryTable(name="act_order", pkJoinColumns=@primaryKeyJoinColumn(name="order_id"))

@Table(appliesTo="act_order",fetch=FetchMode.SELECT)

public class ActOrder extends Order{

@Column(name="txt",table="act_order")

private String text;

...

}

 @Table(appliesTo="order")

public class Order {

@id(name="order_id")

private String orderId;

...

}

1 背景:

子表act_order 通过主键order_id关联 基表order 表主键order_id,一对一的关联,但order的记录有可能是找不到一条对应的act_order.

本次操作是修改act_order 子表的txt值从null -> 'aaa'。

库表里面已经存在一条数据:

optional=false导致的问题之Hibernate源码分析

 2 日志:

Hibernate: insert into SUB_ORDER(TXT, ORDER_ID) values (?, ?)
09 Aug 2019 06:21:36,590 -ERROR insert into SUB_ORDER (TXT, ORDER_ID) values ('aaa', 51598892) com.reserveamerica.framework.persistence.toplinkimpl.BaseOraclePooledConnector$2.sqlException(BaseOraclePooledConnector.java:442)
java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (LIVE_TX.PK_ACT_ORDER) violated

at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:450)
at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:399)
at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:1059)
at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:522)
at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:257)
at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:587)
at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:225)
at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:53)
at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:943)
at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1150)
at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:4798)
at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:4875)
at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1361)
at sun.reflect.GeneratedMethodAccessor99.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at oracle.ucp.jdbc.proxy.StatementProxyFactory.invoke(StatementProxyFactory.java:353)
at oracle.ucp.jdbc.proxy.PreparedStatementProxyFactory.invoke(PreparedStatementProxyFactory.java:178)
at com.sun.proxy.$Proxy78.executeUpdate(Unknown Source)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2421)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2485)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2805)
at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:114)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:260)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:180)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)

根据日志是主键冲突导致插入失败,看到这条日志会感觉很奇怪,本应update语句为何变成了insert?

org.hibernate.persister.entity.AbstractEntityPersister:

 optional=false导致的问题之Hibernate源码分析

 程序会判断act_order库表是一个NullableTable (默认optional=true),该条记录的所有旧值(除了primary key)是null,所以isRowToUpdate=false的错误结论。

3 初步的解决方案如下,参考红色字体:

@Table(appliesTo="act_order",fetch=FetchMode.SELECT,optional=false)

public class ActOrder extends Order

这样的话即便txt字段为null或者空串"",都会生成一条记录到act_order 库表。order有一条记录,则act_order也会对应生成一条记录。

 本以为问题轻松解决,但第二天就遭到打脸。新数据创建没有任何问题,历史数据有可能一条order找不到对应act_order记录.因为一旦 设置为optional=false,hibernate就会认为一条order记录一定会对应一条act_order.如果查找不到,系统就会抛出异常。

4 最终解决方案是。

  • 回滚代码改动
  • 提供的一个sql脚本,删除txt为null的所有act_order记录。
  • 修改程序代码,如果txt="",程序强制设置txt=null。原因如下:当txt="",hibernate会认为至少有一个字段为不为空(NullableTable=false),hibernate将生成一条记录将会插入实体到库表中,但是存到库表的字段值依然是null。