In Part I of this blog series I discussed how object-oriented programming (OOP) can be used to simplify some common tasks in OpenText Content Server. I introduced RHNode
and RHUser
, which wrap the basic functionality of a Content Server node and user into an object. I also highlighted that similar classes exist for permissions, workflows, and other Content Server types.
In this blog post I introduce RHModel
, which is a framework for creating your own classes and objects that can be persisted. Although the framework uses the database to store data behind the scenes, it's easier to think about your data as objects instead of as a Record
or RecArray
. The framework abstracts away the database interactions such that you don't need to write any SQL to query, save, update, or delete data. Furthermore, the framework manages foreign-key relationships between objects such that referential integrity is maintained (e.g., deleting a "parent" cascades to its "children").
The motivation for this framework came after years of creating custom tables in Content Server. Custom tables are time consuming, difficult, error-prone, highly repetitive, and require a lot of code. It was while working with the Django Web framework (opens new window) and Apple Core Data (opens new window) that I realized many of these practices could be applied to OScript. The RHModel
framework is inspired by these frameworks, and much of the syntax I use will be familiar to anyone who knows them.
The RHModel
framework embraces the don't repeat yourself (opens new window) (DRY) principle by deriving the underlying database schema and the API from a single definition. This means you can define your data model with RHModel
, run a script, and the base API and underlying database table will be ready to use.
# Example
Let's jump in with an example that demonstrates how this works. Say we want to create a simple Contact
model with the following fields:
firstName
- the first name of the contact, requiredlastName
- the last name of the contact, requireddob
- the date of birth of the contact, optionalemail
- the email address of the contact, optional
To start, you must subclass (or "orphan" in OScript terminology) $RHModel.Model
and $RHModel.ModelDB
into your OSpace and give them the name Model
and ModelDB
. These classes are closely related, but are kept separate to keep the API and database layers apart.
Below the Model
and ModelDB
classes we need two Contact
subclasses (one under each; screenshot to follow). A few features on these subclasses need to be implemented:
RHModel :: Contact
- setfEnabled
to true;RHModelDB :: Contact
- setfEnabled
to true; andRHModelDB :: Contact
- setfTableName
to something unique (e.g.,mymodule_contact
). This is the database table where the data will be persisted.
Finally, the FieldInstances()
method on RHModelDB :: Contact
needs to be implemented to define the fields of the model:
function List FieldInstances(Object prgCtx=.fPrgCtx)
return { \
._('PKField').New(prgCtx, 'id'), \
._('StringField').New(prgCtx, 'firstName'), \
._('StringField').New(prgCtx, 'lastName'), \
._('DateField').New(prgCtx, 'dob', false), \
._('StringField').New(prgCtx, 'email', false) \
}
end
2
3
4
5
6
7
8
9
Here is a screenshot:
This may look odd but is actually quite simple. The underscore ._()
method is a shortcut to fetch the field definition from the related subsystem, and the parameter is the field type (e.g., PKField
is a primary key field, Stringfield
is a normal string field, etc.). The New()
constructor requires at least a prgCtx
and a field name, but more parameters are accepted. For example, the optional false
parameter on the dob
and email
fields indicates the field values are optional and can be set to null
(or Undefined
). More options are available (e.g., adding a database index), but I'll leave it at that for now.
One requirement is that every model has a PKField
named id
. There is no exception to this.
Once this is setup we can generate the base API and underlying database schema. The database table can be quickly created with the _ResetTable()
function on ModelDB
, which is a developer shortcut to completely wipe and recreate the underlying table. A formal approach for managing the table is provided on the admin.index
page and is where an administrator installing the module would do that. Table schemas can also be versioned, which helps provide a migration path with new versions of a module.
The API is dynamically generated at system start, so we just need to restart Builder (after a "Build OSpace", of course) and we're ready to start interacting with the model. The framework automatically synthesizes setter and getter methods (in the form setFieldName(value) and fieldName()) for each field, and adds the model class to the globals with an "RH" prefix (e.g., $RHContact
). The prefix can be changed in the model if you'd prefer something else.
With everything ready we can start interacting with the model:
// create a new contact instance
Frame contact = $RHContact.New(prgCtx)
// set some values using the synthesized setters
contact.setFirstName('Bob')
contact.setLastName('Patterson')
// save it to the database
contact.save()
// get the primary key of this new contact
Integer contactID = contact.id()
2
3
4
5
6
7
8
9
10
11
12
We can also fetch a model in another request and operate on it:
Frame contact = $RHContact.objects(prgCtx).get('id','=',contactID)
if IsDefined( contact )
// these are the synthesized getters
String firstName = contact.firstName()
String lastName = contact.lastName()
// update the email field
contact.setEmail('chris@rhouse.ch')
// update the database
contact.save()
// or, delete it
contact.delete()
else
// this contact doesn't exist
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
That's how easy it is to create, modify, fetch, delete, and persist objects.
# QuerySets
The last example introduced the objects()
method. This method returns a QuerySet
, which is an object that initially represents all objects in the system of that model type. The set can be reduced with a filter criteria using the filter()
or get()
method, which accepts a fieldName, operator, and value as parameters. For example:
// all contacts with lastName='Patterson'
Frame contacts_queryset = $RHContact.objects(prgCtx).filter('lastName', '=', 'Patterson')
// all contacts with firstname='Bob' and lastName starting with 'Pat'
Frame contacts_queryset = $RHContact.objects(prgCtx).filter('firstName','=','Bob').filter('lastName','startsWith','Pat')
2
3
4
5
The filter()
method supports a number comparison operators (e.g., equals, not equals, like, greater than, etc.), but also set operators (e.g., in, not in, and subqueries). SQL injection is not possible, which means a field name, operator, or value from a web request can be passed directly to the method.
The filter()
method returns the QuerySet
object such that the chaining of multiple filter criteria is possible (as shown in the second example above). No database calls are made on the QuerySet
until one of the following methods is called:
fetch()
- executes the underlying SQL and returns the database results as aRecArray
(rarely used);iterator()
oriter()
- resolves theQuerySet
and returns the results as anIterator
object (more on theIterator
class in a moment);get()
- filters and resolves theQuerySet
and returns the first object (useful when targeting a single instance); orgetByID()
- a shortcut forget()
when fetching byid
.
This means we can do any of the following:
// retrieve the raw data as a RecArray
RecArray contacts = contact_queryset.fetch()
// retrieve the contact with id=1 as long as it also matches the other filter criteria
Frame contact = contact_queryset.getByID(1)
// retrieve an Iterator
Frame contacts = contact_queryset.iterator()
2
3
4
5
6
7
8
# Iterators
I mentioned the Iterator
class in the previous blog post and here, so it's probably a good time to discuss it. The Iterator
class is simply a collection of items that can be iterated, but also contains optimizations to improve performance with large sets of objects. Looping an Iterator
is simple:
Frame myIterator = …
while myIterator.hasNext()
Dynamic item = myIterator.next()
// do something with item
end
2
3
4
5
6
This means we can iterate our contacts
like this:
while contacts.hasNext()
Frame contact = contacts.next()
echo( contact.firstName() + " " contact.lastName() )
end
2
3
4
The Iterator
object has a few methods of its own, such as:
next()
- get the next item (as seen in the examples above)totalCount()
- returns the total number of itemsreverse()
- returns a newIterator
with the items reversedcounter()
- used within a loop it gives the 1-based index of the loop (effectively how many timesnext()
has been called)counter0()
- similar tocounter()
, but the 0-based indexsort(<parms>)
- sorts the items in the iterator
Let's see this in use:
// sort the contacts by the lastName field
Frame sortedContacts = contacts.sort({'lastName'})
while sortedContacts.hasNext()
Frame contact = sortedContacts.next()
echo( Str.Format("Record %1 of %2:", sortedContacts.counter(), sortedContacts.totalCount() )
echo( contact.firstName() + " " contact.lastName() )
end
2
3
4
5
6
7
8
The Iterator
class shows up in a few other places (e.g., the children()
method on RHNode
, memberships()
method on RHUser
, and elsewhere), but it can also be used on its own:
RecArray myRecArray = CAPI.Exec(…)
Frame myIterator = $RHCore.Iterator.New(myRecArray)
while myIterator.hasNext()
Record rec = myIterator.next()
// do something with rec
end
2
3
4
5
6
7
# Foreign-Key Relationships
The RHModel
framework shines when it comes to foreign-key relationships. The ForeignKeyField
field is available to link to other models, but also available are the KUAFField
and DTreeField
fields to link to DTree
and KUAF
. These are just integers at the database level, but are treated quite differently at the API level.
Let's extend our original Contacts
model to include a KUAF
field, which means each contact will belong to a Content Server user:
function List FieldInstances(Object prgCtx=.fPrgCtx)
return { \
._('PKField').New(prgCtx, 'id'), \
._('KUAFField').New(prgCtx, 'user'), \ /** added user field **/
._('StringField').New(prgCtx, 'firstName'), \
._('StringField').New(prgCtx, 'lastName'), \
._('DateField').New(prgCtx, 'dob', false), \
._('StringField').New(prgCtx, 'email', false) \
}
end
2
3
4
5
6
7
8
9
10
Everything is the same except we now have a setter and getter for the user field. These look like this:
// Construct an RHUser based on the user defined in prgCtx
Frame user = $RHCore.RHUser.New(prgCtx)
contact.setUser(user)
// UserID is also accepted
contact.setUser(prgCtx.USession().fUserID)
2
3
4
5
6
A foreign-key getter returns the target as a fully instantiated object and not as the integer that connects the relationship at the database level. That is:
// fetches the user as an RHUser (not as an integer)
Frame user = contact.user()
// we can then call methods on the user
String displayName = user.displayName()
2
3
4
5
Of course, models are also key-value compliant (key-values were introduced in Part I), which means the following works as well:
String displayName = contact.valueForKeyPath('user.displayName')
Finally, the framework also provides methods for following reverse foreign key relationships. This is not demonstrable with the current Contact
example, so we'll leave it at that for now and get back to it another time.
# Adding methods and callbacks
Models can also be augmented with additional methods. For example, a fullName
method could be added to the Model :: Contact
class:
function String fullName()
return .firstName() + " " + .lastName()
end
2
3
This method can be called directly, but is also key-value compliant:
String fullName = contact.fullName()
// or
String fullName = contact.valueForKeyPath('fullName')
2
3
Finally, RHModel
provides a number of "callback" methods that are executed on different events. One such method is didChangeValueForKey()
, which is called whenever a setter has changed the value of a field. These can be overridden to add logic to your model. For example, say your model has an isActive
boolean field, and you'd like to update the activeDate
date field whenever isActive
is set to true
(or Undefined
when it's set to false
). This is simply:
function Void didChangeValueForKey(String key, Dynamic oldValue, Dynamic newValue)
switch key
case 'isActive'
if newValue
.setActiveDate( Date.Now() )
else
.setActiveDate( Undefined )
end
end
end
end
2
3
4
5
6
7
8
9
10
11
The model class is the optimal place to add methods that relate directly to your objects. No longer do you need to call and pass data to outside functions in some "utils" package.
# Summary
The use of RHModel
has replaced my need to write custom database tables in Content Server. The model framework abstracts away the nuances of creating and managing tables, and provides a rich, clean, and object-oriented API for interacting with my data. I'm already using the model framework in a few projects with multiple levels of foreign-key relationships and can't begin to express how much simpler it has made things.
In my next blog post I will introduce a new template language for simplifying the rendering of HTML. Stay tuned!