Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grails. Id is null after calling save

I've already searched about this, but still cannot figure out what I'm doing wrong. After calling save() the domain object id is null.

I've read it'll happen if there's a problem when saving the object, and that save(flush:true) should throw an error if that's the case, but it's not. Look at my code and the output:

def pic = new Picture(title:'XX', path:"XXX")
album.addToPictures(pic).save()
if(pic.validate())
   println "no errors. New id: " + pic.id
else
   println "with errors"

Output:

no errors. New id: null

And when using flush:true

def pic = new Picture(title:'XX', path:"XXX")
album.addToPictures(pic).save(flush:true)
if(pic.validate())
   println "no errors. New id: " + pic.id
else
   println "with errors"

Output:

no errors. New id: 17

As you can see, there aren't any errors creating the object, and I should be able to get the id of the object after just calling save(). Any ideas?

Thanks

like image 263
manolowar Avatar asked Nov 28 '12 00:11

manolowar


2 Answers

You are misunderstanding the timing of when an object actually gets persisted to the database. An object does not get persisted when you call obj.save(), it gets persisted when whichever of the following happens first:

  • the transaction in which save() is called is committed
  • the Hibernate session in which save() is called is closed

A transaction can be started explicitly with

SomeDomainClass.withTransaction {
  // code in here runs within a transaction
}

Normally, a transaction is also implicitly started for each call to a service method

class MyService {

  void doSomething () {
    // code in here runs within a transaction
  }  
}

If you don't explicitly or implicitly use transactions, saved objects get persisted when the Hibernate session closes, which is (roughly) when the HTTP request completes.

However, if you call someObject.save(flush: true) you are telling Hibernate to persist the object immediately, which is why

album.addToPictures(pic).save(flush: true)

assigns an ID to the Picture instance, but

album.addToPictures(pic).save()

will only assign the ID when the enclosing session/transaction is closed/committed

Update

Futher to your comment

The problem is that I want to use the id as part of the name of a file I need to save. What about if I get an error saving the file? Should I use a explicit transaction and roll it back?

Yes, use an explicit transaction, and save the file once you're sure the object has been successfully persisted, roll the transaction back if persistence fails

def pic = new Picture(title:'XX', path:"XXX")

Picture.withTransaction { TransactionStatus status ->        

  try {
    album.addToPictures(pic).save()

  } catch(ex) {
    status.setRollbackOnly()
    throw ex
  }
}

// At this point you can be sure pic has been persisted, so use pic.id to save the file

Update 2

Further to your comment

I don't want to save the file once I'm sure the object has been successfully persisted, but the opposite, I want to persist the object once the file has been successfully saved. So, I'm going to reformulate my questions as "Is there a way to configure Grails so that I can know the id that's going to be assigned to the new object before the object is effectively saved in the database?"

You already know that

album.addToPictures(pic).save(flush:true)

will provide you with the ID of the Picture instance, so if you do this within a transaction you can get the ID without actually committing the transaction. However, I think this will only work if you're using a database that uses sequences (Oracle, Postgres). Something like the following should work

Picture.withTransaction { TransactionStatus status ->        

  try {
    def pic = new Picture(title:'XX', path:"XXX")  
    album.addToPictures(pic).save(flush: true)

    // pic.id should now be assigned, so save the file. I'm assuming an
    // an exception will be thrown if saving the file fails

  } catch(ex) {
    // you may also want to try rolling back the file save here, i.e. delete it
    status.setRollbackOnly()
    throw ex
  }
}
like image 110
Dónal Avatar answered Oct 30 '22 13:10

Dónal


and that save(flush:true) should throw an error

That's not true. save(failOnError: true) will cause an Exception to be thrown.

Nothing is wrong with your code, nor is anything wrong with the behavior you're seeing. By not calling flush; save(), you're trying to access a generated ID before the actual insert happens. That is why it is null.

However, forcing a flush will (sometimes) force hibernate to write, which then gives you the ID you're expecting. If you need the ID immediately after calling save(), use save(flush: true). There is nothing wrong with that.

like image 2
Gregg Avatar answered Oct 30 '22 13:10

Gregg