diff --git a/docs/datasource-connector.md b/docs/datasource-connector.md index 29f7c6df..5dfec6de 100644 --- a/docs/datasource-connector.md +++ b/docs/datasource-connector.md @@ -1,19 +1,44 @@ -## LoopBack DataSource +# LoopBack DataSource and Connector +## Overview 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 sources. Data sources are typically databases +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. Connectors are not used directly by -application code. The `DataSource` class provides APIs to configure a connector and exposes functions via `DataSource` -or model classes. +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. ![model-datasource-connector](datasource-connector.png "LoopBack Model, DataSource, and Connector") -### Creating dataSource +The diagram above illustrates the relationship between LoopBack `Model`, `DataSource`, and `Connector`. -The `DataSource` constructor available from `loopback-datasource-juggler` module: +1. Define the `Model` using [LoopBack Definition Language (LDL)](definition-language.md). Now we have a model definition in +plain JSON or JavaScript object. + +2. 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. + +3. 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. + +4. 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. + +## LoopBack DataSource + +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. + +### Creating a DataSource + +The `DataSource` constructor is available from `loopback-datasource-juggler` module: var DataSource = require('loopback-datasource-juggler').DataSource; @@ -28,24 +53,24 @@ The `DataSource` constructor available from `loopback-datasource-juggler` module database: 'mydb' }); -#### Connector +#### connector -The connector argument passed the DataSource constructor can be one of the following: +The `connector` argument passed the DataSource constructor can be one of the following: * The connector module from `require(connectorName)` * The full name of the connector module, such as 'loopback-connector-oracle' * The short name of the connector module, such as 'oracle', which will be converted to 'loopback-connector-' * A local module under ./connectors/ folder - 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. -#### Settings +#### settings -The settings argument configures the connector. Settings object format and defaults +The `settings` argument configures the connector. Settings object format and defaults depends on specific connector, but common fields are: * `host`: Database host @@ -57,14 +82,14 @@ depends on specific connector, but common fields are: For connector-specific settings refer to connector's readme file. -### Using DataSource to create models +## Creating a Model `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`. -DataSource is a factory for model classes. DataSource connected with specific database or other -backend system using connector. - + var DataSource = require('loopback-datasource-juggler').DataSource; var ds = new DataSource('memory'); + var User = ds.define('User', { name: String, bio: String, @@ -79,28 +104,80 @@ 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 -What behaviors does data source add to the model? The methods are contributed by the underlying connector -through the data access object. -### Injecting behaviors to the model +## More DataSource Features +In addition to data access functions mixed into the model class, `DataSource` also provides APIs to interact with the +underlying backend system. -### Connecting to database +### Discovering model definitions from the data source -DataSource connecting 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. +Some connectors provide discovery capability so that we can use DataSource to discover model definitions from existing +database schema. -To disconnect from database server call `dataSource.disconnect` method. This call -forwarded to connector if connector have ability to connect/disconnect. +The following APIs allow UI or code to discover database schema definitions that can be used to build LoopBack models. -### Discovering model definitions from the database + // List database tables and/or views + ds.discoverModelDefinitions({views: true, limit: 20}, cb); -### Synchronizing model definitions and database tables + // 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. + +### Synchronizing model definitions against the data source DataSource instance have two methods for updating db structure: `automigrate` and `autoupdate` for relational databases. @@ -109,6 +186,32 @@ The `automigrate` method drop table (if exists) and create it again, `autoupdate 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: @@ -124,15 +227,63 @@ and a `callback` argument, which receive boolean value depending on db state: } }); -## LoopBack Connectors +## LoopBack Connector + +Connectors implement the logic to integrate with specific backend systems, such as databases or REST services. + +### LoopBack Connector Modules | Type | Package Name | | --------- |:--------------------------------------------------------------------------------------:| +| Memory | [Built-in](https://github.com/strongloop/loopback-datasource-juggler) | | MongoDB | [loopback-connector-mongodb](https://github.com/strongloop/loopback-connector-mongodb) | | Oracle | [loopback-connector-oracle](https://github.com/strongloop/loopback-connector-oracle) | +| REST | [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest) | | MySQL | [loopback-connector-mysql](https://github.com/strongloop/loopback-connector-mysql) | -## Build your own connector + +### Interaction between DataSource and Connector + +#### Initializing connector + +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 forwarded to connector if connector +have ability to connect/disconnect. + +#### Accessing data/services + +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() + +## Building your own connectors LoopBack connectors provide access to backend systems including databases, REST APIs and other services. Connectors are not used directly by application code. We create @@ -146,13 +297,13 @@ For example, var ds = new DataSource(oracleConnector, { host : 'localhost', database : 'XE', - username : 'strongloop', - password : 'strongloop', + username : 'username', + password : 'password', debug : true }); -## Generic connector implementations +### Implementing a generic connector A connector module can implement the following methods to interact with the data source. @@ -192,7 +343,7 @@ Another way is to directly export the connection function which takes a settings ... } -## CRUD connector implementations +### Implementing a CRUD connector To support CRUD operations for a model class that is attached to the dataSource/connector, the connector needs to provide the following functions: diff --git a/docs/definition-language.md b/docs/definition-language.md index 103dba0e..7e3b034e 100644 --- a/docs/definition-language.md +++ b/docs/definition-language.md @@ -98,7 +98,7 @@ Models describe the shape of data. To leverage the data, we'll add logic to the There are a few ways to add methods to a model constructor: -### 1) Create the model constructor from a data source +### Create the model constructor from a data source A LoopBack data source injects methods on the model. @@ -121,7 +121,7 @@ A LoopBack data source injects methods on the model. }); -### 2) Attach the model to a data source +### Attach the model to a data source A plain model constructor created from `ModelBuilder` can be attached a `DataSource`. @@ -130,7 +130,7 @@ A plain model constructor created from `ModelBuilder` can be attached a `DataSou User.attachTo(ds); // The CRUD methods will be mixed into the User constructor -### 3) Manually add methods to the model constructor +### Manually add methods to the model 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. @@ -319,30 +319,6 @@ Oracle database table, you can use the following syntax: "oracle": {"column": "FIRST_NAME", "type": "VARCHAR", "length": 32} -#### Advanced example - - var User = modelBuilder.define('User', { - name: String, - bio: ModelBuilder.Text, - approved: Boolean, - joinedAt: Date, - age: Number, - address: { - street: String, - city: String, - state: String, - zipCode: String, - country: String - }, - emails: [{ - label: String, - email: String - }], - friends: [String] - }); - - - ### Relations between models #### hasMany @@ -425,12 +401,18 @@ and each user appearing in many groups, you could declare the models this way, ### Extend from a base model +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 }); ### Mix in model definitions +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); \ No newline at end of file + User.mixin(Group, TimeStamp); \ No newline at end of file