. . .

Best practices

This page contains some common use cases and proven practices when using ApiOmat.

Versioning

Most changes in the API are caused by adding more classes and attributes to a module. ApiOmat itself can handle these changes, even if the data is stored in MongoDB. Also, running apps with old SDKs will continue to work, because unknown data is ignored during requests. This implies that even pure REST requests will still work with missing or unknown attributes; these will be ignored by the ApiOmat REST API.

Changes in class or attribute names cannot be managed automatically, because data conversions or deletions in the database cannot be made without manual intervention. Existing apps therefore won't work as expected after this kind of breaking changes.

If breaking changes have to be implemented, best practice is to leave the old module as it is and create a second one. Since ApiOmat 3.3 it's possible to create a new version of your module. The new module version should copy all classes and attributes of the old one, so it can access the data objects that were already persisted.

Caching

ApiOmat can connect to legacy systems using a variety of APIs. Although these non-mobile optimized systems can be made accessible for mobile devices very easily using ApiOmat, usually some actions need a caching mechanism to ensure proper performance on the users' side.

A typical use case would be fetching a large list of detailed results from a legacy system. Imagine the legacy system would only support calls which return a list of reduced details of entities:

List<Customer> getAllCustomersList()

The returned customer list will only contain the customers username, but all other attributes (details) of the customer such as the firstName, lastName, etc will be null. To get these details, a second request is needed which requires the name of a single customer:

Customer getCustomerDetails(String name)

Retrieving a list of thousands of customers would usually last several minutes depending on the legacy system. To speed up this use case, fetching the customer details could be cached in ApiOmat's MongoDB using only a few straightforward lines of code:

public List<Customer> doGetAll( String query, Request r ) {
List<Customer> returnList = new ArrayList<>( );
/* get customers from legacy system */
List<Customer> customers = searchCustomersInLegacy( query );
/* look for details in cache first and fetch from legacy system if not found */
customers.forEach( c -> {
IModel<?> cachedCustomer = this.model.findByForeignId( c.getForeignId( ), "Ordinary", "CustomerCache", r );
if ( cachedCustomer == null ) {
Customer detailCustomer = searchCustomerDetailInLegacy( c.getForeignId( ) );
returnList.add( detailCustomer );
/* save new in cache */
cachedCustomer = convertToCached( detailCustomer );
((CustomerCache)cachedCustomer).save( );
}
else {
returnList.add( convertToOriginal( cachedCustomer ) );
/* maybe check creation date and drop model from cache afterwards... */
}
} );
return returnList;
}

Everything that is needed is an additional non-transient class which will be used for storing the cached data. The example above used two convert methods to transform an object between both representations.

Automatic SDK download

Downloading frontend SDKs to your development environment can be invoked via a REST request in the same fashion as all other actions made in ApiOmat. To learn more, take a look at this article.

Internationalization / i18N

An internationalization of texts can be realized by using maps. The language code is the key and the text its associated value:

{"language_code1":"lang1_text","language_code2":"lang2_text",....}

The following examples demonstrate this.

Android
final String language_code="de";
final Conference conference = new Conference();
 
Map<String, String> i18n_name = new HashMap<String,String>();
i18n_name.put("de", "Wie benutzt man properties");
i18n_name.put("en", "How to use properties");
conference.setI18n_name(i18n_name);
 
Map<String, String> i18ns_guests = new HashMap<String,String>();
i18ns_guests.put("de", "Gastsprecher: John Doe");
i18ns_guests.put("en", "Guest speaker: John Doe");
conference.setI18n_guests(i18ns_guests);
 
conference.saveAsync(new AOMEmptyCallback() {
@Override
public void isDone(ApiomatRequestException exception) {
//if exception != null an error occurred
if(exception == null) {
Log.i ("Conference",conference.getI18n_name().get(language_code));
Log.i ("Conference",conference.getI18n_guests().get(language_code));
}
}
});
JavaScript
var LANGUAGE="de";
function showConference (conference)
{
console.log(conference.getI18n_name()[LANGUAGE]);
console.log(conference.getI18n_guests()[LANGUAGE]);
}
 
function initConference ()
{
var conference=new Apiomat.Conference();
var i18n_name = {
"de" : "Wie benutzt man properties",
"en" : "How to use properties"
}
conference.setI18n_name(i18n_name);
var i18ns_guests = {
"de" : "Gastsprecher: John Doe",
"en" : "Guest speaker: John Doe"
}
conference.setI18n_guests(i18ns_guests);
var saveCB = {
onOk: function () {
showConference(conference);
},
onError: function (error) {
console.log("error "+error);
}
}
conference.save(saveCB);
}

