Avni Model Persistence Framework

History

There were two ways to register object schema in the realm database.

  1. Provide a class (and realm will look for "schema" property in the class to get the definition.
  2. Provide a plan JavaScript object describing the object's schema.

In earlier versions of Avni models with Realm, we were using approach 1. This meant that when we did db.objects(...) the objects returned were of model class with all the domain methods on it.

On upgrade

On the upgrade of Realm to 10.x, taking approach 1 now required that the registered class must have a constructor which takes specific arguments and extends from Realm.Object class (calling it super). This by itself was not an issue but the model class now cannot be constructed outside the realm database context (e.g. db.write taking a code block). One could not simply do new Encounter() anymore.

The above was a huge issue for us since we used our models in service, actions, and rules - in the avni client and in the web app. Avni models were imagined to be plain rich domain objects (like we use with Hibernate). Hence we had to choose option 2 and create the model class objects before handing them over to the rest of the code. Note that the rest of the code uses fields as well as methods of model classes.

Solution adopted

When an object is loaded from the realm using db.objects(...).filtered(...) we will objects that are not instances of our model classes - as the realm is not aware of the model class. So before these objects are given to the rest of the code to call methods etc, we wrap them under model classes.

Since all objects fetched from the database come from db.objects(...), the db class is proxied. See RealmProxy class which proxies the actual realm class. Other realm classes like RealmResults, RealmList, etc can also be proxied now - so see classes RealmResultsProxy, RealmListProxy, etc. These classes ensure that the object getting back to the caller is model classes or collections that have models.

What does the model look like?

See ProgramEnrolment (https://github.com/avniproject/avni-models/blob/master/src/ProgramEnrolment.js) as an example.

  • constructor with that argument
  • note the getters/setters delegating to that

that is the actual object that realm got out from the database. Our proxies when creating model class instances pass that to the model class. Model classes in getters for other model object instances (like EncounterType) or model collections (like Encounters) instantiate the model classes backed by realm. You can follow the helper methods in PersistedObject which do this.

that is not meant to be used outside these framework classes (PersistedObject, framework classes, etc). See all framework classes here - https://github.com/avniproject/avni-models/tree/master/src/framework

When model instance classes are saved then we get hold of the underlying object that and persist that. thats are connected to children that. e.g. that in program enrolment is connected via program encounters or encounter type property to a that(s) correspond to them. Also, model instance ProgramEnrolment is not linked to EncounterType and ProgramEncounter model class instances.

Pattern of usage

When we do db.objects(...) or db.objects(...).filtered(...) and we want to our model class instances, we can simply do .map(_.identity)

Limitations

  • We cannot use JS spread operator in model objects
  • We cannot use Object.keys on model objects (as it would give back that which is not what we want)
  • lodash binds to Array.prototype functions that mutate array ('pop', 'push', 'shift', 'sort', 'splice', 'unshift'). this implies that if lodash is used on RealmListProxy which is an array as it extends from it, our overridden methods will not be called. Hence we cannot use these methods to mutate. We usually don't use lodash to mutate realm lists, but if it is done then we can simply use array methods available to us which have been overridden in RealmListProxy.

Bypass for performance reasons

When we display a list of objects in the mobile view (like Individual Search Results), we are providing infinite scrolling to the user. Hence we cannot follow the approach of converting all the realm objects to model instance objects and providing it to the

. Hence in this case we can use the function getUnderlyingRealmCollection from our model framework. The underlying collection is provided to FlatList and when renderItem or renderRow etc are called then we materialize it to model class instance.

More details

TOOL - INTELLIJ LIVE TEMPLATES

for defining properties (as they can be mistake-prone)

for entity collection properties

get $prop$() {
    return this.toEntityList("$prop$", $entity$);
}

set $prop$(x) {
    this.that.$prop$ = this.fromEntityList(x);
}

for sub-entity properties

get $prop$() {
    return this.toEntity("$prop$", $entity$);
}

set $prop$(x) {
    this.that.$prop$ = this.fromObject(x);
}

for primitive properties

get $prop$() {
    return this.that.$prop$;
}

set $prop$(x) {
    this.that.$prop$ = x;
}