I am writing a scheduling feature in a new Grails 2.1.0 application. I am transitioning from a Ruby on Rails project, so much of my query strategy derives from Rails style. I have the following domain classes:
Schedule.groovy
class Schedule {
// Number of minutes between available appointment slots
int defaultAppointmentInterval
Time officeOpenStart
Time officeOpenEnd
Time lunchStart
Time lunchEnd
static hasMany = [inventorySlots: InventorySlot]
static constraints = {
// long validation rules
}
def boolean isAvailableAt(Date dateTime) {
def isAvailable = true
if (inventorySlots.isEmpty()) {
isAvailable = false
} else if (inventorySlotsSurroundingTime(dateTime).isEmpty()) {
isAvailable = false
}
isAvailable
}
def inventorySlotsSurroundingTime(Date dateTime) {
InventorySlot.surroundingTime(dateTime) {
and {
inventorySlot.schedule = this
}
}
}
}
InventorySlot.groovy
class InventorySlot {
Date startTime
static belongsTo = [schedule: Schedule]
static constraints = {
startTime nullable: false, blank: false
}
static mapping = {
tablePerHierarchy true
schedule lazy: false
}
static namedQueries = {}
def static surroundingTime(Date time) {
[UnboundedInventorySlot.surroundingTime(time), BoundedInventorySlot.surroundingTime(time)].flatten()
}
def endTime() {
return ((BoundedInventorySlot) this).endTime?: (UnboundedInventorySlot (this)).endTime()
}
}
UnboundedInventorySlot.groovy
class UnboundedInventorySlot extends InventorySlot {
static namedQueries = {
// surroundingTime { time ->
// le 'startTime', time
// ge 'startTime', time - 1
// }
}
@Override
def static surroundingTime(Date time) {
findAllByStartTimeLessThanEqualsAndStartTimeGreaterThanEquals(time, time - 1)
}
def Date endTime() {
def endTime
// If the office closing is defined, use that, otherwise use tonight @ 23:59:59
endTime = schedule?.officeOpenEnd?: new DateTime(startTime + 1).withTimeAtStartOfDay().plusSeconds(-1).toDate()
return endTime
}
}
BoundedInventorySlot.groovy
class BoundedInventorySlot extends InventorySlot {
Date endTime
static constraints = {
endTime nullable: false, blank: false, validator: {val, obj ->
if (val.date != obj.startTime.date) { return ["invalid.differentDate", val.date] }
}
}
static namedQueries = {
// surroundingTime { time ->
// le 'startTime', time
// ge 'endTime', time
// }
}
@Override
def static surroundingTime(Date time) {
findAllByStartTimeLessThanEqualsAndEndTimeGreaterThanEquals(time, time)
}
@Override
def Date endTime() {
endTime
}
}
What I would like to do is to implement the Schedule#isAvailableAt(Date) method as follows:
def boolean isAvailableAt(Date dateTime) {
def isAvailable = true
if (inventorySlots.isEmpty()) {
isAvailable = false
} else if (inventorySlots.surroundingTime(dateTime).isEmpty()) {
isAvailable = false
}
isAvailable
}
where the inventorySlots.surroundingTime()
invocation is essentially InventorySlot.surroundingTime()
but instead of querying the universe of InventorySlots, it pre-filters on just the instances associated with the schedule instance. This is very common in Rails, but any searches for "chained query" or "collection query" in Grails doesn't seem to provide good documentation. Thanks for any help.
I can think of two approaches off the top of my head which would work:
A more complex dynamic finder:
InventorySlots.findAllByScheduleAndStartTimeLessThanEqualsAndEndTimeGreaterThanEquals(this, time, time -1)
You can chain named queries together, then use any of the autowired finders to run the actual query, so if you uncomment your named query:
static namedQueries = {
surroundingTime { time ->
le 'startTime', time
ge 'startTime', time - 1
}
}
You could simply call:
InventorySlots.surroundingTime(time).findAllBySchedule(this)
You might also want to look into where queries in Grails 2 if you are not a fan of the criteria builder syntax. They are more type safe than criteria queries, and can be chained in the same fashion.
Update: Unfortunately, I'm not familiar with the inner workings of how named queries work with polymorphism, and I presume trouble with that is why you commented that out. I think worst case though, you could build a query on the parent like this one:
surroundingTime { time ->
or {
and {
eq('class', BoundedInventorySlot.name)
le 'startTime', time
ge 'startTime', time
}
and {
eq('class', UnboundedInventorySlot.name)
le 'startTime', time
ge 'startTime', time - 1
}
}
}
***Update: Could you leverage the spread operator to simplify your task? i.e. Keep this code, but remove the .flatten() and call surroundingTime as a named query or where query.
def static surroundingTime(Date time) {
[UnboundedInventorySlot.surroundingTime(time), BoundedInventorySlot.surroundingTime(time)]
}
Then you could call:
Schedule.surroundingTime(time)*.findAllBySchedule(this).flatten()
Not ideal that the caller needs to know to combine the results, but an interesting approach maybe.