-
Notifications
You must be signed in to change notification settings - Fork 51
Persistence Engine Overview
This document describes the features of the Persistence Engine in Adventurer's Codex, and a high level overview of the design. For further implementation details see the source code documentation for the Engine.
Adventurer's Codex is powered by the HTML 5 Local Storage API. At it's core, Local Storage is just like a regular Javascript Object, but it's persisted between browser sessions.
Adventurer's Codex uses Local Storage for all of it's client side data storage, but when used bare, the Local Storage API is quite primitive. Instead of accessing Local Storage directly, code in Adventurer's Codex interacts with Local Storage through a custom Persistence Engine. This engine provides 3 overarching features to modules across the application.
-
The Persistence Engine tracks data throughout the application and ensures that data is stored uniquely, and without repeated entries. In this role it mostly functions as an organizer of the data stored in local storage.
-
It functions as a lightweight ORM that maps the raw JSON to and from AC Model objects. This allows Models to leave the serialization of their data to the engine and removes boilerplate code.
-
It provides a way for modules to search for and filter data by Model type (similar to high level SQL-esq features).
The Persistence Engine allows for the creation of mapped model objects which are serialized and persisted automatically. To do this, we must first create a Mapped Model Object. In Adventurer's Codex is is recommended to leverage the KO.Mapping API to create mappings between models and the serialized data, but some older code will perform this transformation manually.
function User() {
var self = this;
self.ps = PersistenceService.register(User, self);
self.firstName = ko.observable();
self.lastName = ko.observable();
self.fullName = ko.computed(function() {
return self.firstName() + ' ' + self.lastName();
});
}
In the above example, we define a few properties and a method for our user class, and then we register the class with the persistence service, and save the resulting token. This token allows us to shortcut the usual, state-less methods for persisting data, and leverage the power of our mapped models.
Once we have the user model laid out, we need to map it. We first create an ignore section on our mapping to tell the Persistence Service to ignore the values of both mapping
and fullName
since they are either static, or computed. Once we've done that, we can provide 2 short methods that perform the serialization and deserialization to our raw data.
function User() {
// ...
self.mapping = {
ignore: ['fullName', 'mapping']
};
self.importValues = function(values) {
ko.mapping.fromJS(values, self.mapping, self);
};
self.exportValues = function() {
return ko.mapping.toJS(self, self.mapping);
};
self.save = function() {
self.ps.save();
};
self.delete = function() {
self.ps.delete();
};
}
Once we've specified the import and export functionality, we can call self.ps.save
which will invoke the save method on our stored Persistence Token and save our model.
Note: When a model object is persisted, it is assigned an internal, unique ID. If needed, you can access this ID via any retrieved model's __id
property. This property is automatically added to each model when it is persisted and although it is possible to change the value of this identifier, doing so is not recommended and not supported.
Persistence Tokens allow Models to not have to worry about writing lots of boiler-plate code and instead just call the various save and delete methods on themselves to persist or remove their data.
Persistence Tokens also provide a findAll
method which is a shortcut method to find all the objects of a given class.
There are a number of high-level API endpoints that modules can use to interact with the data in the Persistence Engine. They are laid out below.
Making a query using the Persistence Engine is very simple, and there are a number of ways to get at the data you might need.
To get a list of all of the tables in storage you can use the following API.
var tables = PersistenceService.listAll();
Once you have a table you'd like to query, you can use the following methods to get data from them.
If you have set up mapped objects then you can query for them based on the Model name like so:
var people = PersistenceService.findAll(Person);
Note that for this call the Persistence Engine expects the actual function-object for your model classes. This will return a list of mapped model objects that you can use in your code.
Note: The findAll
method, like it's name implies, returns a list of all records stored in the database for a particular table. With large data sets this is not very efficient as mapping operations are performed on every object in the returned set.
As of v1.1 of Adventurer's Codex, the PersistenceService now supports a more granular query syntax.
// This method takes a model type, a property and a value to match.
// It returns all results that match this filter.
var europeans = PersistenceService.findAllBy(Person, 'ethnicity', 'european');
// This method takes a model type, a property and a value to match.
// It returns the first result that matches or null if no match is found.
var bob = PersistenceService.findFirstBy(Person, 'name', 'Bob');
These can be really useful for developers, and is the recommended way to query database state in Adventurer's Codex.
For example, the following code will retrieve a list of raw Person records from the database.
var rawPeople = PersistenceService.findAllObjs('Person');
Note: This is probably not useful to you, as these are unmapped objects in their raw Javascript object form. This method is useful for accessing data in testing and debugging, or in doing database migrations as it does not depend on internal model state.
Warning: While this is a more performant method of retrieving data from the Persistence Engine, it is important to note that this it technically an internal API and it's structure could change at any time.
Persisting data with the Persistence Engine is simple, even without the use of Persistence Tokens.
You can save any mapped model using the following syntax:
PersistenceService.save(Skill, mySkill);
The same goes for deleting data.
Note Currently this API is not at it's preferred state by the Author's admission. There is room for improvement.
PersistenceService.delete(skill, skill.__id);
In addition to containing object-specific operations, the Persistence Service also has support for dropping and listing tables.
// List all tables. (This operation is highly optimized for performance and does not search all the tables).
var tables = PersistenceService.listAll();
// Drop a table.
PersistenceService.drop('Skill');
//Drop all tables.
PersistenceService.dropAll();