LoopBack DataSource Juggler is an ORM that provides a common set of interfaces for interacting with databases, REST APIs, and other data sources. It was initially forked from JugglingDB.
LoopBack DataSource Juggler consists of the following components:
To define model dataSource have single method dataSource.define
. It accepts three
arguments:
tableName
or so.Examples of model definition:
var User = dataSource.define('User', {
email: String,
password: String,
birthDate: Date,
activated: Boolean
});
var User = dataSource.define('User', {
email: { type: String, limit: 150, index: true },
password: { type: String, limit: 50 },
birthDate: Date,
registrationDate: {
type: Date,
default: function () { return new Date }
},
activated: { type: Boolean, default: false }
}, {
tableName: 'users'
});
DataSource is a factory for model classes. DataSource connected with specific database or other backend system using connector.
All model classes within single datasource shares same connector type and one database connection. But it's possible to use more than one datasource to connect with different databases.
DataSource
constructor available on loopback-datasource-juggler
module:
var DataSource = require('loopback-datasource-juggler').DataSource;
DataSource constructor accepts two arguments. First argument is connector. It could be connector name or connector package:
var dataSourceByConnectorName = new DataSource('memory');
var dataSourceByConnectorModule = new DataSource(require('redis'));
Second argument is optional settings. Settings object format and defaults depends on specific connector, but common fields are:
host
: Database hostport
: Database portusername
: Username to connect to databasepassword
: Password to connect to databasedatabase
: Database namedebug
: Turn on verbose mode to debug db queries and lifecycleFor connector-specific settings refer to connector's readme file.
Type | Package Name |
---|---|
MongoDB | loopback-connector-mongodb |
Oracle | loopback-connector-oracle |
MySQL | loopback-connector-mysql |
LoopBack connectors provide access to backend systems including databases, REST APIs and other services. Connectors are not used directly by application code. We create a DataSource to interact with the connector.
For example,
var DataSource = require('loopback-datasource-juggler').DataSource;
var oracleConnector = require('loopback-connector-oracle');
var ds = new DataSource(oracleConnector, {
host : '127.0.0.1',
database : 'XE',
username : 'strongloop',
password : 'password',
debug : true
});
The connector argument passed the DataSource constructor can be one of the following:
require(connectorName)
npm install loopback-datasource-juggler
Also install the appropriated connector, for example for mongodb:
npm install loopback-connector-mongodb
check following list of available connectors
Define a model class
Name | Type | Description |
---|---|---|
className |
String
|
|
properties |
Object
|
hash of class properties in format |
settings |
Object
|
other configuration of class |
more advanced case var User = dataSource.define('User', {
email: { type: String, limit: 150, index: true },
password: { type: String, limit: 50 },
birthDate: Date,
registrationDate: {type: Date, default: function () { return new Date }},
activated: { type: Boolean, default: false }
});
Register a property for the model class
Name | Type | Description |
---|---|---|
propertyName |
|
Define single property named propertyName
on model
Name | Type | Description |
---|---|---|
model |
String
|
name of model |
propertyName |
String
|
name of property |
propertyDefinition |
Object
|
property settings |
Extend existing model with bunch of properties
Name | Type | Description |
---|---|---|
model |
String
|
name of model |
props |
Object
|
hash of properties Example:
|
Get the schema name
Resolve the type string to be a function, for example, 'String' to String
Name | Type | Description |
---|---|---|
type |
String
|
The type string, such as 'number', 'Number', 'boolean', or 'String'. It's case insensitive |
Name | Type | Description |
---|---|---|
result |
Function
|
if the type is resolved |
Build models from dataSource definitions
schemas
can be one of the following:
Name | Type | Description |
---|---|---|
schemas |
|
The schemas |
Name | Type | Description |
---|---|---|
result |
Object
|
A map of model constructors keyed by model name |
Introspect the json document to build a corresponding model
Name | Type | Description |
---|---|---|
name |
String
|
The model name |
json |
Object
|
The json object |
[Object} options The options |
|
Name | Type | Description |
---|---|---|
result |
|
{} |
Export public API
Name | Type | Description |
---|---|---|
models |
Object
|
Model constructors |
Name | Type | Description |
---|---|---|
definitions |
Object
|
Definitions of the models |
Determine the spherical distance between two geo points.
Determine the spherical distance to the given point.
Simple serialization.
Export the GeoPoint
class.
Model class - base class for all persist objects
provides common API to access any database connector.
This class describes only abstract behavior layer, refer to lib/connectors/*.js
to learn more about specific connector implementations
ModelBaseClass
mixes Validatable
and Hookable
classes methods
Name | Type | Description |
---|---|---|
data |
Object
|
initial object data |
Name | Type | Description |
---|---|---|
prop |
String
|
property name |
params |
Object
|
various property configuration |
Return string representation of class
Convert instance to Object
Name | Type | Description |
---|---|---|
onlySchema |
Boolean
|
restrict properties to dataSource only, default false when onlySchema == true, only properties defined in dataSource returned, otherwise all enumerable properties returned |
Name | Type | Description |
---|---|---|
result |
Object
|
|
Checks is property changed based on current property and initial value
Name | Type | Description |
---|---|---|
propertyName |
String
|
property name |
Reset dirty attributes
this method does not perform any database operation it just reset object to it's initial state
Module exports class Model
DataSource - connector-specific classes factory.
All classes in single dataSource shares same connector type and one database connection
Name | Type | Description |
---|---|---|
name |
String
|
type of dataSource connector (mysql, mongoose, oracle, redis) |
settings |
Object
|
any database-specific settings which we need to establish connection (of course it depends on specific connector)
|
DataSource creation, waiting for connection callback var dataSource = new DataSource('mysql', { database: 'myapp_test' });
dataSource.define(...);
dataSource.on('connected', function () {
// work with database
});
Define a model class
Name | Type | Description |
---|---|---|
className |
String
|
|
properties |
Object
|
hash of class properties in format |
settings |
Object
|
other configuration of class |
more advanced case var User = dataSource.define('User', {
email: { type: String, limit: 150, index: true },
password: { type: String, limit: 50 },
birthDate: Date,
registrationDate: {type: Date, default: function () { return new Date }},
activated: { type: Boolean, default: false }
});
Mixin DataAccessObject methods.
Name | Type | Description |
---|---|---|
ModelCtor |
Function
|
The model constructor |
Attach an existing model to a data source.
Name | Type | Description |
---|---|---|
ModelCtor |
Function
|
The model constructor |
Define single property named prop
on model
Name | Type | Description |
---|---|---|
model |
String
|
name of model |
prop |
String
|
name of propery |
params |
Object
|
property settings |
Drop each model table and re-create. This method make sense only for sql connectors.
Name | Type | Description |
---|---|---|
or |
String
|
{[String]} Models to be migrated, if not present, apply to all models |
[cb] |
Function
|
The callback function |
Update existing database tables. This method make sense only for sql connectors.
Name | Type | Description |
---|---|---|
or |
String
|
{[String]} Models to be migrated, if not present, apply to all models |
[cb] |
Function
|
The callback function |
Discover existing database tables. This method returns an array of model objects, including {type, name, onwer}
options
all: true - Discovering all models, false - Discovering the models owned by the current user
views: true - Including views, false - only tables
limit: The page size
offset: The starting index
Name | Type | Description |
---|---|---|
options |
Object
|
The options |
[cb] |
Function
|
The callback function |
The synchronous version of discoverModelDefinitions
Name | Type | Description |
---|---|---|
options |
Object
|
The options |
Name | Type | Description |
---|---|---|
result |
|
Discover properties for a given model.
property description
owner {String} The database owner or schema
tableName {String} The table/view name
columnName {String} The column name
dataType {String} The data type
dataLength {Number} The data length
dataPrecision {Number} The numeric data precision
dataScale {Number} The numeric data scale
nullable {Boolean} If the data can be null
options
owner/schema The database owner/schema
Name | Type | Description |
---|---|---|
modelName |
String
|
The table/view name |
options |
Object
|
The options |
[cb] |
Function
|
The callback function |
The synchronous version of discoverModelProperties
Name | Type | Description |
---|---|---|
modelName |
String
|
The table/view name |
options |
Object
|
The options |
Name | Type | Description |
---|---|---|
result |
|
Discover primary keys for a given owner/modelName
Each primary key column description has the following columns:
owner {String} => table schema (may be null)
tableName {String} => table name
columnName {String} => column name
keySeq {Number} => sequence number within primary key( a value of 1 represents the first column of the primary key, a value of 2 would represent the second column within the primary key).
pkName {String} => primary key name (may be null)
The owner, default to current user
options
owner/schema The database owner/schema
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
[cb] |
Function
|
The callback function |
The synchronous version of discoverPrimaryKeys
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
Name | Type | Description |
---|---|---|
result |
|
Discover foreign keys for a given owner/modelName
foreign key description
fkOwner String => foreign key table schema (may be null)
fkName String => foreign key name (may be null)
fkTableName String => foreign key table name
fkColumnName String => foreign key column name
keySeq Number => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
pkOwner String => primary key table schema being imported (may be null)
pkName String => primary key name (may be null)
pkTableName String => primary key table name being imported
pkColumnName String => primary key column name being imported
options
owner/schema The database owner/schema
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
[cb] |
Function
|
The callback function |
The synchronous version of discoverForeignKeys
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
Name | Type | Description |
---|---|---|
result |
|
Retrieves a description of the foreign key columns that reference the given table's primary key columns (the foreign keys exported by a table). They are ordered by fkTableOwner, fkTableName, and keySeq.
foreign key description
fkOwner {String} => foreign key table schema (may be null)
fkName {String} => foreign key name (may be null)
fkTableName {String} => foreign key table name
fkColumnName {String} => foreign key column name
keySeq {Number} => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
pkOwner {String} => primary key table schema being imported (may be null)
pkName {String} => primary key name (may be null)
pkTableName {String} => primary key table name being imported
pkColumnName {String} => primary key column name being imported
options
owner/schema The database owner/schema
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
[cb] |
Function
|
The callback function |
The synchronous version of discoverExportedForeignKeys
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
options |
Object
|
The options |
Name | Type | Description |
---|---|---|
result |
|
Discover one schema from the given model without following the associations
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
[options] |
Object
|
The options |
[cb] |
Function
|
The callback function |
Discover schema from a given modelName/view
options
{String} owner/schema - The database owner/schema name
{Boolean} associations - If relations (primary key/foreign key) are navigated
{Boolean} all - If all owners are included
{Boolean} views - If views are included
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
[options] |
Object
|
The options |
[cb] |
Function
|
The callback function |
Discover schema from a given table/view synchronously
options
{String} owner/schema - The database owner/schema name
{Boolean} associations - If relations (primary key/foreign key) are navigated
{Boolean} all - If all owners are included
{Boolean} views - If views are included
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
[options] |
Object
|
The options |
Discover and build models from the given owner/modelName
options
{String} owner/schema - The database owner/schema name
{Boolean} associations - If relations (primary key/foreign key) are navigated
{Boolean} all - If all owners are included
{Boolean} views - If views are included
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
[options] |
Object
|
The options |
[cb] |
Function
|
The callback function |
Discover and build models from the given owner/modelName synchronously
options
{String} owner/schema - The database owner/schema name
{Boolean} associations - If relations (primary key/foreign key) are navigated
{Boolean} all - If all owners are included
{Boolean} views - If views are included
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
[options] |
Object
|
The options |
Check whether migrations needed This method make sense only for sql connectors.
Name | Type | Description |
---|---|---|
[models] |
Array.<String>
|
A model name or an array of model names. If not present, apply to all models |
Freeze dataSource. Behavior depends on connector
Return table name for specified modelName
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
Return column name for specified modelName and propertyName
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
propertyName |
|
The property name |
Name | Type | Description |
---|---|---|
result |
String
|
columnName |
Return column metadata for specified modelName and propertyName
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
propertyName |
|
The property name |
Name | Type | Description |
---|---|---|
result |
Object
|
column metadata |
Return column names for specified modelName
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
Name | Type | Description |
---|---|---|
result |
Array.<String>
|
column names |
Find the ID column name
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
Name | Type | Description |
---|---|---|
result |
String
|
columnName for ID |
Find the ID property name
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
Name | Type | Description |
---|---|---|
result |
String
|
property name for ID |
Find the ID property names sorted by the index
Name | Type | Description |
---|---|---|
modelName |
String
|
The model name |
Name | Type | Description |
---|---|---|
result |
Array.<String>
|
property names for IDs |
Define foreign key to another model
Name | Type | Description |
---|---|---|
className |
String
|
The model name that owns the key |
key |
String
|
name of key field |
foreignClassName |
String
|
The foreign model name |
Close database connection
Name | Type | Description |
---|---|---|
[cb] |
Fucntion
|
The callback function |
Enable a data source operation to be remote.
Name | Type | Description |
---|---|---|
operation |
String
|
The operation name |
Disable a data source operation to be remote.
Name | Type | Description |
---|---|---|
operation |
String
|
The operation name |
Get an operation's metadata.
Name | Type | Description |
---|---|---|
operation |
String
|
The operation name |
Get all operations.
Define an operation to the data source
Name | Type | Description |
---|---|---|
name |
String
|
The operation name |
options |
Object
|
The options |
[Function} fn The function |
|
Check if the backend is a relational DB
Name | Type | Description |
---|---|---|
result |
Boolean
|
Check if the data source is ready
Name | Type | Description |
---|---|---|
obj |
|
|
args |
|
Name | Type | Description |
---|---|---|
result |
boolean
|
Define a hidden property
Name | Type | Description |
---|---|---|
obj |
Object
|
The property owner |
key |
String
|
The property name |
value |
Mixed
|
The default value |
Define readonly property on object
Name | Type | Description |
---|---|---|
obj |
Object
|
The property owner |
key |
String
|
The property name |
value |
Mixed
|
The default value |
Export public API
DAO class - base class for all persist objects
provides common API to access any database connector.
This class describes only abstract behavior layer, refer to lib/connectors/*.js
to learn more about specific connector implementations
DataAccessObject
mixes Inclusion
classes methods
Name | Type | Description |
---|---|---|
data |
Object
|
initial object data |
Create new instance of Model class, saved in database
Name | Type | Description |
---|---|---|
data |
|
[optional] |
callback(err, |
|
obj) callback called with arguments:
|
Update or insert a model instance
Name | Type | Description |
---|---|---|
data |
Object
|
The model instance data |
[callback] |
Function
|
The callback function |
Find one record, same as all
, limited by 1 and return object, not collection,
if not found, create using data provided as second argument
Name | Type | Description |
---|---|---|
query |
Object
|
search conditions: {where: {test: 'me'}}. |
data |
Object
|
object to create. |
cb |
Function
|
callback called with (err, instance) |
Check whether a model instance exists in database
Name | Type | Description |
---|---|---|
id |
id
|
identifier of object (primary key value) |
cb |
Function
|
callbacl called with (err, exists: Bool) |
Find object by id
Name | Type | Description |
---|---|---|
id |
|
primary key value |
cb |
Function
|
callback called with (err, instance) |
Find all instances of Model, matched by query
make sure you have marked as index: true
fields for filter or sort
Name | Type | Description |
---|---|---|
params |
Object
|
(optional)
|
callback |
Function
|
(required) called with arguments:
|
Find one record, same as all
, limited by 1 and return object, not collection
Name | Type | Description |
---|---|---|
params |
Object
|
search conditions: {where: {test: 'me'}} |
cb |
Function
|
callback called with (err, instance) |
Destroy all matching records
Name | Type | Description |
---|---|---|
[where] |
Object
|
An object that defines the criteria |
[cb] |
Function
|
callback called with (err) |
Destroy a record by id
Name | Type | Description |
---|---|---|
id |
|
The id value |
cb |
Function
|
callback called with (err) |
Return count of matched records
Name | Type | Description |
---|---|---|
where |
Object
|
search conditions (optional) |
cb |
Function
|
callback, called with (err, count) |
Save instance. When instance haven't id, create method called instead. Triggers: validate, save, update | create
Name | Type | Description |
---|---|---|
options |
|
{validate: true, throws: false} [optional] |
callback(err, |
|
obj) |
Update single attribute
equals to `updateAttributes({name: value}, cb)
Name | Type | Description |
---|---|---|
name |
String
|
name of property |
value |
Mixed
|
value of property |
callback |
Function
|
callback called with (err, instance) |
Update set of attributes
this method performs validation before updating
Name | Type | Description |
---|---|---|
data |
Object
|
data to update |
callback |
Function
|
callback called with (err, instance) |
Reload object from persistence
Name | Type | Description |
---|---|---|
callback |
Function
|
called with (err, instance) arguments |
Define readonly property on object
Name | Type | Description |
---|---|---|
obj |
Object
|
|
key |
String
|
|
value |
Mixed
|
Define scope
Module exports class Model
Delete object from persistence
Hooks mixins
Module exports
List of hooks available
Allows you to load relations of several objects and optimize numbers of requests.
Name | Type | Description |
---|---|---|
objects |
Array
|
array of instances |
{String}, |
|
{Object} or {Array} include - which relations you want to load. |
cb |
Function
|
Callback called when relations are loaded Examples:
|
Include mixin for ./model.js
Declare hasMany relation
Name | Type | Description |
---|---|---|
anotherClass |
Relation
|
class to has many |
params |
Object
|
configuration {as:, foreignKey:} |
User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
Declare belongsTo relation
Name | Type | Description |
---|---|---|
anotherClass |
Class
|
class to belong |
params |
Object
|
configuration {as: 'propertyName', foreignKey: 'keyName'} Usage examples Suppose model Post have a belongsTo relationship with User (the author of the post). You could declare it this way: Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); When a post is loaded, you can load the related author with: post.author(function(err, user) { // the user variable is your user object }); The related object is cached, so if later you try to get again the author, no additional request will be made. But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: post.author(true, function(err, user) { // The user is reloaded, even if it was already cached. }); This optional parameter default value is false, so the related object will be loaded from cache if available. |
Many-to-many relation
Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag'
Presence validator
Length validator
Numericality validator
Inclusion validator
Exclusion validator
Format validator
Custom validator
Uniqueness validator
This method performs validation, triggers validation hooks.
Before validation obj.errors
collection cleaned.
Each validation can add errors to obj.errors
collection.
If collection is not blank, validation failed.
Name | Type | Description |
---|---|---|
callback |
Function
|
called with (valid) |
ExpressJS controller: render user if valid, show flash otherwise user.isValid(function (valid) {
if (valid) res.render({user: user});
else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
});
Return true when v is undefined, blank array, null or empty string otherwise returns false
Name | Type | Description |
---|---|---|
v |
Mix
|
Name | Type | Description |
---|---|---|
result |
Boolean
|
whether |
Module exports
Validation mixins for model.js
Basically validation configurators is just class methods, which adds validations
configs to AbstractClass._validations. Each of this validations run when
obj.isValid()
method called.
Each configurator can accept n params (n-1 field names and one config). Config
is {Object} depends on specific validation, but all of them has one common part:
message
member. It can be just string, when only one situation possible,
e.g. Post.validatesPresenceOf('title', { message: 'can not be blank' });
In more complicated cases it can be {Hash} of messages (for each case):
User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});
Validate presence. This validation fails when validated field is blank.
Default error message "can't be blank"
with custom message Post.validatesPresenceOf('title', {message: 'Can not be blank'});
Validate length. Three kinds of validations: min, max, is.
Default error messages:
length validations with custom error messages User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}});
User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}});
Validate numericality.
User.validatesNumericalityOf('age', { message: { number: '...' }});
User.validatesNumericalityOf('age', {int: true, message: { int: '...' }});
Default error messages:
Validate inclusion in set
User.validatesInclusionOf('gender', {in: ['male', 'female']});
User.validatesInclusionOf('role', {
in: ['admin', 'moderator', 'user'], message: 'is not allowed'
});
Default error message: is not included in the list
Validate exclusion
Company.validatesExclusionOf('domain', {in: ['www', 'admin']});
Default error message: is reserved
Validate format
Default error message: is invalid
Validate using custom validator
Default error message: is invalid
Example:
User.validate('name', customValidator, {message: 'Bad name'});
function customValidator(err) {
if (this.name === 'bad') err();
});
var user = new User({name: 'Peter'});
user.isValid(); // true
user.name = 'bad';
user.isValid(); // false
Validate using custom async validator
Default error message: is invalid
Example:
User.validateAsync('name', customValidator, {message: 'Bad name'});
function customValidator(err, done) {
process.nextTick(function () {
if (this.name === 'bad') err();
done();
});
});
var user = new User({name: 'Peter'});
user.isValid(); // false (because async validation setup)
user.isValid(function (isValid) {
isValid; // true
})
user.name = 'bad';
user.isValid(); // false
user.isValid(function (isValid) {
isValid; // false
})
Validate uniqueness
Default error message: is not unique
LoopBack Definition Language (LDL) is simple DSL to define data models in JavaScript or plain JSON. With LoopBack, we often start with a model definition which describes the structure and types of data. The model establishes common knowledge of data in LoopBack.
Let's start with a simple example in plain JSON.
{
"id": "number",
"firstName": "string",
"lastName": "string"
}
The model simply defines a user
model that consists of three properties:
Each key in the JSON object defines a property in our model which will be cast
to its associated type. The simplest form of a property definition is
propertyName: type
. The key is the name of the property and the value is the
type of the property. We'll cover more advanced form later in this guide.
LDL supports a list of built-in types, including the basic types from JSON:
Note: The type name is case-insensitive, i.e., either "Number" or "number" can be used.
The same model can also be described in JavaScript code:
var UserDefinition = {
id: Number,
firstName: String,
lastName: String
}
As we can see, the JavaScript version is less verbose as it doesn't require
quotes for property names. The types are described using JavaScript constructors,
for example, Number
for "Number"
. String literals are also supported.
Now we have the definition of a model, how do we use it in LoopBack Node.js code? It's easy, LoopBack will build a JavaScript constructor (or class) for you.
LDL compiles the model definition into a JavaScript constructor using
ModelBuilder.define
APIs. ModelBuilder is the basic factory to create model
constructors.
ModelBuilder.define() method takes the following arguments:
Here is an example,
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
// Create an instance of the ModelBuilder
var modelBuilder = new ModelBuilder();
// Describe the user model
var UserDefinition = {
id: Number,
firstName: String,
lastName: String
}
// Compile the user model definition into a JavaScript constructor
var User = modelBuilder.define('User', UserDefinition);
// Create a new instance of User
var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
console.log(user.id); // 1
console.log(user.firstName); // 'John'
console.log(user.lastName); // 'Smith'
That's it. Now you have a User constructor representing the user model.
At this point, the constructor only has a set of accessors to model properties. No behaviors have been introduced yet.
Models describe the shape of data. To leverage the data, we'll add logic to the model for various purposes, such as:
There are a few ways to add methods to a model constructor:
A LoopBack data source injects methods on the model.
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
// Compile the user model definition into a JavaScript constructor
var User = ds.define('User', UserDefinition);
// Create a new instance of User
User.create({id: 1, firstName: 'John', lastName: 'Smith'}, function(err, user) {
console.log(user); // The newly created user instance
User.findById(1, function(err, user) {
console.log(user); // The user instance for id 1
user.firstName = 'John1'; // Change the property
user.save(function(err, user) {
console.log(user); // The modified user instance for id 1
});
};
});
A plain model constructor created from ModelBuilder
can be attached a DataSource
.
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
User.attachTo(ds); // The CRUD methods will be mixed into the User constructor
Static methods can be added by declaring a function as a member of the model constructor. Within a class method, other class methods can be called using the model as usual.
// Define a static method
User.findByLastName = function(lastName, cb) {
User.find({where: {lastName: lastName}, cb);
};
User.findByLastName('Smith', function(err, users) {
console.log(users); // Print an array of user instances
});
Instance methods can be added to the prototype. Within instance methods, the model instance itself can be referenced with this keyword.
// Define a prototype method
User.prototype.getFullName = function () {
return this.firstName + ' ' + this.lastName;
};
var user = new User({id: 1, firstName: 'John', lastName: 'Smith'});
console.log(user.getFullName()); // 'John Smith'
As we mentioned before, a complete model definition is an object with three properties:
There are a set of options to control the model definition.
id
property will be added to the model automaticallyid
property will be added to the modelData source specific mappings The model can be decorated with connector-specific options to customize the mapping between the model and the connector. For example, we can define the corresponding schema/table names for Oracle as follows:
{
"name": "Location",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "LOCATION"
}
},
...
}
A model consists of a list of properties. The basic example use
propertyName: type
to describe a property.
Properties can have options in addition to the type. LDL uses a JSON object to describe such properties, for example:
"id": {"type": "number", "id": true, "doc": "User ID"}
"firstName": {"type": "string", "required": true, "oracle": {"column": "FIRST_NAME", "type": "VARCHAR(32)"}}
Note "id": "number"
is a short form of "id": {"type": "number"}
.
LDL supports the following data types.
LDL supports array types as follows:
{emails: [String]}
{"emails": ["String"]}
{emails: [{type: String, length: 64}]}
A model often has properties that consist of other properties. For example, the
user model can have an address
property
that in turn has properties such as street
, city
, state
, and zipCode
.
LDL allows inline declaration of such properties, for example,
var UserModel = {
firstName: String,
lastName: String,
address: {
street: String,
city: String,
state: String,
zipCode: String
},
...
}
The value of the address is the definition of the address
type, which can be
also considered as an anonymous model.
If you intend to reuse the address model, we can define it independently and reference it in the user model. For example,
var AddressModel = {
street: String,
city: String,
state: String,
zipCode: String
};
var Address = ds.define('Address', AddressModel);
var UserModel = {
firstName: String,
lastName: String,
address: 'Address', // or address: Address
...
}
var User = ds.define('User', UserModel);
Note: The user model has to reference the Address constructor or the model
name - 'Address'
.
A model representing data to be persisted in a database usually has one or more
properties as an id to uniquely identify the model instance. For example, the
user
model can have user ids.
By default, if no id properties are defined and the idInjection
of the model
options is false, LDL will automatically add an id property to the model as follows:
id: {type: Number, generated: true, id: true}
To explicitly specify a property as id
, LDL provides an id
property for the
option. The value can be true, false, or a number.
LDL supports the definition of a composite id that has more than one properties. For example,
var InventoryDefinition =
{
productId: {type: String, id: 1},
locationId: {type: String, id: 2},
qty: Number
}
The composite id is (productId, locationId) for an inventory model.
Note: Composite ids are NOT supported as query parameters in REST APIs yet.
Constraints are modeled as options too, for example:
Format conversions can also be declared as options, for example:
Data source specific mappings can be added to the property options, for example, to map a property to be a column in Oracle database table, you can use the following syntax:
"oracle": {"column": "FIRST_NAME", "type": "VARCHAR", "length": 32}
A hasMany
relation builds a one-to-many connection with another model. You'll
often find this relation on the "other side" of a belongsTo
relation. This
relation indicates that each instance of the model has zero or more instances
of another model. For example, in an application containing users and posts, a
user has zero or more posts. For example,
// setup relationships
User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
// creates instance methods:
// user.posts(conds)
// user.posts.build(data) // like new Post({userId: user.id});
// user.posts.create(data) // build and save
Define all necessary stuff for one to many
relation:
many
modelone
modelExample:
var Book = db.define('Book');
var Chapter = db.define('Chapters');
// Style 1
Book.hasMany(Chapter, {as: 'chapters'});
// Style 2
Book.hasMany('chapters', {model: Chapter, foreignKey: 'chapter_id'});
Scope methods created on the base model by hasMany allows to build, create and query instances of other class. For example,
Book.create(function(err, book) {
// using 'chapters' scope for build:
var c = book.chapters.build({name: 'Chapter 1'});
// same as:
c = new Chapter({name: 'Chapter 1', bookId: book.id});
// using 'chapters' scope for create:
book.chapters.create();
// same as:
Chapter.create({bookId: book.id});
// using scope for querying:
book.chapters(function() {/* all chapters with bookId = book.id */ });
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
});
A belongsTo
relation sets up a one-to-one connection with another model, such
that each instance of the declaring model "belongs to" one instance of the other
model. For example, if your application includes users and posts, and each post
can be written by exactly one user.
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
The code above basically says Post has a reference called author
to User using
the userId
property of Post as the foreign key. Now we can access the author
in one of the following styles:
post.author(callback); // Get the User object for the post author asynchronously
post.author(); // Get the User object for the post author synchronously
post.author(user) // Set the author to be the given user
A hasAndBelongsToMany
relation creates a direct many-to-many connection with
another model, with no intervening model. For example, if your application
includes users and groups, with each group having many users and each user
appearing in many groups, you could declare the models this way,
User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
user.groups(callback); // get groups of the user
user.groups.create(data, callback); // create a new group and connect it with the user
user.groups.add(group, callback); // connect an existing group with the user
user.groups.remove(group, callback); // remove the user from the group
LDL allows a new model to extend from an existing model. For example, Customer can extend from User as follows. The Customer model will inherit properties and methods from the User model.
var Customer = User.extend('customer', {
accountId: String,
vip: Boolean
});
Some models share the common set of properties and logic around. LDL allows a model to mix in one or more other models. For example,
var TimeStamp = modelBuilder.define('TimeStamp', {created: Date, modified: Date});
var Group = modelBuilder.define('Group', {groups: [String]});
User.mixin(Group, TimeStamp);
LoopBack is centered around models, which represent data and behaviors. The
concept of DataSource
is introduced to encapsulate business logic to exchange
data between models and various data sources. Data sources are typically
databases that provide create, retrieve, update, and delete (CRUD) functions.
LoopBack also generalize other backend services, such as REST APIs, SOAP Web
Services, and Storage Services, as data sources.
Data sources are backed by connectors which implement the data exchange logic
using database drivers or other client APIs. In general, connectors are not used
directly by application code. The DataSource
class provides APIs to configure
the underlying connector and exposes functions via DataSource
or model classes.
The diagram above illustrates the relationship between LoopBack Model
,
DataSource
, and Connector
.
Define the Model using LoopBack Definition Language (LDL). Now we have a model definition in plain JSON or JavaScript object.
Create an instance of ModelBuilder or DataSource. Please note that DataSource extends from ModelBuilder. ModelBuilder is responsible for compiling model definitions to JavaScript constructors representing model classes. DataSource inherits that function from ModelBuilder. In addition, DataSource adds behaviors to model classes by mixing in methods from the DataAccessObject into the model class.
Use ModelBuilder or DataSource to build a JavaScript constructor (i.e, the model class) from the model definition. Model classes built from ModelBuilder can be later attached to a DataSource to receive the mixin of data access functions.
As part of step 2, DataSource initializes the underlying Connector with a settings object which provides configurations to the connector instance. Connector collaborates with DataSource to define the functions as DataAccessObject to be mixed into the model class. The DataAccessObject consists of a list of static and prototype methods. It can be CRUD operations or other specific functions depending on the connector's capabilities.
DataSource is the unified interface for LoopBack applications to integrate with backend systems. It's a factory for data access logic around model classes. With the ability to plug in various connectors, DataSource provides the necessary abstraction to interact with databases or services to decouple the business logic from plumbing technologies.
The DataSource
constructor is available from loopback-datasource-juggler
module:
var DataSource = require('loopback-datasource-juggler').DataSource;
DataSource
constructor accepts two arguments:
connector: The name or instance of the connector module
settings: An object of properties to configure the connector
var dataSource = new DataSource({
connector: require('loopback-connector-mongodb'),
host: 'localhost',
port: 27017,
database: 'mydb'
});
The connector
argument passed the DataSource constructor can be one of the following:
require(connectorName)
var ds1 = new DataSource('memory');
var ds2 = new DataSource('loopback-connector-mongodb'));
var ds3 = new DataSource(require('loopback-connector-oracle'));
Note: LoopBack provides a built-in connector named as memory
to use in-memory
store for CRUD operations.
The settings
argument configures the connector. Settings object format and defaults
depends on specific connector, but common fields are:
host
: Database hostport
: Database portusername
: Username to connect to databasepassword
: Password to connect to databasedatabase
: Database namedebug
: Turn on verbose mode to debug db queries and lifecycleFor connector-specific settings refer to connector's readme file.
DataSource
extends from ModelBuilder
, which is a factory for plain model
classes that only have properties. DataSource
connected with specific databases
or other backend systems using Connector
.
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
var User = ds.define('User', {
name: String,
bio: String,
approved: Boolean,
joinedAt: Date,
age: Number
});
All model classes within single data source shares same connector type and one database connection or connection pool. But it's possible to use more than one data source to connect with different databases.
Alternatively, a plain model constructor created from ModelBuilder
can be
attached a DataSource
.
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
var builder = new ModelBuilder();
var User = builder.define('User', {
name: String,
bio: String,
approved: Boolean,
joinedAt: Date,
age: Number
});
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('memory');
User.attachTo(ds); // The CRUD methods will be mixed into the User constructor
In addition to data access functions mixed into the model class, DataSource
also provides APIs to interact with the underlying backend system.
Some connectors provide discovery capability so that we can use DataSource to discover model definitions from existing database schema.
The following APIs allow UI or code to discover database schema definitions that can be used to build LoopBack models.
// List database tables and/or views
ds.discoverModelDefinitions({views: true, limit: 20}, cb);
// List database columns for a given table/view
ds.discoverModelProperties('PRODUCT', cb);
ds.discoverModelProperties('INVENTORY_VIEW', {owner: 'STRONGLOOP'}, cb);
// List primary keys for a given table
ds.discoverPrimaryKeys('INVENTORY', cb);
// List foreign keys for a given table
ds.discoverForeignKeys('INVENTORY', cb);
// List foreign keys that reference the primary key of the given table
ds.discoverExportedForeignKeys('PRODUCT', cb);
// Create a model definition by discovering the given table
ds.discoverSchema(table, {owner: 'STRONGLOOP'}, cb);
You can also discover and build model classes in one shot:
// Start with INVENTORY table and follow the primary/foreign relationships to discover associated tables
ds.discoverAndBuildModels('INVENTORY', {visited: {}, associations: true}, function (err, models) {
// Now we have an object of models keyed by the model name
// Find the 1st record for Inventory
models.Inventory.findOne({}, function (err, inv) {
if(err) {
console.error(err);
return;
}
console.log("\nInventory: ", inv);
// Follow the product relation to get information about the product
inv.product(function (err, prod) {
console.log("\nProduct: ", prod);
console.log("\n ------------- ");
});
});
});
In addition to the asynchronous APIs, DataSource
also provides the synchronous
ones. Please refer to the DataSource API references.
DataSource instance have two methods for updating db structure: automigrate
and
autoupdate
for relational databases.
The automigrate
method drop table (if exists) and create it again, autoupdate
method generates ALTER TABLE query. Both method accepts an optional array of
model names and a callback function to be called when migration/update done. If
the models
argument is not present, all models are checked.
In the following example, we create first version of the CustomerTest model, use
automigrate
to create the database table, redefine the model with second
version, and use autoupdate
to alter the database table.
// Create the 1st version of 'CustomerTest'
ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options);
// Create DB table for the model
ds.automigrate(schema_v1.name, function () {
// Discover the model properties from DB table
ds.discoverModelProperties('CUSTOMER_TEST', function (err, props) {
console.log(props);
// Redefine the 2nd version of 'CustomerTest'
ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options);
// Alter DB table
ds.autoupdate(schema_v2.name, function (err, result) {
ds.discoverModelProperties('CUSTOMER_TEST', function (err, props) {
console.log(props);
});
});
});
});
To check if any db changes required use isActual
method. It accepts
and a callback
argument, which receive boolean value depending on db state:
dataSource.isActual(models, function(err, actual) {
if (!actual) {
dataSource.autoupdate(models, function(err, result) {
...
});
}
});
Connectors implement the logic to integrate with specific backend systems, such as databases or REST services.
Type | Package Name |
---|---|
Memory | Built-in |
MongoDB | loopback-connector-mongodb |
Oracle | loopback-connector-oracle |
REST | loopback-connector-rest |
MySQL | loopback-connector-mysql |
The connector module can export an initialize
function to be called by the
owning DataSource instance.
exports.initialize = function (dataSource, postInit) {
var settings = dataSource.settings || {}; // The settings is passed in from the dataSource
var connector = new MyConnector(settings); // Construct the connector instance
dataSource.connector = connector; // Attach connector to dataSource
connector.dataSource = dataSource; // Hold a reference to dataSource
...
};
The DataSource calls the initialize
method with itself and an optional postInit
callback function. The connector receives the settings from the dataSource
argument and use it to configure connections to backend systems.
Please note connector and dataSource set up a reference to each other.
Upon initialization, the connector might connect to database automatically.
Once connection established dataSource object emit 'connected' event, and set
connected
flag to true, but it is not necessary to wait for 'connected' event
because all queries cached and executed when dataSource emit 'connected' event.
To disconnect from database server call dataSource.disconnect
method. This
call is forwarded to the connector if the connector have ability to connect/disconnect.
The connector instance can have an optional property named as DataAccessObject that provides static and prototype methods to be mixed into the model constructor. DataSource has a built-in DataAccessObject to support CRUD operations. The connector can choose to use the CRUD DataAccessObject or define its own.
When a method is invoked from the model class or instance, it's delegated to the DataAccessObject which is backed by the connector.
For example,
User.create() --> dataSource.connector.create() --> Oracle.prototype.create()
LoopBack connectors provide access to backend systems including databases, REST APIs and other services. Connectors are not used directly by application code. We create a DataSource to interact with the connector.
For example,
var DataSource = require('loopback-datasource-juggler').DataSource;
var oracleConnector = require('loopback-connector-oracle');
var ds = new DataSource(oracleConnector, {
host : 'localhost',
database : 'XE',
username : 'username',
password : 'password',
debug : true
});
A connector module can implement the following methods to interact with the data source.
exports.initialize = function (dataSource, postInit) {
var settings = dataSource.settings || {}; // The settings is passed in from the dataSource
var connector = new MyConnector(settings); // Construct the connector instance
dataSource.connector = connector; // Attach connector to dataSource
connector.dataSource = dataSource; // Hold a reference to dataSource
/**
* Connector instance can have an optional property named as DataAccessObject that provides
* static and prototype methods to be mixed into the model constructor. The property can be defined
* on the prototype.
*/
connector.DataAccessObject = function {};
/**
* Connector instance can have an optional function to be called to handle data model definitions.
* The function can be defined on the prototype too.
* @param model The name of the model
* @param properties An object for property definitions keyed by propery names
* @param settings An object for the model settings
*/
connector.define = function(model, properties, settings) {
...
};
connector.connect(..., postInit); // Run some async code for initialization
// process.nextTick(postInit);
}
Another way is to directly export the connection function which takes a settings object.
module.exports = function(settings) {
...
}
To support CRUD operations for a model class that is attached to the dataSource/connector, the connector needs to provide the following functions:
/**
* Create a new model instance
*/
CRUDConnector.prototype.create = function (model, data, callback) {
};
/**
* Save a model instance
*/
CRUDConnector.prototype.save = function (model, data, callback) {
};
/**
* Check if a model instance exists by id
*/
CRUDConnector.prototype.exists = function (model, id, callback) {
};
/**
* Find a model instance by id
*/
CRUDConnector.prototype.find = function find(model, id, callback) {
};
/**
* Update a model instance or create a new model instance if it doesn't exist
*/
CRUDConnector.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
};
/**
* Delete a model instance by id
*/
CRUDConnector.prototype.destroy = function destroy(model, id, callback) {
};
/**
* Query model instances by the filter
*/
CRUDConnector.prototype.all = function all(model, filter, callback) {
};
/**
* Delete all model instances
*/
CRUDConnector.prototype.destroyAll = function destroyAll(model, callback) {
};
/**
* Count the model instances by the where criteria
*/
CRUDConnector.prototype.count = function count(model, callback, where) {
};
/**
* Update the attributes for a model instance by id
*/
CRUDConnector.prototype.updateAttributes = function updateAttrs(model, id, data, callback) {
};