Retail:Developers Guide/How-to/MigrateToMasterdataInIndexedDB
Contents |
Introduction
The WebPOS uses several technologies to provide offline support, and WebSQL is a key one, as it allows it to store masterdata information like Products or Prices, so that they are available while there is no connection.
At the same time, WebSQL is a deprecated standard, and browser providers are considering removing support for it. In fact, it has already been removed from Safari by default, and can only be enabled back using a special configuration.
Therefore, we are in the process of migrating all usage of WebSQL across the application to IndexedDB. This changes involves a significant API change, and this document will guide you through the process of migrating your code related to masterdata storage and querying to this new API.
Core models
If your code is using Core masterdata models, like Product, BusinessPartner, TaxRate or Discount, then it may need to be changed.
It's important to note that the Backbone models for most of these masterdata models have not been removed, but they have been moved to IndexedDB. This means that if your code is receiving already information coming from these models (so for example, if you are reading the product in the receipt lines, or if you are reading the business partner from the receipt), then this code will continue to work as before.
However, if your code was explicitly doing queries to these models, then you will need to change it so that it uses the new masterdata API. You can check the main masterdata API documentation to understand how you can create queries for these models.
An easy way to find if you are doing queries is to try to find in your code lines similar to these:
OB.Dal.find(OB.Model.Product, {name: 'GPD'}, function(data){ console.log(data); }); OB.Dal.get(OB.Model.Product, id, function(data){ console.log(data); }); OB.Dal.query(OB.Model.Product, query, params, function(data){ console.log(data); });
All code doing queries in this way will need to be replaced by calls to the new API, as the old OB.Dal API is no longer valid for masterdata queries, and so far only remains as a legacy API for models not yet migrated to WebSQL you might have, or for the implementation of the Remote mode for Products and BusinessPartner models.
Custom models
First step: identifying masterdata models
The first thing is to identify which masterdata models your code is currently using. To do this, you need to search for models which are being added to the array of masterdata models in the application:
OB.OBPOSPointOfSale.Model.PointOfSale.prototype.models.push( OB.Model.BillofMaterialsProduct );
This is an example of this. If a model is currently being added to this array, it is a masterdata model using WebSQL, and we need to migrate it.
Second step: decide how to migrate it
The fact that something was initially defined as a WebSQL masterdata model, doesn't necessarily mean that it needs to be it. In several cases, we saw that a masterdata model implementation was used for a dataset that normally returned very few records. In those cases, in fact we recomment to use Terminal Properties instead. It is a good option in case you expect the data not to have more than around 50 records.
If your dataset will be bigger than that, then we recommend to migrate it to an IndexedDB model.
Third step: migrate the model to IndexedDB
For this, you need to do three mandatory actions, and an optional one:
- Remove the model from the list of WebSQL masterdata models. You need to remove the following code:
OB.OBPOSPointOfSale.Model.PointOfSale.prototype.models.push( OB.Model.BillofMaterialsProduct );
- Define a new Masterdata model which will replace your WebSQL model and register it in the MasterdataController:
function BOMProductDefinition() { class BOMProduct extends OB.App.Class.MasterdataModel { constructor() { super(); this.indices = [ new OB.App.Class.Index({ name: 'product_name_idx', properties: [{ property: 'product' }, { property: 'product_name' }] }) ]; } getName() { return 'BOMProduct'; } } OB.App.MasterdataController.registerModel(BOMProduct); }
You will notice that this model doesn't specify any structure or properties. This is different from previous Backbone models. Backbone models needed to specify the structure in the frontend, because a WebSQL relational table would be created from it. As IndexedDB simply stores Javascript plain objects, we don't need to specify a structure when defining it.
Extension of the model properties only happens in the backend Java classes. A new property added there (using the same ExtensionProperties mechanism used before) will automatically appear in the frontend models.
- Change your ProcessHQLProperty class in the backend so that it extends from MasterDataProcessHQLQuery class instead and annotate it with the @MasterDataModel annotation. See here to know more details.
- (Optional) Remove the Backbone model entirely, and change your code so that it understands plan Javascript objects instead.
This is the preferred course of action, as our support for Backbone models will drop at some point. If that's the case, then the API will return a plain JS object you can read directly:
const bomProduct= await OB.App.MasterdataModels.BOMProduct.withId(id); console.log(bomProduct.someProperty);
However, if you want to keep using it, you have the option of calling the function to get a Backbone model which you can still use in your current code:
const bomProduct= await OB.App.MasterdataModels.BOMProduct.withId(id); const backboneBOMProduct = OB.Dal.transform(OB.Model.BillofMaterialsProduct, bomProduct); console.log(backboneBOMProduct.get('someproperty'));
Fourth step: migrate the queries so they use the new API
With previous changes, if you log in the application, your model should be created in IndexedDB and it should be correctly populated with the data coming from the backend.
However, you still need to migrate your current queries so they use the new API. There are two main cases:
- Simple queries using OB.Dal.get, filtering by id, can be migrated by using the .withId endpoint of the API:
let bomProduct = await OB.App.MasterdataModels.BOMProduct.withId(bomProductId);
- For more complex queries, you need to use the .find endpoint of the API in combination with a Criteria. You can find many examples in the main masterdata API documentation.
And that's it! If you follow these four steps, your code will be using IndexedDB instead of WebSQL.
Additional considerations
Promises and callbacks
Old code in the WebPOS made heavy use of callbacks. In all new APIs, instead, we are using promises and we promote using the async/await keywords whenever possible, as we believe it makes code much easier to read.
This means that code that before would do something like this:
OB.Dal.find(OB.Model.BusinessPartner, {id: id}, function(data){ if(data && data.length>0) { OB.Dal.find(OB.Model.BPLocation, {bpartner: id}, function(locdata) { doStuffWithBP(data.at(0), locdata.at(0)); }) } });
Now will look like this:
const bp = await OB.MasterdataModels.BusinessPartner.withId(id); if(bp) { const criteriaBploc = new OB.App.Class.Criteria().criterion('bpartner', id) const loc = await OB.MasterdataModels.BusinessPartnerLocation.find(criteriaBploc); doStuffWithBp(bp, loc); }
Whenever possible, we are now using this style of coding. However, all new API functions return a promise, and it is technically possible to do the same thing using the usual .then() approach with a callback. So the same functionality could be implemented like this:
OB.App.MasterdataModels.BusinessPartner.withId(id).then(bp=>{ if(bp){ const criteriaBploc = new OB.App.Class.Criteria().criterion('bpartner', id); OB.App.MasterdataModels.BusinessPartnerLocation.find(criteriaBploc) .then(loc=>doStuffWithBp(bp, loc)); } })
Both approaches are technically correct, but we strongly promote the first one whenever possible, as we believe it leads to more readable code.
However, when replacing old code, there are some cases in which there are so many callbacks called one after another, that it makes replacing it by async/await quite difficult. In those cases, it's correct to use the .then syntax.
When replacing old code, you will then need to refactor it the way it makes more sense in each case.
Index definition and complex queries
When using the WebSQL API, it was possible to do whatever query that followed the basic rules of an SQL relational database.
However, IndexedDB has one main restriction: you need to define at each store indices in order to later on perform queries filtering by those indices.
Our API needs to comply with this restriction, so in order to implement a complex query (so, a query not simply filtering by the main id of the entity), it is important to try to ensure that there is at least one index associated with the model that contains the properties you want to use in the criteria of the query. This is explained in the main masterdata API documentation here.
For more complex queries, indices cannot be used, and results are iterated instead. It's important to read the previously mentioned section of the documentation to understand how to ensure good performance when the model may have a large number of records.
Joins in queries
In WebSQL, it was possible to join results from different tables in the database, by using the JOIN/INNER JOIN/LEFT JOIN SQL commands.
However, it is not possible to do this in IndexedDB. In case the records of two models needs to be joined, this needs to be done by executing two queries and joining the results in memory. As this operation is not performance efficient, in general it should be avoided, maybe by redefining the models so that it is not required, if possible.