This post discusses a relatively simple topic in GORM: how to use dependent variables in a domain class. It’s simple in the sense that it’s been discussed on the mailing list, but I haven’t seen it documented anywhere so I thought I’d do so here.
I started with a simple two-class domain model that I discussed in my last GORM post.
class Quest {
String name
static hasMany = [tasks:Task]
String toString() { name }
}
class Task {
String name
static belongsTo = [quest:Quest]
String toString() { name }
}
As before, there is a one-to-many relationship between quests and tasks. A quest has many tasks, and the belongsTo
setting implies a cascade-all relationship, so inserting, updating, or deleting a quest does the same for all of its associated tasks.
In Bootstrap.groovy
, I also have:
def init = { servletContext ->
new Quest(name:'Seek the grail')
.addToTasks(name:'Join King Arthur')
.addToTasks(name:'Defeat Knights Who Say Ni')
.addToTasks(name:'Fight Killer Rabbit')
.save()
}
which shows how the classes are intended to work together.
The first change I want to make is to give tasks a start date and end date. My first attempt is to just add properties with those names, of type java.util.Date
.
class Task {
String name
Date start
Date end
// ... rest as before ...
}
This leads to a minor problem. If I start up the server, I don’t see any quests or tasks. The reason is that my bootstrap code tries to create tasks without start and end dates, which violates the database schema restriction. My generated schema marks both start
and end
columns as “not null”.
There are many ways to fix that. I can either assign both start
and end
properties for each task in my bootstrap code, or add a constraint in Task that both can be nullable
, or do what I did here, which is to give them default values.
class Task {
String name
Date start = new Date()
Date end = new Date() + 1
// ... rest as before ...
}
I do have a constraint in mind, actually. I’d like to ensure that the end
date is after the start
date. That requires a custom validator, which is also pretty easy to implement:
class Task {
// ...
static constraints = {
name(blank:false)
start()
end(validator: { value, task ->
value >= task.start
})
}
}
That works fine.
Now for the dependent variable. My tasks all have a start
and an end
, so implicitly they have a duration. I could add the duration
variable to my Task class, but I don’t want to save it in the database. It’s dependent on the values of start
and end
. I also don’t want to be able to set it from the gui.
Here’s the result:
class Task {
String name
Date start
Date end
int getDuration() { (start..end).size() }
void setDuration(int value) {}
static transients = ['duration']
// ... rest as before ...
}
This computes the duration
from the start
and end
dates by returning the number of days between them. It relies on the fact that Groovy modifies java.util.Date
to have the methods next()
and previous()
, and since Date
implements Comparable
, it can then be used in a range, as shown.
(As an aside, this implementation is probably pretty inefficient. If the number of days between start and end was substantial, I think this implementation executes the next()
method over and over until it reaches the end. I thought about trying to subtract the two dates, but interestingly enough the Date
class only has plus()
and minus()
methods that take int
values, not other Dates. I considered adding a category that implemented those methods, but haven’t tried it yet. I’d like to look in the Groovy source code for the plus()
and minus()
implementations, but I couldn’t find it. I did find something similar in org.codehaus.groovy.runtime.DefaultGroovyMethods
, but I’m not sure that’s the same thing. Sigh. Still a lot to learn…)
By putting 'duration'
in the transients
closure, I ensure that it isn’t saved in the database.
The getDuration
method is pretty intuitive, but adding set method as a no-op is somewhat annoying. If I leave it out, then Groovy will generate a setter that can modify the duration. As an alternative, according to GinA I can also supply my own backing field and mark it as final:
class Task {
// ...
final int duration
int getDuration() { (start..end).size() }
// ...
}
Just to be sure, I added the following test to my TaskTests
:
void testSetDuration() {
Task t = new Task(name:'Join King Arthur')
shouldFail(ReadOnlyPropertyException) {
t.duration = 10
}
q.addToTasks(t).save()
}
That passed without a problem.
Interestingly, the dynamic scaffold still generates a modifiable input text field for duration, both in the create and edit views. I can put my own value in it and submit the form without a problem. The result does not get saved, which is correct, but I don’t see an exception thrown anywhere in the console. If I generate the static scaffolding, I know that in Task.save
there is a line like
t.properties = params
which is how the form parameters are transfered to the object. Presumably the internal logic knows enough to avoid trying to invoke a setter on a final field. Of course, as soon as I generate the static scaffolding, I usually just delete that row in the GSP form.
There’s one final (no pun intended) issue with the dynamic scaffolding. The generated list view puts its properties in <g:sortableColumn>
tags. This holds true for the duration, as well. Normally, when I click on the column header, the result is sorted, ascending or descending, by that property. If I click on the duration
column header, however, I get an “org.hibernate.QueryException: could not resolve property: duration of: Task
“.
It turns out that the User Guide has a “Show Source” link for every tag. When I clicked on that link for the sortableColumn
tag, I saw near the top:
if(!attrs.property)
throwTagError("Tag [sortableColumn] is missing required attribute [property]")
The error I got in the console is “could not resolve property”, but it’s possible this is the source of that issue. I’m not sure. The only other source (again, no pun intended) of the problem I could see was the execution of the list
action at the bottom. That would imply that Grails is generating the Hibernate query and we’re failing at that point, which would be consistent with the error reported above.
At any rate, the duration property now works in the domain class. I can always modify the views to ensure I don’t try to set it.
Leave a Reply