Grails

Composite Id for legacy DB with no version column attempts SQL update for new rows

Details

  • Type: Bug Bug
  • Status: Closed Closed
  • Priority: Major Major
  • Resolution: Fixed
  • Affects Version/s: 1.0-RC1
  • Fix Version/s: 1.0.2
  • Component/s: Persistence
  • Labels:
    None
  • Environment:
    Windows XP, Grails 1.0RC1_Snapshot, DB2/400 (AS/400 a.k.a. iSeries database)

Description

Most AS/400 legacy database files have composite keys and they rarely have a version number or timestamp. The Grails Hibernate DSL is fantastic but the Composite ID has an issue when creating a new row. The domain.save() operation causes an SQL update. The problem is that the version facility is turned off (via version:false) and the Hibernate generator is set to 'assigned' (id generator:'assigned') and Hibernate is not able to sense that an insert is required. The Hibernate documentation says that it will attempt to SQL-select by the composite id, and determine the update or insert operation required based on whether or not a row is returned with the query. But my log doesn't show the select statement. Only the update, which fails.

What I'd like is to be able to do is explicitly call a save() or update() but Grails domain.save() seems to run Hibernate saveOrUpdate(). Maybe a new method called insert (since save is already used.) Note most of my AS/400 Hibernate applications, as King's Hibernate book suggests on p.325, explicitly calls save() or update(). I always try to get my clients to add a version column and then use triggers to update it but they don't want to bother.

Activity

Hide
Donat Denoncourt added a comment -

My first workaround for this issue was to put SessionFactory sessionFactory in the controller and call Hibernate's save() directly.
My more recent workaround is to use a custom plug-in that adds a method called insert to all domains, which uses the Hibernate save. Perhaps an optional plug-in is a better solution than adding a method to standard Grails. Note that it's up to the programmer to keep track of new Domain objects and selectively call the new method (I've been putting a transient boolean in my domains.) That said, I'd like to change this to a minor bug.

Show
Donat Denoncourt added a comment - My first workaround for this issue was to put SessionFactory sessionFactory in the controller and call Hibernate's save() directly. My more recent workaround is to use a custom plug-in that adds a method called insert to all domains, which uses the Hibernate save. Perhaps an optional plug-in is a better solution than adding a method to standard Grails. Note that it's up to the programmer to keep track of new Domain objects and selectively call the new method (I've been putting a transient boolean in my domains.) That said, I'd like to change this to a minor bug.
Hide
Donat Denoncourt added a comment -

I now have a plugin called explicitInsert that I would like to add to the plugin list but I don't have an SVN account. I'm going to try to attach the zip version of that plugin here. It's very little code. (The plugin architecture is great!) Could someone either say the plugin is not useful or direct me on acquiring a SVN account for the plugin project?

Show
Donat Denoncourt added a comment - I now have a plugin called explicitInsert that I would like to add to the plugin list but I don't have an SVN account. I'm going to try to attach the zip version of that plugin here. It's very little code. (The plugin architecture is great!) Could someone either say the plugin is not useful or direct me on acquiring a SVN account for the plugin project?
Hide
Donat Denoncourt added a comment -

this is a very trivial grails plugin that I use to resolve this bug/issue

Show
Donat Denoncourt added a comment - this is a very trivial grails plugin that I use to resolve this bug/issue
Hide
Graeme Rocher added a comment -

Hi Donat,

Why don't you create a page on http://grails.org/Plugins and attach your zip on there. This is probaby far too little code to put in the standard repo

I haven't decided yet whether it should be part of code Grails, if there is enough demand i guess we could add a insert() method

Show
Graeme Rocher added a comment - Hi Donat, Why don't you create a page on http://grails.org/Plugins and attach your zip on there. This is probaby far too little code to put in the standard repo I haven't decided yet whether it should be part of code Grails, if there is enough demand i guess we could add a insert() method
Hide
Tyler Williams added a comment -

This problem is not isolated to composite keys. The problem seems to be the "version false" setting. Simple example...

class Priority {
String descr
static mapping = { version false }
}

Then the following fails...

def p1 = new Priority(descr:"High")
p1.id = 1
p1.save()

With this stacktrace...

org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:655)
at org.springframework.orm.hibernate3.HibernateAccessor.convertHibernateAccessException(HibernateAccessor.java:412)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:378)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:338)
at Script0.run(Script0:3)
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:61)
at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:46)
at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:24)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2403)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2307)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2607)
at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:92)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:250)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:234)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:142)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:390)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:374)
... 2 more

Show
Tyler Williams added a comment - This problem is not isolated to composite keys. The problem seems to be the "version false" setting. Simple example... class Priority { String descr static mapping = { version false } } Then the following fails... def p1 = new Priority(descr:"High") p1.id = 1 p1.save() With this stacktrace... org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:655) at org.springframework.orm.hibernate3.HibernateAccessor.convertHibernateAccessException(HibernateAccessor.java:412) at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:378) at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:338) at Script0.run(Script0:3) Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:61) at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:46) at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:24) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2403) at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2307) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2607) at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:92) at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:250) at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:234) at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:142) at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298) at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27) at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000) at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:390) at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:374) ... 2 more
Hide
Graeme Rocher added a comment -

So what do you suggest Tyler? This behaviour doesnt make sense for all applications, we could add an argument to save like:

obj.save(insert:true)

To force an insert, would that make sense?

Show
Graeme Rocher added a comment - So what do you suggest Tyler? This behaviour doesnt make sense for all applications, we could add an argument to save like: obj.save(insert:true) To force an insert, would that make sense?
Hide
David Beaton added a comment -

I've been having problems with this too.

I think obj.save(insert:true) would be quite a good way of going about it, but it might be better to set this behavior in the domain class, rather than potentially repeat it in multiple places within controller classes.

Show
David Beaton added a comment - I've been having problems with this too. I think obj.save(insert:true) would be quite a good way of going about it, but it might be better to set this behavior in the domain class, rather than potentially repeat it in multiple places within controller classes.
Hide
Graeme Rocher added a comment -

For the moment I added:

obj.save(insert:true)

If there is massive demand I'll add a configuration argument

Show
Graeme Rocher added a comment - For the moment I added: obj.save(insert:true) If there is massive demand I'll add a configuration argument

People

Vote (2)
Watch (2)

Dates

  • Created:
    Updated:
    Resolved: