Reputation: 4625
I have a GORM object that I'm working with in an integration test. It has a beforeUpdate
hook that keeps a history of the previous password hashes. The code looks something like this:
class Credentials {
List passwordHistory = []
String username
String password
static hasMany = [passwordHistory : String]
def beforeUpdate() {
// of course I'm not really storing plain-text passwords, but
// this is just for illustration.
if (isDirty('password')) { passwordHistory << password }
}
}
In the integration test, I'm wondering why:
appUser.credentials.password = newPassword
appUser.save(flush: true)
sessionFactory.currentSession.flush()
AppUser.withNewSession {
appUser = AppUser.get(appUser.id)
appUser.credentials.empty // eagerly fetch the list while session is open
}
assert !appUser.credentials.passwordHistory.empty() // contains the previous password
works, but
appUser.credentials.password = newPassword
appUser.save()
sessionFactory.currentSession.flush()
AppUser.withNewSession {
appUser = AppUser.get(appUser.id)
appUser.credentials.empty // eagerly fetch the list while session is open
}
assert !appUser.credentials.passwordHistory.empty() // is empty
does not. The difference is the flush: true
in the appUser.save()
call. I thought the call to save()
attached the object to the current session, but flushing the current session does not add the password to the passwordHistory
list. What's really going on here?
Upvotes: 7
Views: 969
Reputation: 423
In the first case you flush the session, so the object become "dirty". When you open a new session with newSession closure, you can modify an object, but if you want to make the modification "dirty" you must do an explicit call to the save() method in the new session open. If you do that, after closure closed, you can see the modification you made at the object in the closure.
In the second case before the withNewSession the object isn't dirty, so non explicit call is nedeed for modifying the object, and this is the reason why you see the list empty in the second case.
Upvotes: 0
Reputation: 3560
If I'm interpreting the Grails code correctly, you're actually dealing with two different sessions. From the documentation:
Integration tests run inside a database transaction by default, which is rolled back at the end of the each test. This means that data saved during a test is not persisted to the database.
If you dig through the Grails GORM method logic, you'll see that when you're within a transaction, GORM grabs its session from a ThreadLocal
resources map that's maintained by the TransactionSynchronizationManager
class. If it doesn't find one, it opens a new session and binds it to the map - important distinction, it explicitly opens a new session. It doesn't just call sessionFactory.getCurrentSession()
.
At the end of the save()
GORM logic, if you pass in flush:true
it will flush the session associated with the transaction - the one it got from the resources map in TransactionSynchronizationManager
.
On the other hand when you call flush()
you're calling it on the session that you get from sessionFactory.getCurrentSession()
which I believe is a session bound to your thread from the CurrentSessionContext
used by the Hibernate SessionFactory
. The actual implementation of CurrentSessionContext
is beside the point because (unless there's a Grails-specific implementation that I'm missing) it wouldn't return the same session that's held by the TransactionSynchronizationManager
.
Upvotes: 1