balteo
balteo

Reputation: 24659

My JPA @OneToOne relationship causes an insert instead of an update on the target entity

I have a Member entity which has a @OneToOne relationship to an Address entity as follows:

In the Member entity:

@OneToOne(cascade=CascadeType.ALL)
private Address address;

The Address entity:

@RooJpaEntity
public class Address {

    private String formattedAddress;
    private double latitude;
    private double longitude;
}

The issue I have is that each time I update the member's address as follows:

public void modifyAddress(Member member, Address address){
        member.setAddress(address);
        memberRepository.save(member);
    }

... instead of updating the row in the address table it inserts a new row in the address table and updates the FK in the member table as in the sql given below:

Generated SQL:

Hibernate: 
    /* insert com.bignibou.domain.Address
        */ insert 
        into
            address
            (formatted_address, latitude, longitude, version) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* update
        com.bignibou.domain.Member */ update
            member 
        set
            address=?,
            version=? 
        where
            id=? 
            and version=?

I am not sure how to fix the issue (I don't want to inline the Adress data in the member table for now). I would like for my app to update the address table instead of inserting a new row in that table.

Can anyone please advise?

edit 1:

I have modified my entities as follows:

Here is the full Address entity:

@RooJpaEntity
public class Address {

    private String formattedAddress;
    private double latitude;
    private double longitude;

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "address")
    private Member member;
}

Here is the relevant property in Member entity:

@OneToOne
private Address address;

My method:

@Override
public void modifyAddress(Member member, Address address){
    address.setMember(member);
    updateAddress(address);
}

The behavior of my app is unchanged...

edit 2: If I modify the app as below I get a StaleObjectStateException.

@Override
    public void modifyAddress(Member member, Address address){
        long addressId = member.getAddress().getId();
        address.setId(addressId);
        address.setMember(member);
        updateAddress(address);
    }

Address:

@RooJpaEntity
public class Address {

    private String formattedAddress;
    private double latitude;
    private double longitude;

    @OneToOne
    private Member member;
}

In Member:

@OneToOne
private Address address;

StaleObjectStateException:

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.Address#5]
    org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
    org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:903)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891)
    org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:879)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:366)
    com.sun.proxy.$Proxy49.merge(Unknown Source)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:241)
    com.sun.proxy.$Proxy48.merge(Unknown Source)
    org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:353)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:333)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:318)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96)
    org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260)
    org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:155)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.data.jpa.repository.support.LockModeRepositoryPostProcessor$LockModePopulatingMethodIntercceptor.invoke(LockModeRepositoryPostProcessor.java:92)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
    com.sun.proxy.$Proxy65.save(Unknown Source)
    com.bignibou.service.PreferencesServiceImpl_Roo_Service.ajc$interMethod$com_bignibou_service_PreferencesServiceImpl_Roo_Service$com_bignibou_service_PreferencesServiceImpl$updateAddress(PreferencesServiceImpl_Roo_Service.aj:53)
    com.bignibou.service.PreferencesServiceImpl.updateAddress(PreferencesServiceImpl.java:1)
    com.bignibou.service.PreferencesServiceImpl_Roo_Service.ajc$interMethodDispatch1$com_bignibou_service_PreferencesServiceImpl_Roo_Service$com_bignibou_service_PreferencesServiceImpl$updateAddress(PreferencesServiceImpl_Roo_Service.aj)
    com.bignibou.service.PreferencesServiceImpl.modifyAddress_aroundBody12(PreferencesServiceImpl.java:100)
    com.bignibou.service.PreferencesServiceImpl$AjcClosure13.run(PreferencesServiceImpl.java:1)
    org.springframework.transaction.aspectj.AbstractTransactionAspect.ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96cproceed(AbstractTransactionAspect.aj:59)
    org.springframework.transaction.aspectj.AbstractTransactionAspect$AbstractTransactionAspect$1.proceedWithInvocation(AbstractTransactionAspect.aj:65)
    org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260)
    org.springframework.transaction.aspectj.AbstractTransactionAspect.ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(AbstractTransactionAspect.aj:63)
    com.bignibou.service.PreferencesServiceImpl.modifyAddress(PreferencesServiceImpl.java:93)
    com.bignibou.controller.PreferenceController.modifyAddress(PreferenceController.java:168)

edit 3:

Address's hashcode:

public int hashCode() {
        return new HashCodeBuilder().append(formattedAddress).append(getId()).append(latitude).append(longitude).toHashCode();
    }

Address's equals (from ITD):

public boolean Address.equals(Object obj) {
        if (!(obj instanceof Address)) {
            return false;
        }
        if (this == obj) {
            return true;
        }
        Address rhs = (Address) obj;
        return new EqualsBuilder().append(formattedAddress, rhs.formattedAddress).append(id, rhs.id).append(latitude, rhs.latitude).append(longitude, rhs.longitude).append(member, rhs.member).isEquals();
    }

ITD for Address entity:

privileged aspect Address_Roo_Jpa_Entity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long Address.id;

    @Version
    @Column(name = "version")
    private Integer Address.version;

    public Long Address.getId() {
        return this.id;
    }

    public void Address.setId(Long id) {
        this.id = id;
    }

    public Integer Address.getVersion() {
        return this.version;
    }

    public void Address.setVersion(Integer version) {
        this.version = version;
    }

}

By the way, ITDs are just aspects.

edit 4: I have set up a sample app for you here: https://github.com/balteo/sample-app-gab

Upvotes: 1

Views: 5888

Answers (1)

Gab
Gab

Reputation: 8323

(wrong original diagnostic i delete, i let the link anyway cause they are still instructive)

See : http://www.objectdb.com/api/java/jpa/OneToOne and JPA Hibernate One-to-One relationship and http://www.techavalanche.com/2012/05/10/one-to-one-unidirectional-mapping-using-foreign-key/

Edit :

  • Actually you get StaleObjectStateException cause you're modifying primary key of existing entity
  • If you revert to your original (and correct) configuration (where member own unidirection OneToOne relationship).
    You can have an update on address table by saving member doing the following :
    • Save a member m0 with address a0
    • flush
    • retrieve m0.a0 and edit values
    • save m0
    or
    • Save a member without address m1
    • Save an address a1
    • flush
    • attach a1 to m1, save m1
    or
    • Save a member m2 with an adress a2
    • flush
    • Set m2 address to null
    • save m2, here a2 will be updated and remain without associated member (orphan), you can force the a2 deletion on such a case using delete-orphan option on relationship

Ofc, the flush call here are needed only if operations take place in same transaction.

All of this run fine with your sample app.

Please note an interesting thing, OneToOne relationship DOES NOT ensure unicity by itself between address and member. (you can have 2 member refering to same address). You have to explicit unique constraint using @JoinColumn(name = "address", unique=true)

Upvotes: 2

Related Questions