|
|
|
Programmatic Model API
The Model API accesses resources programmatically using collection-like constructs. In addition to the basic CRUD programming model, ZRM introduces a list-function, the LCRUD model. The list function allows for listing and filtering collection members using simple conditions.
Object types
The involved object types are Collection, List, and Member
-
Collection - Implements the LCRUD interface that returns
List and Member objects.
-
List - The a standard Java
List.
-
Member - The construct that contains the definitions of the fields and their values. Each
Member has also a unique identifier and last updated the timestamp field.
The following example shows some common interactions with the Model API:
// 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()
// filtered list using 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[11..20].list()
// paged and filtered results
def filtered_paged = collection.filter(firstname__endswith: 'ph')[201..300].list()
Basic LCRUD
For these examples, the following simple resource model is defined in the /app/models folder:
// File: /app/models/persons.groovy
fields = [
firstname: [type: 'CharField', max_length: 30],
birthdate: [type: 'DateField'],
ischild: [type: 'BooleanField'],
]
collections = [
children: [member_filters: [ischild: true]],
]
For this example, a collection of persons is instantiated :
import zero.resource.TypeCollection
...
def persons = TypeCollection.retrieve('persons')
List all members
A List of all collection Member objects can be retrieved from the database with the list() method:
def allPersons = persons.list()
allPersons.each { person ->
println person.firstname
}
List a subset of members
A List of subset of the Member objects can be retrieved by passing a filter condition to the list method as an argument (see Filter Conditions section below for further details):
def somePersons = persons.list(firstname__startswith: 'Jo')
somePersons.each { person ->
println person.birthdate
}
Create a new member
A new Member object can be created in the database with the create(member) method:
def 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(id) method:
def person = persons.retrieve(1)
Update a member
A collection member can be updated in the database with the update(member) method:
def result = persons.update(id: '1', firstname:'Bob', birth_date:'1961-12-05')
The update -method returns true if the update operation was successful, otherwise it return false .
Delete a member
A collection member can be deleted with the delete(id) method:
def result = persons.delete('1')
The delete -method returns true if the delete operation was successful, otherwise it will return false .
Filters
Filtered collections
A filter 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 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.
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')
Filter conditions
The filter conditions are specified using the following convention: [field name][double underscore][operator]. This convention makes the programmatic 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 | 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 | CharField,EmailField,USPhoneField,USStateField | endswith | firstname__endswith: 'oe' |
| Starts with | CharField,EmailField,USPhoneField,USStateField | startswith | firstname__startswith: 'Jo' |
| Contains | CharField,EmailField,USPhoneField,USStateField | contains | firstname__contains: 'Jo' |
| In | any field | in | firstname__in: ['Joe', 'Bob', 'Dan'] |
| Year equal to | DateField, DateTimeField | year | birthdate__year: '2005' |
| Month equal to | DateField, DateTimeField | month | birthdate__month: '12' |
| Day equal to | DateField, DateTimeField | day | birthdate__day: '5' |
| After date | DateField, DateTimeField, TimeField | after | birthdate__after: '2005-12-12' |
| Before date | DateField, DateTimeField, TimeField | before | birthdate__before: '2005-12-12' |
| Between | DateField, DateTimeField, TimeField | 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
Because 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.
Note: Paging collections uses a 0=-based index. For example, the first member in a collection is indexed at =0 and the 10th at 9. Ordering of members in a collection is transient and not fixed from collection to collection.
Paging using ranges
You can specify a range of the data using the following 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.limit(10,10).list()
This has the same effect as using [11..20]. You can also use limit(start, count) with filtering as shown in the following example:
def filtered_paged = collection.filter(firstname__endswith: 'ph').limit(201,100).list()
Named collections
Filters can be specified in the model declaration by defining a named collection:
// File: /app/models/persons.groovy
fields = [ . . .]
collections = [
children: [member_filters: [ischild: true]],
]
A named collection is accessed in the following way:
def children = persons.children.list()
def childrenNamedBob = persons.children.list(firstname: 'Bob')
Note that persons.list(ischild:true) returns a List while persons.filter(ischild:true) and persons.children return a Collection
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. 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
}
}
|