TechFind
TechFind

Reputation: 3696

Hibernate savOrUpdate either insert or update only

@Autowired
private SessionFactory sessionFactory;
public String getAccountsDetails(List<Account> accountList) {
    Session session = sessionFactory.openSession();
    for (Account account : accountList) {
        int i = 0;
        AccountDetails accountDetails = new AccountDetails();
        accountDetails.setAccountsId(Long.parseLong(account.getId()));//LINE no -20
        accountDetails.setName(account.getName());
        accountDetails.setSubAccount(account.getAccountSubType());
        session.saveOrUpdate(accountDetails);
        if (++i % 20 == 0) {
            session.flush();  
            session.clear();  
        } 
    }
        session.getTransaction().commit();
        session.close();
}

Output:

this always runs update even there is no data in db.

   Hibernate: update name=?, subaccount=? where accounts_id=?
    ....

If I comment account id in LINE no-20, it always runs insert.

Hibernate: insert into table test... ....

dto class:

@Entity
@Table(name="test")
public class AccountDetails{
    @Id
    @GeneratedValue
    @Column(name = "accounts_id")
    private Long accountsId;

    @Column(name = "name")
    private String name;

    @Column(name = "subaccount")
    private String subAccount;

}

Query: Oracle db:

 create table test (accounts_id NUMBER(10) NOT NULL PRIMARY KEY, 
    name VARCHAR(100),
    subaccount VARCHAR(100) 
    )

My requirement is if there is no data in db then insert otherwise update.

EDIT

I have created a new field in my dto as:

@Version
@Column(name = "version")
private long version;

and created a column in db as version number(100). Whenever I run the application, it always run one updte statement first, then it throws StaleObjectStateException as:

Hibernate: update test set accountsubtype=?, accounttype=?, acctnum=?, active=?, currentbalance=?, currentbalancewithsubaccounts=?, description=?, fullyqualifiedname=?, name=?, subaccount=?, version=? where accounts_id=? and version=?
ERROR 2014-09-22 11:57:25,832 [[qbprojects].connector.http.mule.default.receiver.04] org.hibernate.event.def.AbstractFlushingEventListener: Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.trinet.mulesoft.quickbooks.dto.AccountDetails#63]
    at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1932)
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2576)

Upvotes: 4

Views: 18543

Answers (1)

acdcjunior
acdcjunior

Reputation: 135762

TL;DR

In your case, saveOrUpdate() will generate:

  • an INSERT if the accountsId is null.
  • an UPDATE otherwise.

That is why when you have line 20 (accountDetails.setAccountsId(...);), it is UPDATEing.

The simplest way out is probably checking it yourself and calling save() if the accountsId does not yet exist or update() otherwise.

Details:

See Automatic State Detection - italics are written by me:

saveOrUpdate() does the following:

  • if the object is already persistent in this session, do nothing;
  • if another object associated with the session has the same identifier, throw an exception;
  • if the object has no identifier property, save() it;
    • No identifier property means no @Id or XML equivalent.
  • if the object's identifier has the value assigned to a newly instantiated object, save() it;
    • Notice that newly instantiated object is defined by the unsaved-value attribute.
    • In other words, this bullet says: if the @Id property value equals unsaved-value (see below), it is considered unsaved and then save() it.
    • This is your scenario. Notice that you are using unsaved-value=null because it is the default.
  • if the object is versioned by a <version> or <timestamp>, and the version property value is the same value assigned to a newly instantiated object, save() it;
    • This is the same as the item before, but with the <version> and <timestamp> properties instead of the @Id property.
    • Notice also both <version> and <timestamp> also have a unsaved-value=null attribute to define what is an unsaved or newly instantiated object.
  • otherwise update() the object

More details about the unsaved-value attribute (the quote below is from the unsaved-value for the @Id/identifier property):

unsaved-value (optional - defaults to a "sensible" value): an identifier property value that indicates an instance is newly instantiated (unsaved), distinguishing it from detached instances that were saved or loaded in a previous session.

For Long identifiers, the "sensible" default is most likely null.

Workarounds:

If just wanted to assign the identifier, we could use <generator class="assigned"/> and it'd be OK. But you wan't both of worlds: assign sometimes and generate the others.

1 - As suggested above, one option is to check if the object already exists yourself and calling save() or update() accordingly.

2 - Another option is to implement an Interceptor with the isTransient() method overridden. In that method you'd have to, again, check if the entity already exists yourself. Advantage: you only have to implement it once (on the Interceptor). Disadvantage: well, it is an Interceptor and you'd have to wire it and everything else it requires.

3 - Lastly, the docs hint there is a possibility (we'd have to confirm) that you could do what you want by both using a generator and setting the attribute unsaved-value="undefined". But you'd have to resort to XML mapping, as you can't set the unsaved-value through annotations:

The unsaved-value attribute is almost never needed in Hibernate and indeed has no corresponding element in annotations.

Setting unsaved-value via XML would be something like:

<id name="accountsId" column="accounts_id" type="long" unsaved-value="undefined">
    <generator class="auto" />
</id> 

Upvotes: 8

Related Questions