Data modeling / module structure / use of references

There are different approaches to model your data. We'll first go into the advantages of each approach on its own, and then propose a way to combine the two approaches.

Using references

Using the "Reference" type for class attributes is familiar to people working with SQL databases containing normalized data without redundancies, where "foreign keys" point to related data in other tables.

  • The dedicated API endpoints for working with references make it easy to add/get/remove references to/from an object at runtime

    • For example if there's an object at https://apiomat.yourcompany.com/yambas/rest/apps/YourApp/models/YourModule/YourClass/123 and the class of that object has a reference attribute "yourRef", than a POST to https://apiomat.yourcompany.com/yambas/rest/apps/YourApp/models/YourModule/YourClass/123/yourRef creates the reference.

  • No redundancy

    • A referenced object only has a single entry in the DB but can be referenced by multiple other objects

But:

  • When a client wants to show some data on a GUI and some of the data is in an object of class Person and some in an object of class Address, the client must first do two requests to get all data. This has several implications:

    • Multiple requests means there's a high latency between the client wanting to show some data and the client having retrieved all data

    • If the two classes (the referencing and the referenced) have a strong relation it might have implications on the transactionality when doing WRITE operations (PUT, DELETE) on existing objects. If one client fetches Person 123 and makes changes to Address 456 depending on the data in the person object, but a different client makes changes to Person 123 before the changes of the first client to the address have been sent to the server, the address change could lead to an invalid state.

  • Although the referencing and the referenced object can "live" on their own, when importing data to ApiOmat the referenced data must be imported first, followed by the referencing data, which leads to a dependency tree and thus the import can't be fully parallelized.

Using embedded objects

People with experience with NoSQL databases have heard about the practice to model data according to how the frontend needs it, with all relevant data being in one JSON document and this document being stored in the DB.

  • Clients only need to make one single request to Create/Read/Update/Delete objects including their referenced objects

    • Low latency

    • Transactionality

But:

  • Embedded references aren't shared. They only "live" within the embedding object.

    • Depending on the data model this can lead to redundant data, for example if there's a Person class and an Address class and two people live at the same address - there would now be two embedded address objects containing the same data (street, zip, etc.).

  • The embedded object can't be transferred on its own

    • If a client wants to work with only an embedded object, it needs to load the embedding object nonetheless, leading to larger request and response payloads.

  • The embedded object can't be created before the embedding object exists

    • This can be a problem when importing a lot of data, as you need to build up a dependency tree and you can't fully parallelize the import.

Combining references and embedded objects

A good way to model your data is to use a combination of the aforementioned methods:

  • Create non-transient classes with standard references for storing the data

  • Create transient classes with embedded objects for transferring the data to frontends

    • These transient classes are modeled according to the views in the frontend and they create/read/update/delete the actual data via the non-transient classes

Some things to keep in mind when using this approach:

  • Leads to some computational overhead on the server (instead of the client doing multiple requests to get all the data it needs, the data from multiple objects is turned into a new object here)

  • Leads to multiple DB requests per HTTP request

    • Still much faster than multiple HTTP requests which also lead to multiple DB requests though

  • Depending on the name of the class attributes the queries to the transient classes can't just be passed through to the non-transient classes

Also note that when using standard references there's the drawback of the "dependency tree" which can slow down the import of lots of data. One workaround for this is to not use the standard Reference type of ApiOmat, but instead use String attributes which contain the ID of the referenced object. When the classes using these references aren't meant to be "frontend classes" anyway, this isn't a downside regarding the API simplicity (the reference endpoints aren't used). But in this case advanced queries can't just be passed through but require proper handling via code (for example when the Person class has a "String reference" to the Address class, the query "address.city == 'Berlin'" can't just be passed through to the non-transient Person class). You'd have to first load filtered Person objects, then see if their "String-referenced" Address object has the proper country attribute (or alternatively include the country filter in the query and see whether the Address object search yields a result or not) and if not, don't include the Person object in the final transient PersonWithAddress object.

References and inheritance across modules

Currently (ApiOmat 3.0, 3.1, 3.2, 3.3) used modules are put as *.jar file into the lib directory of a module. When the module is deployed, the module and its dependencies live in its own classloader. This means there are no performance penalties when working with references or inheritance across modules, as the classloader contains everything.

The only downside of having a deep module hierarchy is more complex dependency updates and deployments. For example when module A uses module B and you update B via uploading it, you first need to download A so that the new B.jar is put in A's lib directory. But this can be automated with scripts and/or a proper CI/CD setup (Jenkins, GitLab CI/CD, Travis CI, ...).