Model API
In addition to the basic CRUD programming model, Zero Resource Model (ZRM) introduces a list-function, the LCRUD model. The list function allows for listing and filtering collection members using simple conditions.
Contents
- Important classes to know
- Basic LCRUD
- Data Validation
- Custom Resource Event Handlers
Important classes to know
The involved class types are Member, Collection, and TypeCollection:
-
zero.resource.Member - A class that represents a member instance in a collection. It dynamically
holds data based on an instances configured type and fields definitions.
Each Member instance has a unique identifier and an
updatedfield holding the timestamp it was last updated. -
zero.resource.Collection - Implements the LCRUD interface that returns Member instances.
-
zero.resource.Type - A class that contains all the metadata about a model declaration. It is this
class that is created from the JSON definitions in the
/app/modelsdirectory. Also, public methods from Collection exist on Type as well and act as a delegate for the default, unfiltered collection of a given Type. -
zero.resource.TypeCollection - Implements the LCRUD iterface specifically for the
zero.resource.Collectionclass.
The following example shows some common interactions with the Model API in Groovy:
// retrieve the default collection for model 'persons'
def collection = TypeCollection.retrieve('persons')
// create a new member in the collection, create returns Member instance
def joe = collection.create([firstname: 'Joe'])
// update member in the collection, update returns Member instance
joe.firstname = 'Joseph'
joe = collection.update(joe)
assert 'Joseph' == joe.firstname
// delete member from collection
collection.delete(joe.id)
// retrieve member from collection
def id = joe.id
joe = collection.retrieve(id)
// list all collection results
def all_people = collection.list()
// using list() with conditions
def some_people = collection.list(firstname: 'Joseph')
people = collection.list(firstname__contains: 'se')
people = collection.list(firstname__endswith: 'ph')
// paged results from 11th to 20th items in the overall collection
def paged_people = collection[10..19].list()
// paged and filtered results
def filtered_paged = collection.filter(firstname__endswith: 'ph')[201..300].list()
// excluding results
def excluded = collection.exclude(ischild: true).list()
// get a map of members
def map = collection.in([1, 3, 5])
Basic LCRUD
For these examples, the following simple resource model is defined in the
/app/models/persons.json file:
{
"fields" : {
"firstname": {"type": "string", "max_length": 30},
"birthdate": {"type": "date"},
"ischild": {"type": "boolean"}
},
"collections" : {
"children": {"memberFilters": {"ischild": true}}
}
}
For this example, a collection of persons is instantiated:
import zero.resource.TypeCollection
...
Type persons = TypeCollection.retrieve('persons')
List all members
A List of all collection Member instances can be retrieved from the database
with the list() method:
List<Member> everybody = persons.list()
everybody.each { person ->
println person.firstname
}
List a subset of members
A List of the subset of Member objects can be retrieved by passing a
filtering condition to the list method as an argument (see Filter Conditions
section below for further details):
List<Member> somePersons = persons.list(firstname__startswith: 'Jo')
somePersons.each { person ->
println person.birthdate
}
Create a new member
A new Member instance can be created and saved into the backing database with
the create(Member) method:
Member member = new Member(firstname:'Joe', birthdate:'1978-01-21', ischild: true) Member person = persons.create(member)
A shortcut is provided to simply send in a Map:
Member person = persons.create(firstname:'Joe', birthdate:'1978-01-21', ischild: true)
Retrieve a member
An individual collection member can be retrieved from the database with the
retrieve(Object) method:
Member person = persons.retrieve(1)
Update a member
A collection member can be updated in the database with the update(Map)
method:
Member member = new Member(id: '1', firstname:'Bob', birth_date:'1961-12-05') Member result = persons.update(member)
Similar to the overloaded create() methods, update() has a convenience
method for Maps:
Member result = persons.update(id: '1', firstname:'Bob', birth_date:'1961-12-05')
Delete a member
A collection member can be deleted with the delete(Object) method:
boolean result = persons.delete(1)
The delete() method returns true if the delete operation was successful,
otherwise it will return false.
Filtering and excluding members
A filter or exclusion can be applied to a collection to create a new collection that is a subset of the original. Each filtered collection is a distinct collection instance that can be stored, used, and reused independent of its parent. The filter conditions can be passed in dynamically, which eliminates the need for writing a different service for each subset of the data that the client might need. The filters can be chained, therefore each filter creates a new logical subset-collection. However, data is not retrieved from the database until a collection member is actually accessed.
filter()
The filter() method is used to create a new Collection instance that only
includes Members that meet the provided conditions.
def somePersons = persons.filter(firstname__startswith: 'Jo') def fewerPersons = somePersons.filter(firstname__endswith: 'e') ....or.... def fewerPersons = persons.filter(firstname__startswith: 'Jo').filter(firstname__endswith: 'e')
exclude()
The exclude() method is the opposite of the filter() method. Whereas
filter() includes Members that meet the provided conditions, exclude()
excludes Members that meet the conditions.
def birthdate_not_christmas = persons.exclude(birthdate__month: 12, birthdate__day: 25)
The exclude() and filter() methods return a new instance of Collection and
can therefore be chained without affecting the original Collection. These
methods can be used together. For instance, the following code snippet:
def filtered_and_excluded = persons.filter(ischild: true).exclude(firstname__in: ['Phil', 'Brian'])
Filter conditions
The filter conditions are specified using the following convention:
[field name][delimiter][operator]. This convention makes the
Model API and HTTP API symmetric (this convention makes it easy to
pass filters as URI parameters). The supported filters are:
| Comparison | Operand | Operator | Example |
|---|---|---|---|
| Equal to | any field type | equals | firstname__equals: 'Joe' |
| Equal to (shortcut) | any field | firstname: 'Joe' | |
| Greater than | any field | gt | firstname__gt: 'Joe' |
| Greater than or equal | any field | gte | firstname__gte: 'Joe' |
| Less than | any field | lt | firstname__lt: 'Joe' |
| Less than or equal | any field | lte | firstname__lte: 'Joe' |
| Ends with | string | endswith | firstname__endswith: 'oe' |
| Starts with | string | startswith | firstname__startswith: 'Jo' |
| Contains | string | contains | firstname__contains: 'Jo' |
| In | any field | in | firstname__in: ['Joe', 'Bob', 'Dan'] |
| Year equal to | date, date-time | year | birthdate__year: '2005' |
| Month equal to | date, date-time | month | birthdate__month: '12' |
| Day equal to | date date-time | day | birthdate__day: '5' |
| After | date, time date-time | after | birthdate__after: '2005-12-12' |
| Before | date, time date-time | before | birthdate__before: '2005-12-12' |
| Between | date, time date-time | between | birthdate__between: ['2005-12-12', '2006-12-12'] |
More than one filter condition can be combined with logical AND using a comma as a separator between the filters:
def somePersons = persons.filter(firstname__startswith: 'Jo', birthdate__before: '1990-12-12')
Paging collections
Often it is impractical and inefficient to return the full results of a
collection, the Model API supports a paging scheme you can use to indicate what
slice of data a list() or filter() call should return.
0 and the 10th at 9. Ordering of
members in a collection is transient and not fixed when filters are applied.
Paging using ranges
You can specify a range of the data using the following Groovy syntax:
def paged_people = collection[10..19].list()
Paging ranges can also be combined with filters:
def filtered_paged = collection.filter(firstname__endswith: 'ph')[200..299].list()
Paging using start and count
Some clients might need to specify the start index and the page size, or count,
rather than an explicit range of data. To do this, use the limit(start,count)
method on a collection as shown in the following example:
def filtered_paged = collection.filter(firstname__endswith: 'ph').limit(201,100).list()
in()
The in() method takes a list of identifiers and returns a map where the key is
the id of the Member and the value is the Member itself. This is useful when
you have a small set of Members to operate on:
def subset = collection.in([30, 10, 32021]) def member30 = subset[30] def member32021 = subset[32021]
orderBy()
The orderBy() method returns a new Collection instance with the orderBy
attribute of the new Collection set to the parameter of the method. The method
takes a list of strings and is provided to be used in the method chaining
abilities of filter() and exclude(), and paging:
// SELECT * FROM persons ORDER BY birthdate DESC def youngest_to_oldest = collection.orderBy(['-birthdate']).list() // SELECT * FROM persons WHERE firstname LIKE '%don' ORDER BY updated def filtered = collection.exclude(firstname__endswith: 'don').orderBy(['updated']).list()
latest()
The latest() method returns the most recently updated Member of the Collection
instance. This method can be used with filter() and exclude() methods in
order to refine the Collection first:
def latest = collection.latest() def filtered_latest = collection.filter(birthdate__year: '1978').latest()
The latest() method is simply a shortcut for setting the orderBy attribute
of the Collection instance, limiting the fetched size to 1 and returning the
first result.
iterator()
Collection implements Java's Iterable interface and returns an Iterator of its Members:
def persons = TypeCollection.retrieve('persons')
persons.each { member ->
println member.firstname
}
size()
The size() method will return the total matched size. If the Collection
is unfiltered, the size is the total number of Members in the Collection.
Calling this method has the side affect of evaluating the Collection if it has
not yet been. The size is cached so subsequent calls to this method do not
require accessing the database again:
def persons = TypeCollection.retrieve('persons')
def numberOfPeople = persons.size()
def numberOfSomePeople = persons.filter([first_name__startswith: 'B']).size()
Remember, size() is a method on Collection and can be chained with any
method that returns a Collection as shown above with filter().
Named collections
Filters can be specified in the model declaration by defining a named collection:
{
"fields" : { . . . },
"collections" : {
"children": {"memberFilters": {"ischild": true}}
}
}
In Groovy, a named collection is accessed in the following way:
def persons = TypeCollection.retrieve('persons')
def children = persons.children.list()
def childrenNamedBob = persons.children.[0..4].list(firstname: 'Bob')
Data Validation
Zero Resource Models provides a robust data validation API. A call to
Member.validate() returns a data structure that contains error information.
A data structure is useful because you can decide how to represent this
information back to the HTTP response:
def member = new Member('person', [firstname: 'Joe', birthdate:'1978-01-21'])
def messages = member.validate()
if(messages) {
// do something useful with messages
} else {
TypeCollection.retrieve('person').create(member)
}
The data structure is simply a map of lists (i.e. List<Map<String,String>>.
The map prevents more than one field from having validation errors. The map's
key is the field name. The value is a list of error messages. The list as
values in the map prevents each field from being in violation of more than one
validation rule.
The validate() method is also called in the create() or update() methods
if the data is not yet validated. If a validation error occurs, the
zero.resource.exceptions.ValidationException is thrown:
try {
persons.create(firstname:'Joe', birthdate:'1978-01-21')
} catch (ValidationException e) {
def messages = e.messages
if (messages) {
// do something useful with messages
}
}
Custom Resource Event Handlers
The Model API can be easily used in resource event handlers for RESTful resource access. If the black box ZRM REST enablement doesn't suit your needs, you can use the API and implement your own RESTful resource handling while still leveraging the simplicity ZRM offers.
The following Groovy example shows a file in the /app/resources/my_json_person.groovy file illustrates how to utilize the API in order to customize you resource handling needs:
import zero.resource.TypeCollection
def onList() {
def collection = TypeCollection.retrieve('person')
request.status = 200
// assign the Collection
request.json.output = collection
request.view = 'JSON'
render()
}
def onCreate() {
def collection = TypeCollection.retrieve('person')
def json = zero.json.Json.decode(request.input[])
def data = collection.type.fromJson(json)
data = collection.create(data)
locationUri = getRequestedUri(false) + '/' + data.id
request.headers.out.Location = locationUri
request.status = 201
request.json.output = data
request.view = 'JSON'
render()
}
def onRetrieve() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
def data = collection.retrieve(id: memberId )
request.status = 200
request.json.output = data
request.view = 'JSON'
render()
}
def onUpdate() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
def json = zero.json.Json.decode(request.input[])
def data = collection.type.fromJson(json)
data.id = memberId
data = collection.update(data)
request.status = 204
}
def onDelete() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
collection.delete(memberId)
request.status = 204
}
Overriding ZRM.delegate()
It is possible to delegate into the ZRM HTTP default implementation while also
providing functionality. In order to do this, first create your own Resource
Event Handler as shown in the previous example. Instead of completely overriding
all behavior, simply call ZRM.delegate() in each method. You can provide
your own custom logic before and after the ZRM.delegate() method call.
Further, you can obtain a reference to relevant objects after making the call
into the ZRM black box implementation. Collection and Member objects
are made available in the /event/resource zone upon completion of the
ZRM.delegate() call. Consider the following example:
import zero.resource.TypeCollection
def onList() {
ZRM.delegate()
def collection = event.resource.collection[]
}
def onCreate() {
ZRM.delegate()
def collection = event.resource.collection[]
def createdMember = event.resource.member[]
}
def onRetrieve() {
ZRM.delegate()
def collection = event.resource.collection[]
def retrievedMember = event.resource.member[]
}
def onUpdate() {
ZRM.delegate()
def collection = event.resource.collection[]
def updatedMember = event.resource.member[]
}
def onDelete() {
ZRM.delegate()
def collection = event.resource.collection[]
}
It is important to note that once the ZRM.delegate() method is called, all
HTTP response processing has been completed. Thus, the HTTP method has been set
and any response body content has been set. Logic done before the
ZRM.delegate() call can affect the response, however.
Using the JSON Renderer
The example in the previous section illustrates how to use the TypeCollection,
Type, Collection, and Member classes in your own resource event handler.
Collection, Type, and Member classes can be assigned as JSON output and handled
by custom converters provided by the zero.resource component. In fact, the
black box HTTP API leverages the JSON rendering framework as well.