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.
Important classes to know
The involved class types are Member, Collection, Type, 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()
The orderBy() method supports ordering that is both ascending and
descending. The example above illustrates both usages. If the field is prepended
with "-" than this indicates that the order is to be descending as used with
['-birthdate']. Ascending should not have a "-" as in ['updated'].
You should note that orderBy() takes a list of strings that are field names.
Thus you can order by multiple fields using both ascending and descending. For
instance, the following example specifies three fields and the equivalend SQL:
// SELECT * FROM persons WHERE firstname LIKE '%don' ORDER BY updated, firstname, birthdate DESC def filtered = collection.orderBy(['updated', 'firstname', '-birthdate']).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 the Java™ 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
}
}
Working with large byte arrays
You can store and retrieve large byte arrays by declaring a field in your model as follows:
// app/models/mycontent.json
{
"fields":{
"content": {"type":"string", "format":"any"}
}
}
This snippet illustrates the use of the any format of a string. Byte arrays transferred
using the ZRM's HTTP API is encoded in BASE64. Thus, byte arrays are a format of string.
However, when using the Model API, you can store a byte array without BASE64. Just as you
would call the create() method and pass in a string, date, or integer as a
value for a field, you can pass in a byte array for a field with the string format as
any. For example, assuming the above declaration of a content field in the mycontent
model, set and retrieve byte arrays as follows:
// get a reference to the mycontent model type def mycontents = TypeCollection.retrieve('mycontent')
// get some bytes from a string (could be from anywhere) // and store in the ZRM-backed database def somearray = 'an array were some bytes are obtained'.bytes def somecontent = mycontents.create(content: somearray)
// retrieve the stored bytes as cast as string def somecontent = mycontents.retrieve(somecontent.id) def contentstring = somecontent.content as String