Grails Integration: Invariant checks when executing findById
andresteingress opened this issue · comments
continued discussion from: http://andresteingress.wordpress.com/2010/05/15/domain-patterns-in-enterprise-projects/
Source Code:
@Invariant({ amount >= 0.00 && vendor })
class Payment {
private Payment() {}
//Properties
BigDecimal amount
String notes
Date dateCreated
Date lastUpdated
def Product paymentFor
def Vendor vendor
//Relationships
static def belongsTo = [org.cannva.imp.Vendor]
Comment:
I’m having problems retrieving a class after it has been persisted.
for example:
payment = Payment.create(amount, vendor)
payment.save() // works fine
...
payInstance = Payment.findById(1) // throws an error.
Going through the debugger I see that the setters get called one by one by hibernate and checking the invariant after each setter.
Stepping through I see it “payInstance = Payment.findById(1)” goes though RequestContextHolder.java (requestAttributes), then SimpleTypeConverter.java (registerDefaultEditors), which ends up calling each setter in alphabetical order.
a class invariant is checked after each public constructor call and before/after each public method invocation (including property setters). if the contract can not be fulfilled in these cases, the object is assumed to be in an invalid state.
so far for theory. when working with grails/hibernate we have to be wary about reflection mechanisms for loading/constructing objects and how they are used in these contexts.
Whenever Payment.findById(1) is called and an object with id 1 is found, Hibernate creates that object with the default constructor. in our case the default constructor is private, so GContracts won't inject any assertions here. next, grails unwraps the proxy which is returned by the internal criteria object (see FindByPersistentMethod).
if the class invariant of Payment should be fulfilled, it needs to be assured that after construction the class invariant holds. E.g. a possible way to do this for a reference type is to introduce a NONE/NULL object (null object pattern), constraints need to avoid persisting those special objects:
@Invariant({ amount >= 0.00 && vendor })
class Payment {
private Payment() {}
//Properties
BigDecimal amount
String notes
Date dateCreated
Date lastUpdated
Product paymentFor
Vendor vendor = Vendor.NONE
//Relationships
static belongsTo = [org.cannva.imp.Vendor]
static constraints = {
// check that vendor does not equal Vendor.NONE!
}
class Vendor {
static final NONE = new Vendor()
// ....
}
I think I get it, I tried this for this case and it works, but does this mean I would need to do the same thing with amount? The only reason it's working is because alphabetically amount is set first, and when checked, amount has a value and vendor has the default value.
For example, say I want to absolutely make notes mandatory:
@Invariant({ amount >= 0.00 && notes && vendor })
class Payment {
//Properties
BigDecimal amount
String notes
Vendor vendor = Vendor.NONE
This fails Invariant Assertion when hibernate is loading the saved object. When grails sets amount and checks, notes is null and fails there.
Modifying it to the following:
@Invariant({ amount >= 0.00 && notes && vendor })
class Payment {
//Properties
BigDecimal amount = 0.00
String notes = "Default Notes"
Vendor vendor = Vendor.NONE
Passes Invariant Assertion.
If I am understanding this correctly this means I would have to have default values (that passes the invariant check), then create validation/constraints to make sure the user actually changes the default values. Is there a better way to achieve this with Value Fields with primitive types?
My constraints look something like (any better ways?):
static constraints = {
amount (min:0.00, max:100000.00, scale:2)
notes (widget:'textarea')
notes (nullable: false, blank: false)
notes(validator: {
if (it.contains('Default Notes')) return ['invalid.notes']
})
//Not sure if this is right for vendor:
vendor (none: false)
vendor(validator: {
if (it == Vendor.NONE) return ['invalid.vendor']
})
}
Much appreciated. Thank You.
things should get easier with 1.1.3 - see this wiki entry