问题描述
最近使用springdata自动化的JpaRepository进行对mysql数据库操作,遇到一个问题。
有一个实体bean是OrderInfoBean,主键为orderPKId,利用orderInfoRepository.save方法保存orderInfoBean时,返回的orderInfoBean的orderPKId的属性值是MySQL数据库的自增主键id的值。后期由于要增加分区,主键变成了reqDate和orderPkId之后,用orderInfoRepository.save方法保存orderInfoBean时,返回的orderInfoBean的orderPKId的属性值就为空了。以下是改成联合主键之后的部分代码
@Entity
@IdClass(OrderIdClass.class)
public class OrderInfoBean {
@Id
private String reqDate;
@Id
@GeneratedValue
private Long orderPkId;
private String orderNo;
}
public class OrderIdClass implements Serializable {
private static final long serialVersionUID = -5781462543461514912L;
private String reqDate;
private Long orderPkId;
public String getReqDate() {
return reqDate;
}
public void setReqDate(String reqDate) {
this.reqDate = reqDate;
}
public Long getOrderPkId() {
return orderPkId;
}
public void setOrderPkId(Long orderPkId) {
this.orderPkId = orderPkId;
}
public OrderIdClass(String reqDate) {
this.reqDate = reqDate;
this.orderPkId = 0L;
}
public OrderIdClass(String reqDate, Long orderPkId) {
this.reqDate = reqDate;
this.orderPkId = orderPkId;
}
public OrderIdClass() {
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OrderIdClass idClass = (OrderIdClass) o;
return Objects.equals(orderPkId, idClass.orderPkId) && Objects.equals(reqDate, idClass.reqDate);
}
@Override
public int hashCode() {
return Objects.hash(orderPkId, reqDate);
}
}
OrderInfoBean orderInfoBean = new OrderInfoBean;
orderInfoBean.setOrderNo("1534548648645646");
orderInfoBean.setReqDate("20180426");
/*主键为orderPkId的时候,orderInfoBean的orderPkId属性会赋值成MySQL数据库返回的自增id,但是主键变为orderPkId和reqDate之后,orderInfoBean的orderPkId属性不再有返回值
orderInfoBean = orderInfoRepository.save(orderInfoBean);
在网上没有找到直接资料,于是debug了一下,发现主键不同时,在SimpleJpaRepository(JpaRepository的实现类)这个类的save方法会对实体进行不同的处理。
springdata的JpaRepository对于insert和update操作都使用的save方法,具体调用insert还是update可以先去了解一下hibernate的中对象的三种状态瞬时状态transient、持久状态(托管)persistent、游离(脱管)detached状态的关系。
当主键是单独的orderPkId时,由于orderPkId为空时,默认这个实体是新的直接执行insert操作,对应到hibernate之中即是persist。而当主键是orderPkId和reqDate的联合主键时,对应hibernate的merge操作。
@Transactional
public S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
hibernate中persist和merge的区别
首先看看源码:
em.merge()调用的DefaultMergeEventListener的onmerge方法
switch ( entityState ) {
case DETACHED:
entityIsDetached( event, copyCache );
break;
case TRANSIENT:
entityIsTransient( event, copyCache );
break;
case PERSISTENT:
entityIsPersistent( event, copyCache );
break;
default: //DELETED
throw new ObjectDeletedException(
"deleted instance passed to merge",
null,
getLoggableName( event.getEntityName(), entity )
);
}
首先,会根据实体名字和主键的值在session里面查找,是否这个实体在session的管理中,如果没有,则假设entityState是detached状态,在这个状态下,session会根据主键去数据库查找是否有对应行,如果查找结果 为空
final Object result = source.get( entityName, clonedIdentifier );
source.getLoadQueryInfluencers().setInternalFetchProfile( previousFetchProfile );
if ( result == null ) {
//TODO: we should throw an exception if we really *know* for sure
// that this is a detached instance, rather than just assuming
//throw new StaleObjectStateException(entityName, id);
// we got here because we assumed that an instance
// with an assigned id was detached, when it was
// really persistent
entityIsTransient( event, copyCache );
}
首先会判断这个实体属于三种状态的哪一种,
DefaultMergeEventListener
if ( ForeignKeys.isTransient( entityName, entity, getAssumedUnsaved(), source ) ) {
if ( traceEnabled ) {
LOG.tracev( "Transient instance of: {0}", getLoggableName( entityName, entity ) );
}
return EntityState.TRANSIENT;
}
if ( traceEnabled ) {
LOG.tracev( "Detached instance of: {0}", getLoggableName( entityName, entity ) );
}
return EntityState.DETACHED;
那么将entity的copy一份,copy的对象变成了transient状态
protected void entityIsTransient(MergeEvent event, Map copyCache) {
LOG.trace( "Merging transient instance" );
final Object entity = event.getEntity();
final EventSource source = event.getSession();
final String entityName = event.getEntityName();
final EntityPersister persister = source.getEntityPersister( entityName, entity );
final Serializable id = persister.hasIdentifierProperty() ?
persister.getIdentifier( entity, source ) :
null;
if ( copyCache.containsKey( entity ) ) {
persister.setIdentifier( copyCache.get( entity ), id, source );
}
else {
( (MergeContext) copyCache ).put( entity, source.instantiate( persister, id ), true ); //before cascade!
}
final Object copy = copyCache.get( entity );
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
super.cascadeBeforeSave( source, persister, entity, copyCache );
copyValues( persister, entity, copy, source, copyCache, ForeignKeyDirection.FROM_PARENT );
saveTransientEntity( copy, entityName, event.getRequestedId(), source, copyCache );
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
super.cascadeAfterSave( source, persister, entity, copyCache );
copyValues( persister, entity, copy, source, copyCache, ForeignKeyDirection.TO_PARENT );
event.setResult( copy );
}
从以上代码可以看到,将orderInfoBean这个entity的值赋给了copy,并且将copy的值保存到了数据库,并且返回给event,及repository的save方法返回这个copy对象。
em.persist调用的DefaultPersistEventListener的onPersist方法,判断出entity
EntityState entityState = getEntityState( entity, entityName, entityEntry, source );
的状态是Transient,
@SuppressWarnings({"unchecked"})
protected void entityIsTransient(PersistEvent event, Map createCache) {
LOG.trace( "Saving transient instance" );
final EventSource source = event.getSession();
final Object entity = source.getPersistenceContext().unproxy( event.getObject() );
if ( createCache.put( entity, entity ) == null ) {
saveWithGeneratedId( entity, event.getEntityName(), createCache, source, false );
}
}
然后会返回带有自增键的entity。
Spring Data JPA offers the following strategies to detect whether an entity is new or not:
- Id-Property inspection (default): By default Spring Data JPA inspects the identifier property of the given entity. If the identifier property is
null
, then the entity is assumed to be new. Otherwise, it is assumed to be not new.- Implementing
Persistable
: If an entity implementsPersistable
, Spring Data JPA delegates the new detection to theisNew(…)
method of the entity. See the JavaDoc for details.- Implementing
EntityInformation
: You can customize theEntityInformation
abstraction used in theSimpleJpaRepository
implementation by creating a subclass ofJpaRepositoryFactory
and overriding thegetEntityInformation(…)
method accordingly. You then have to register the custom implementation ofJpaRepositoryFactory
as a Spring bean. Note that this should be rarely necessary. See the JavaDoc for details.
以上摘自SpringDataJPA 2.1.0.M2官方文档。SpringDataJPA判断一个实体是否是新建的:
- 默认是根据实体的主键,如果主键是空,那么实体默认是新建的,否则不是新的
- 实体实现了Persistable接口,JPA会根据isNew()方法判定
- 第三种方法好复杂,暂时略过不表
解决方案
- 方案一
实体类实现Persistable接口,并且重写isNew()方法
@Override
public boolean isNew() {
if (this.orderPkId == null) {
return true;
} else {
return false;
}
}
按照这样修改之后,JpaRepository的实现类SimpleJpaRepository的save方法确实回去调用em.persist
方法,但是还是不能返回MySQL数据库自增的主键。
- 方案二
最后经过请教了线内大佬,得到答复,在程序里面仅标记自增键为主键,然后问题就解决了o(╥﹏╥)o。但是这样一来联合查找的时候,关联关系就只有主键关联,带不上req_date的分区关联了,不知道会不会有性能上的损耗。针对这个,其实可以用QueryDSL来自定义SQL查询语言。
之前一直想的解决方案都是,换一种freestyle点的插入查找方法,比如我小师傅现在用的queryDSL和criteria;结果解决方案还是蛮简单的,不过key point还是看了源码,找出了问题关键所在,才能精准解决问题。