Part II - Object Persistence in OpenText Content Server

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, required
  • lastName - the last name of the contact, required
  • dob - the date of birth of the contact, optional
  • email - 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 - set fEnabled to true;
  • RHModelDB :: Contact - set fEnabled to true; and
  • RHModelDB :: Contact - set fTableName 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
1
2
3
4
5
6
7
8
9

Here is a screenshot:

Model Setup

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()
1
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
1
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')
1
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 a RecArray (rarely used);
  • iterator() or iter() - resolves the QuerySet and returns the results as an Iterator object (more on the Iterator class in a moment);
  • get() - filters and resolves the QuerySet and returns the first object (useful when targeting a single instance); or
  • getByID() - a shortcut for get() when fetching by id.

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()
1
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
1
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
1
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 items
  • reverse() - returns a new Iterator with the items reversed
  • counter() - used within a loop it gives the 1-based index of the loop (effectively how many times next() has been called)
  • counter0() - similar to counter(), but the 0-based index
  • sort(<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
1
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
1
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
1
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)
1
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()
1
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')
1

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
1
2
3

This method can be called directly, but is also key-value compliant:

String fullName = contact.fullName()
// or
String fullName = contact.valueForKeyPath('fullName')
1
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
1
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!