Datamodels
Introduction
The Datamodels module is the heart of bitlibs. Datamodels provide abstractions around pieces of data in your application which actually represent instances of domain models. You define your own Datamodel class by stating the fields the model has, defining operations that can be performed on instances of that model and providing some configuration properties.
Use cases
The most common use cases for the Datamodels module are as follows:
- Defining what your domain models look like, including their fields, validation logic and custom behaviour
- Creating new instances of models using forms in the UI, validating them and saving them to a persistent store
- Fetching a particular instance of a model from a persistent store, editing it using forms in the UI, validating it and saving it back to the persistent store
- Fetching collections of model instances from a persistent store, using parameters for filtering, sorting, etcetera
This chapter explains how you define Datamodels, how you can use them once you have defined them and how you can extend the provided functionality by either subclassing, hooking into the behaviour at predefined points or by listening to emitted signals.
High-level overview
The Datamodels module is like an Object-Relational Mapper for your front-end applications. Datamodels have certain fields, each field being of a certain type which determines the allowed range of values. Fields are how you specify the structure of your data and how it should be validated.
Models can also be obtained from and persisted to a persistent store like an API. In this case the raw data fetched from the persistent store is deserialized into Datamodel instances and serialized back to the format accepted by the persistent store when saving those models. How models are validated is determined by model validators, and how they are (de)serialized by model serializers.
Defining Datamodels
You define the domain models in your application by creating subclasses of either the
Datamodel class or of one of its subclasses. There
are just two things that you are always required to define: the set of fields on the model and some model configuration properties.
If your domain models only exist 'locally' in your front-end application you can just subclass Datamodel directly. If however they also exist in some persistent store, like a datastore behind an API for example, then you will want to subclass one of its subclasses like APIModel. You can also create your own subclass, see section Subclassing Datamodel).
An example domain model definition is as follows:
import Datamodel from "lib/bitlibs/datamodels/Datamodel";
import * as fields from "lib/bitlibs/datamodels/fields";
class User extends Datamodel {
// The model configuration properties
static __config__ = {
name: "user",
primaryKey: "id"
}
// The fields on the model
__fields__ = {
id: new fields.NumberField({
label: "ID",
key: "id",
type: "number",
required: true,
validateOn: "change",
default: null
}, this),
name: new fields.StringField({
label: "Name",
key: "name",
type: "text",
required: true,
maxLength: 50,
validateOn: "change"
}, this),
salutation: new fields.MultipleChoiceField({
label: "Salutation",
key: "salutation",
type: "multiplechoice",
required: true,
options: [
{id: "sir", label: "Sir"},
{id: "madam", label: "Madam"}
],
freeValue: false,
default: "sir",
validateOn: "change"
}, this),
email: new fields.EmailField({
label: "E-mail address",
key: "email",
type: "email",
required: true,
maxLength: 150,
validateOn: "change"
}, this),
}
}
The different parts of this definition are explained in the following sections.
Configuration properties
The first step in defining a domain model is providing some configuration settings (metadata). The required settings differ per Datamodel subclass:
Datamodel
The Datamodel base class has only two configuration properties, both of which are required:
| Name | Type | Description |
|---|---|---|
| name | String | The uniquely identifiable name of the model. |
| primaryKey | String | The name of the primary key field of the model. See section primary keys. |
APIModel
The APIModel class has some additional configuration properties. In the table below, the ones with their name in bold are required:
| Name | Type | Description |
|---|---|---|
| apiRoot | String | The root of the RESTfull API endpoints for this model, relative to the base of the API. For example /users/. Requests to the API regarding this model are built relative to this root. |
| networker | Networker | The Networker instance to use to fetch instances of this model from and save instances to the API. |
| apiModelName | String | The name of the model on the API. If responses from the API are enveloped in a property with the name of the model, this should be set to that name. If null or undefined, bitlibs will expect responses to not be enveloped. |
| createEndpoint | String | A custom create endpoint, if different from apiRoot/. |
| retrieveEndpoint | String | A custom retrieve endpoint, if different from apiRoot/<id>/. |
| updateEndpoint | String | A custom update endpoint, if different from apiRoot/<id>/. |
| deleteEndpoint | String | A custom delete endpoint, if different from apiRoot/<id>/. |
| readModelClass | Constructor | The Datamodel class to use to deserialize responses to create calls to the API, if different from the class itself. Can be used to define a separate creation model from the read model. |
An example APIModel subclass configuration using some of the optional attributes:
import APIModel from "lib/bitlibs/datamodels/APIModel";
import User from "src/models/User";
import networker from "src/network";
class RegistrationForm extends APIModel {
// The model configuration properties
static __config__ = {
name: "registration_form",
apiRoot: "/users/",
networker: networker,
createEndpoint: `/users/register/`,
readModelClass: User,
apiModelName: "user"
}
// The fields on the model
__fields__ = {
...
}
}
In this example we define a user registration form model which will perform a post request to
the endpoint /users/register/ when saved, and deserializes the response enveloped
under the key user using the User model.
Defining fields
The fields on your models are defined in the __fields__ array. There are various types of fields, as listed in the Fields section, each having their own set of configuration properties that define how the field should behave. This influences things like validation, (de)serialization and default field values. Field constructors take two arguments: the field configuration and the Datamodel instance on which the field is defined.
For a complete reference of the available configuration properties see the Fields section.
Getters & setters
Fields are defined in a __fields__ property as not to pollute the namespace of
the object with the field instances. When datamodels are initialized, bitlibs
automatically creates getter and setter methods for all fields so you can still set and
get their value using the field name: user.name returns the value for the "name"
property on the user object and user.name = 'Jan' sets it.
Primary keys
There is one special field on models: the primary key field. You must define
a field with the key defined as the primaryKey configuration attribute on
your model.
Hierarchical models
Bitlibs also supports defining models in a hierarchical fashion to increase modularity and code reuse. You define models within other models as you define any other field by using the EmbeddedModel field type. For example:
...
__fields__ = {
address: new fields.EmbeddedModel(Address, {
label: "Home address",
key: "address",
type: "embedded",
many: false,
required: true
}, this)
}
...
This creates a property called "address" on instances of this model, which is
an instance of the Address datamodel class. In this case there is exactly
one address (as denoted by the many configuration property), which should always
exist (it is required).
You can create arbitrarily deep nestings of models using this strategy. If an
embedded model is required its instance will always exist, otherwise it may be null
if not present. When the many flag is set to true the value will be an array
of instances of the embedded model's class. See the section about EmbeddedModel for more information.
Hooks
The Datamodel class provides specific hooks which you can utilise to implement additional behaviour at certain points. This is one of the ways in which the bitlibs library enables easy customisation of its functionality. Other ways are creating subclasses of the builtin classes and by listening to signals emitted by the various classes of the library.
perform_initialize
The perform_initialize hook is called directly after the model instance is fully
initialised as far as the Datamodel class is concerned. It can be used to provide
additional initialization logic to run. It receives one argument: the context
with which the datamodel was initialised.
perform_validate
The perform_validate hook can be used to provide additional multi-field
validation logic to run after all individual fields have been successfully validated.
It is only executed in case the individual field validations did not yield any errors.
It does not receive any arguments. When there are validation errors, this method
should throw a dictionary keyed by field names with ValidationError instances
as values.
perform_save
The perform_save hook is only available to subclasses of Datamodel and should
implement the logic to actually save instances of that class in some way. It does
not take any arguments. It is not used by APIModel: that class handles creating and updating instances using
an APIPersistenceManager.
perform_delete
The perform_delete hook is also only available to subclasses of Datamodel and
should implement actually deleting instances of that class. It does
not take any arguments.
perform_serialize
The perform_serialize hook can be used to influence the serialization process
of models. After all field values have been serialized into a JSON dictionary, the
dictionary is passed to this hook for optional modification. This method is free
to add, delete or modify this JSON dictionary in accordance with application
requirements.
deserialize_preprocess
The deserialize_preprocess hook is handed the raw JSON dictionary to deserialize
into a model instance along with the context in which the model is being deserialized.
It can be used to modify this dictionary before using it
to populate the fields of a model instance with it. Use cases for this are to remove,
rename or add fields in the dictionary.
perform_deserialize
After a raw JSON dictionary of values has been deserialized into a datamodel instance, this hook is called to finish the deserialization process. It is passed the dictionary of context variables in which it was deserialized.
Custom attributes and methods
Besides the provided methods and hooks you are free to define any additional attributes and methods you require on your datamodel classes. Just make sure they do not clash with any field keys as this will not allow you to use the getters and setters for the fields that clash.
Using Datamodels
After you have defined your application's domain models you can start to benefit from working with instances of these models instead of with raw pieces of data. The following sections cover the most important use cases.
Creating new model instances
To create a new instance of a model class use the new static factory method.
This method completely initializes the model instance, thereby also calling any custom
initialization logic defined on the class. You can pass optional initial values and a context dictionary to the factory method. When creating a model, all required fields are initialised to their default values and all non-required fields are set to null, after which any provided initial values are set.
To define custom initialization logic implement the perform_initialize hook.
let user = await User.new({name: "Jan", address: {city: "Eindhoven"}});
Obtaining model instances
To obtain model instances from a persistent store see section Retrieving models. There are various ways of obtaining a single model instance or multiple model instances:
let jan = await User.get(1);
let users = await User.fetch({"address.city": "Eindhoven"});
let resultset = await User.query({"address.city": "Eindhoven"});
Model context
You can provide an arbitrary dictionary of values when creating new datamodel instances,
which will be saved on the instance. This context is passed to the initialization
methods of all fields defined on the model as well as the perform_initialize hook
and can be used by your application to adjust the behaviour of the model depending on the context in which it is instantiated.
Modifying models
After having created - or otherwise having obtained - a model instance you can modify its field values using several different methods listed below. When modifying field values they are marked as dirty unless specified otherwise. Datamodels keep track of their dirty fields to be able to for example patch only the modified fields when saving a modified model back to an API.
Setters
The first method is by simply using the field setters of the model (see Getters & setters).
You can set a field value like you would set any other attribute of an object.
You can also set field values of embedded models by traversing the hierarchy of
embedded models using the getters of the embedded models and the setter of
the nested field to set. The field setters use the setValue method under
the hood to set the field value. When using a field's setter, the field is always marked as dirty.
Examples:
user.name = "Henk";
user.address.city = "Eindhoven";
setValue
The second method is to use the setValue method directly. This method takes the
name of the field to set, optionally including a dotted path through embedded models,
together with the value to set for that field. It also takes a third optional argument
indicating whether the field should be marked as dirty, which defaults to true.
// Change the name of the user without marking the field dirty
user.setValue("name", "Henk", false);
// Change the value of the field "city" on the embedded model "address"
user.setValue("address.city", "Eindhoven");
setValues
The third method is to set multiple field values at once using the setValues method.
This takes a dictionary of field keys and values to set. When setting values on
embedded models, use a nested dictionary under the key of the embedded model. It also takes a third optional argument indicating whether the fields should be marked as dirty, which defaults to true.
For example:
user.setValues({
name: "Jan",
address: {
city: "Eindhoven"
}
});
Modifying fields directly
You can also set the values of fields directly instead of via the model on which
they are defined. Once you have obtained the field you want to set (see section
Getting field objects), simply call its setValue method.
clearValues
To clear model values and reset all fields to their default value, use the clearValues method.
Validating models
To validate a model's field values, call the validate method.
This will populate the errors attribute on the model itself and any of its
fields which contain an error with instances of ValidationError. It returns a boolean
indicating whether validation was successful (no errors occured).
The actual validation is performed by the model's declared ModelValidator.
You are not required to call the validate method yourself: when saving models this method is always called by
the Datamodel class before proceeding with the actual saving of the instance.
After being validated, any errors can be inspected via the hasErrors and getErrors
methods. Errors can be cleared by calling clearErrors.
You can also validate individual field instances by calling the validate method
on those fields directly.
Custom validation logic
Custom save logic can be implemented using the perform_validate hook.
Saving models
To save model instances, either newly created ones or obtained modified ones,
simly call the save method. This method does not take
any arguments and uses the PersistenceManager defined
on the model to persist it to some store. See section saving models
for more information.
let jan = await User.new({name: "Jan", address: {city: "Eindhoven"}});
await user.save(); // Creates a new instance of this user in the persistent store
let piet = await User.get(2);
piet.address.city = "Rotterdam";
await piet.save(); // Updates the model instance in the persistent store
Getting field objects
If you need to access the BaseField subclass instance for a particular field instead
of the field's value, use the getField method. You can provide it with the name of
the field to obtain, as well as with a dotted path through embedded models ending
in the property name of the field on the inner model.
let nameField = user.getField("name");
let cityField = user.getField("address.city");
Model signals
Datamodel instances have a ModelSignaller instance which emits various signals
you can listen to to hook into its functionality. You subscribe to a signal by
calling the add method on the signaller's property for that signal. The signaller
is defined on the signals attribute of models. You subscribe to signals
of a specific datamodel instance, not to all signals of all instances of that
model.
// Subscribe to the "fieldModified" signal of this user
user.signals.fieldModified.add((field, markDirty) => {
// Do something with the modified field instance
});
The various signals declared on the ModelSignaller class are as follows:
fieldModified
The fieldModified signal is emitted whenever the value for one of the fields
on a model changes. It receives one argument, namely the key of the field which
was modified.
modified
The modified signal is emitted whenever any of the fields on a model change.
This is emitted together with the fieldModified signal when a field value is
modified directly, but also when clearing the fields on a model for example.
You subscribe to signals
Dynamically adding/removing fields
Sometimes you need to be able to dynamically add fields to or remove fields from a model
"at runtime". You can do this using the addField and removeField methods. They both
accept as only argument the field instance to add/remove, like you would also
define it statically on the model. For example, to add an age field to a certain
user instance:
let user = User.get(1);
user.addField(new NumberField({
label: "Age",
key: "age",
type: "number",
min: 18
}), user);
Working with Datamodels in forms
You can easily work with datamodel instances in forms in your UI. The main idea
is to populate your form's inputs with the current field values of the model,
validate the individual field instances when required (for example oninput or
onblur) and display any validation errors in your HTML, and calling the save
method on the model instance when the form is submitted.
For additional view layer integrations for the supported frameworks, see the forms module.
Fields
Fields contain the logic to validate and (de)serialize the attribute values of datamodels and to report any errors. You configure the behaviour of fields at define time. There are some configuration properties which are global to all field classes as well as some field-specific ones.
General configuration properties
The configuration properties that are valid for all field types are as follows. The ones with their name in bold are required, the others are optional.
| Name | Type | Description |
|---|---|---|
| key | String | The key of the field. This will be the name of the getter and setter methods on the datamodel on which this field is defined, and determines the key under which the fields value is serialized. |
| label | String | The human-readable name of this field. Can be used to be displayed in the UI for example. |
| type | String | The unique identifier of the type of field. Can be used by the UI to determine how to display the input for a certain field type, like is done by the forms module. You can assign an arbitrary type string to every field subclass, as long as you use them consistently. |
| required | Boolean, Function | Whether this field is required. Whether a field is empty/blank is determined by the field's isEmpty method. Can also be a function receiving a dictionary of current model values as argument |
| default | any | The default value of this field, if different from the field type's default |
| editable | Boolean | Whether this field value can be modified once intialised. |
| validateOn | "change", "input" | When to validate this field. Used by the forms module to validate the field either oninput or onchange. |
| widget | String | Any custom widget to use to represent this field in the UI, if different from the type configuration property |
| validate | Function | Function which can implement additional custom validation logic for this field. Receives the field's value and a dictionary of all current model field values as arguments. |
| get | Function | A custom getter function for this field's value. Receives the field instance as argument. |
| set | Function | A custom setter function for this field's value. Receives the field instance and value to set as arguments. |
| serialize | Function | A custom serialize function for this field's value. Receives the field instance as argument. |
| shouldSerialize | Boolean | Whether this field should be included in serialized representations of the model on which the field is defined. Respected by the ModelSerializer class |
Field types
In addition to the general configuration properties there are also some field-specific ones. The following field types are provided by bitlibs.
StringField
Represents generic textual values.
Properties:
| Name | Type | Description |
|---|---|---|
| rows | Number | The number of rows to use to represent this textual field in a UI |
| minLength | Number | The minimum required length of the field's value |
| maxLength | Number | The maximum allowed length of the field's value |
NumberField
Represents generic numerical values.
Properties:
| Name | Type | Description |
|---|---|---|
| noFloat | Boolean | Whether to only allow real integers, meaning no real numbers. |
| min | Number | The minimum allowed value |
| max | Number | The maximum allowed value |
| stepsize | Number | The stepsize to use in the UI in for example stepper controls for this field. |
BooleanField
A value which can only either be true or false.
DateField
Represents a date.
The DateField class uses the momentjs library to parse dates.
Properties:
| Name | Type | Description |
|---|---|---|
| format | String | The format in which the date should be. For a list of supported formats see https://momentjs.com/docs/#/displaying/format/. If not specified, the default format is the locale default date format, as denoted by the format string "L". |
TimeField
Represents a time.
The TimeField class uses the momentjs library to parse times.
Properties:
| Name | Type | Description |
|---|---|---|
| format | String | The format in which the time should be. For a list of supported formats see https://momentjs.com/docs/#/displaying/format/. If not specified, the default format is the locale default time format, as denoted by the format string "LT". |
DateTimeField
Represents a date including a time.
The DateTimeField class uses the momentjs library to parse datetimes.
Properties:
| Name | Type | Description |
|---|---|---|
| format | String | The format in which the field value should be. For a list of supported |
formats see https://momentjs.com/docs/#/displaying/format/. If not specified,
the default format is YYYY-M-D H:mm.
EmailField
Represents email addresses.
URLField
Represents URLs.
PhoneField
Represents phone numbers.
PasswordField
Represents passwords.
ImageField
Represents base64 encoded images. Also accepts valid urls as value.
Properties:
| Name | Type | Description |
|---|---|---|
| supportedTypes | Object[] | An array of supported file types. Each element in the array must include at least a mimetype attribute. |
EnumField
A field having a bounded set of valid values.
Properties:
| Name | Type | Description |
|---|---|---|
| values | String[] | An array of valid field values. |
RegexField
A field whose value is validated using the provided regular expression.
Properties:
| Name | Type | Description |
|---|---|---|
| regex | String | The regular expression to validate the field's value against. |
JSONField
A field which can contain arbitrary JSON.
Optionally the field can be configured to validate its value against a given JSONSchema v6 schema.
Properties:
| Name | Type | Description |
|---|---|---|
| schema | JSON | If provided, the JSONSchema v6 schema to validate values against. |
ArrayField
A field containing an arbitrary number of homogeneous fields.
Can be used to represent multi-valued fields, where the user can specify any number of values for the field.
Properties:
| Name | Type | Description |
|---|---|---|
| subtype | String | The type of the field of which the ArrayField can contain an arbitrary number. |
| initialAmount | Number | The initial number of fields contained in this ArrayField. |
An ArrayField is instantiated using an extra argument besides the field config and
model instance: the class of the fields to contain. Its getValue and setValue methods also accept an extra argument: the index of the contained field for which to get/set the value.
It defines two additional methods: addField and removeField, which can be used to add and remove fields to/from the array of contained fields.
MultipleChoiceField
A multiple choice field has a set of predefined values from which it can contain exactly one. It can also optionally allow other values besides those.
Properties:
| Name | Type | Description |
|---|---|---|
| options | Object[] | An array of options, each option having id and label properties. |
| allowFreeValue | Boolean | Whether to also allow other values besides the predefined ones. |
MultipleSelectField
A multiple select field has a set of predefined values from which it can contain an arbitrary number.
Properties:
| Name | Type | Description |
|---|---|---|
| options | Object[] | An array of options, each option having id and label properties. |
EmbeddedModel
An embedded model can be used to embed an instance of a certain model class as field on another model. This enables composition of models in a hierarchical manner for increased code modularity and reuse.
Properties:
| Name | Type | Description |
|---|---|---|
| many | Boolean | Whether the field contains an array of embedded models instead of a single one. |
The constructor of EmbeddedModel accepts an additional argument besides the field
config and model instance on which it is defined: The class of datamodel being embedded.
It accepts either a single model instance or an array of them as valid values,
depending on the many config property. If many is set to true, the value
of the EmbeddedModel field is actually an instance of EmbeddedModelList.
RelatedModel
A related model is like and embedded model, but contains a single model ID or array of IDs instead of actual instances of models.
RelatedModel fields can only be declared on APIModel subclasses.
Properties:
| Name | Type | Description |
|---|---|---|
| many | String | Whether the field contains an array of related model IDs instead of a single one. |
The constructor of RelatedModel accepts an additional argument besides the field
config and model instance on which it is defined: The class of the related datamodel.
If many is false, it accepts either a single model instance or model ID as value.
The field value will then actually be an instance of RelatedModelInstance, which
has a fetch method that can be used to get the actual instance of the related model
with the corresponding ID.
If many is true, it accepts an array of either model instances or IDs as value.
The field value will then actually be an instance of RelatedModelList, which
contains some convenience methods amongst which a fetch method that fetches
the actual instances of all models in the related model list.
Field values
Field values are stored on field instances in an internal format which differs
per field type and are converted to/from their representation when setting and getting
field values using the getValue and setValue methods.
The internal format is also what is validated by the validate methods of fields.
This allows fields to for example work with a momentjs date object instead of with
the textual representation of a date and compare that to any minimum and
maximum dates specified in the field's config.
Errors
Field errors are stored on the errors property, which should be accessed using
the getErrors getter. They can also be set manually using the setErrors method.
The errors property is an array of ValidationError objects, as fields can contain multiple
errors. The getPrimaryError function returns the most significant error on the
field at that time, which is the first ValidationError in the array.
Field signals
Field instances, like datamodels, emit various signals you can listen to to hook into their functionality.
The signaller is defined on the signals attribute of fields. You subscribe to
field signals in the same way you subscribe to Datamodel signals.
The various signals are emitted using a FieldSignaller and are as follows:
modified
This signal is emitted whenever the field's value is changed using the setValue
method. It receives the field instance being modified as argument as well as whether it
was marked as dirty by this modification.
Subclassing BaseField
You can easily create your own, new field type by subclassing BaseField or one of the other provided fields. When doing so, the following methods are the most likely candidates for overriding:
| Method | Description |
|---|---|
| toRaw | This is the method used to convert set values to their internal representation. The default implementation simply passes them through unmodified, but if you have a particular internal representation of your field's values you should override this behaviour. |
| toRepresentation | This is the inverse of toRaw and converts the field's internal format to the representation of the value to be used in the UI. |
| validate | Should verify whether the current field value is valid and if not populate the errors array on the field instance. It does not return anything but receives all of the model's current field values as only argument. This method may also modify the field's value in order to "clean" it. |
| isFieldEmpty | Should return whether the current field value is regarded as "empty". |
Persistence Managers
A persistence manager persists datamodel instances to a certain persistent store
and retrieves specified (collections of) objects from that store. Different types
of persistent stores have different persistent managers for interfacing with them,
for example the APIPersistenceManager class is used to persistent objects to and retrieve
them from a RESTful API.
Every Datamodel subclass has the persistence manager it uses defined on the static attribute __manager__, which subclasses are free to override. For example, the Datamodel base class has an instance of the abstract base class PersistenceManager which does not actually implement any functionality. APIModel has an instance of an APIPersistenceManager which persists objects to and retreives them from a RESTful API.
Persistence managers use ModelSerializers to (de)serialize models to/from their store.
Retrieving models
You can retrieve instances of your datamodel classes in several ways: you can retrieve one specific model or a collection of models satisfying some criteria. These different methods are outlined below.
Getting models
When wanting to retrieve a single model by its primary key you use the static method get on your Datamodel subclass. It only takes the primary key value and an optional context dictionary
as attributes. The context dictionary is passed to the constructor of the returned Datamodel
instance and stored on the instance.
Fetching models
When wanting to fetch multiple models from a persistent store, use the fetch method. It
takes a dictionary of parameters to pass to the persistent store to specify which objects
to fetch as well as an optional context dictionary. This method directly returns the array
of fetched objects.
Querying models
If you do not want to directly receive the array of fetched objects from the persistent
store, use the query method. It works exactly the same as the fetch method but instead
returns a ResultSet instance containing additional information about the returned set.
Not all persistent stores might support this method.
ResultSet
A ResultSet contains additional information about the set of objects returned by the
persistent store, which differs per store. In case of a RESTful API interfaced by an
APIPersistenceManager it includes metadata about the total number of objects in the
set if the result was paginated, for example.
Saving models
Saving models is done by simply calling the save method on a datamodel instance.
This uses the persistence manager to either create a new object in the store or
update an existing object with the same primary key as the model instance.
Custom save logic
Custom save logic can be implemented using the perform_save hook.
Deleting models
Deleting models is done by calling the delete method on a model instance. This marks
the instance as deleted and also removes it from the persistent store. Note that
after deleting a model instance its behaviour during continued use is undefined: you
should discard the instance.
Custom delete logic
Custom delete logic can be implemented using the perform_delete hook.
Subclassing PersistenceManager
When implementing your own persistence manager to interface with a specific persistent store not already supported by bitlibs, these are the methods you need to implement:
get & perform_get
The get method is the method which gets called by the Datamodel class,
and calls the perform_get method for the actual implementation. If you need to
customize the way in which it calls perform_get, and/or when, you can opt
to override this method directly.
If you only need to override the actual implementation of getting objects from
the persistent store, you should implement perform_get.
It takes the class of the model to get, the primary key value and an optional
context dictionary as arguments. It should throw some error when the specified
object could not be found.
fetch & perform_fetch
Like get, this method calls perform_fetch whilst also performing some other
necessary steps. You can opt to override this method directly if you need.
The perform_fetch method should implement fetching certain objects according
to some specified criteria. It takes the class of models to fetch, the
criteria specification and an optional context dictionary as arguments.
query & perform_query
Like fetch & perform_fetch, but should return a ResultSet instance. You
are not required to implement this method if your persistent store does not
support it.
saveModel & perform_saveModel
Implement saving the model to the persistent store. Should both support saving new instances as well as updating/replacing existing ones based on their primary key.
deleteModel & perform_deleteModel
Should delete given model instances from the persistent store.
serializer
The serializer attribute on a persistence manager is a ModelSerializer instance to use to (de)serialize instances to/from the persistent store.
Model Validators
Model validators implement the logic of determining whether a Datamodel's current field values are valid. They also handle setting any validation errors on the model and its fields.
Validating models
Models are automatically validated using their ModelValidator (sub)class instance
when they are saved by calling save. You can however also validate them at any
moment you like by directly calling the validate method on either a model instance
or one of its fields. The provided ModelValidator class validates models by first validating each individual
field, after which it calls the model's perform_validate hook for any additional custom validation logic.
The hook is only called if the individual field validation did not yield any errors.
Handling errors
If any validation errors occured, the errors property of the fields containing
errors will be populated. You should access this using the getErrors and getPrimaryError
methods. The hasErrors method on the datamodel will return false if there are any errors.
Errors are instances of ValidationError.
Subclassing ModelValidator
There are only a couple of methods you need to override when providing your own model validator:
validateModel
This is the most important method. It receives the model to validate as only argument
and should validate it in some way. Any errors that occured should be set on the model
and its fields using setError. Your subclass may also choose to respect the perform_validate
hook of Datamodel.
hasErrors
Should simply return whether the given datamodel is regarded invalid, meaning it has validation errors.
getErrors
Should return all validation errors, keyed by field name. Every value should be
an array of ValidationError instances. Any errors not specific to any field should
be keyed under the NON_FIELD_ERRORS key.
setErrors
Should set the given dictionary of errors on the provided model. The dictionary
should be in the same format as returned by getErrors.
clearModelErrors
As the name suggests, should clear all errors on the model itself and all its fields.
Model Serializers
Model serializers implement serializing models to/from some representation of their values. Only their values are serialized, no other attributes. They are used by persistence managers to (de)serialize models to and from their representation as stored in the persistent store.
Serializing models
The provided ModelSerializer class implements serialization by calling the serialize
method on every field that should be serialized. It respects a shouldSerialize
configuration property on fields that can be used to exclude certain fields.
Deserializing models
Deserialization of models is done in a few steps:
- The
deserialize_preprocessmethod on the Datamodel class to be deserialized into is called, passing it the values to be deserialized as well as any context dictionary which was used to fetch the model from its persistent store. This is the optionalcontextparameter of theget,fetchandquerymethods of persistence managers. - A new model instance of the specified class is initialized with the preprocessed
values as initial values. The
newmethod is also passed the context dictionary. - The instantiated model instance's
perform_deserializehook is called for any custom deserialization logic to perform just after deserialization has otherwise completed.
Subclassing ModelSerializer
When subclassing ModelSerializer you only have to implement the serializeModel
and deserialize methods. You can choose to call the hooks provided by datamodel
in these functions but you can also introduce your own in combination with a custom
Datamodel subclass for instance.
Signallers
ModelSignallers are used to emit signals about certain events pertaining datamodels.
They are declared on Datamodel instances under the signaller attribute. The
supported signals are the attributes on the ModelSignaller class.
Subscribing to signals
You can subscribe to signals by calling the add method on the Signal instance for
that signal. See section Model signals.
Custom signallers
You can easily subclass ModelSignaller and provide your own additional signals
if you want. You then have to define an instance of your custom class on your
Datamodel classes.
Subclassing Datamodel
TODO
APIModel
TODO