In LoopBack, working with data is usually the core of business logic. We typically start by defining models to represent business data. Then we attach models to configured data sources to receive behaviors provided by various connectors. For example, connectors for databases implement methods for models to perform create, read, update, and delete (CRUD) operations. Furthermore, you can expose models and their methods as REST APIs so client applications can easily consume them.

Individual models are easy to understand and work with. But in reality, models are often connected or related. The moment you start to build a real-world application with multiple models, relations between models will come into the picture naturally. For example:

  • A customer has many orders and each order is owned by a customer.
  • A user can be assigned to one or more roles and a role can have zero or more users.
  • A physician takes care of many patients through appointments. A patient can see many physicians too.
  • With the connected models, LoopBack can present data to client applications as a connected graph, with connectors to interact with the backend systems behind the scenes. The data graph is then exposed as a set of APIs for the client to not only interact with each of the model instances, but also “slice and dice” the information based on the client’s need.

    The first part of the blog walks through different types of relations, how to define them, and what Node.js APIs are available for navigating and associating the connected models. The second part of the blog describes how to map data navigation and aggregation capabilities to REST APIs.

    Understanding and defining relations

    LoopBack model relations are inspired by Ruby on Rails Active Record Associations. You’ll find a lot of similarity in terms of concepts and how it works. At the moment, LoopBack supports the four types of relations:

  • belongsTo
  • hasMany
  • hasMany through
  • hasAndBelongsToMany
  • Let’s examine the relation types one by one. We’ll explain what a relation is for, how to declare it, and how to use it with the utility methods mixed into the model by LoopBack.

    The belongsTo relation

    A “belongsTo” relation specifies a one-to-one connection between two models: each instance of the declaring model “belongs to” one instance of the related model. For example, in an application with customers and orders, each order “belongs to” one customer, as illustrated in the diagram below.

    The belongsTo relation - Customer Order

    The declaring model (Order) has a foreign key property that references the primary key property of the target model (Customer). If a primary key is not present, LoopBack will automatically add one.

    Defining a belongsTo relation

    A belongsTo relation has three configurable properties:

  • The target model, such as Customer.
  • The relation name, such as ‘customer’. It becomes a method on the declaring model’s prototype.
  • The foreign key property on the declaring model, such as ‘customerId’. It references the target model’s primary key (Customer.id).
  • You can declare relations in the options property in models.json, for example:

    "Order": {
       "properties": {
           "customerId": "number",
           "orderDate": "date"
       },
       "options": {
         "relations": {
           "customer": {
             "type": "belongsTo",
             "model": "Customer",
             "foreignKey": "customerId"
           }
         }
       }
     }
    

    Alternatively, you can define a “belongsTo” relation in code:

    var Order = ds.createModel('Order', {
        customerId: Number,
        orderDate: Date
    });
    var Customer = ds.createModel('Customer', {
        name: String
    });
    Order.belongsTo(Customer);
    

    You can specify the relation name and foreign key as arguments to the belongsTo() method:

    Order.belongsTo(Customer, {as: customer, foreignKey: customerId);
    

    If the  declaring model doesn’t have a foreign key property, LoopBack will add a property with the same name.  The type of the property will be the same as the type of the target model’s id property.

    If you don’t specify them, then LoopBack derives the relation name and foreign key as follows:

  • relation name: The camel case of the model name, for example, ‘Customer’ ⇒ ‘customer’.
  • foreign key: The relation name appended with ‘Id’, for example, ‘customer’ ⇒ ‘customerId’
  • Methods added to the model for belongsTo

    Once you define the belongsTo relation, a method with the relation name is added to the declaring model class’s prototype automatically, for example: Order.prototype.customer(...).

    Depending on the arguments, the method can be used to get or set the owning model instance.

    Function Code snippet
    Get the customer for the order asynchronously order.customer(function(err, customer) {

    });
    Get the customer for the order synchronously var customer = order.customer();
    Set the customer for the order order.customer(customer);

     

    The hasMany relation

    A “hasMany” relation indicates a one-to-many connection between two models. It indicates that each instance of the declaring model has zero or more instances of another model. This relation often occurs in conjunction with a belongsTo relation. For example, in an application with customers and orders, a customer can have many orders, as illustrated in the diagram below.

    The target model Order has a property customerId as the foreign key to reference the declaring model (Customer) primary key id.

    Defining a hasMany relation

    A hasMany relation has three configurable properties:

  • The target model, such as Order
  • The relation name, such as ‘orders’. It becomes a method on the declaring model’s prototype
  • The foreign key property on the target model, such as ‘customerId’. It references the declaring model’s primary key (Customer.id)
  • You can declare relations in the options property in models.json, for example:

    "Customer": {
        "properties": {
            "id": {"type": "number", "id": true, "generated": true},
            "name": "string"
        },
        "options": {
          "relations": {
            "orders": {
              "type": "hasMany",
              "model": "Order",
              "foreignKey": "customerId"
            }
          }
        }
      }
    

    Alternatively, you can define the relation in code:

    var Order = ds.createModel('Order', {
        customerId: Number,
        orderDate: Date
    });
    var Customer = ds.createModel('Customer', {
        name: String
    });
    

    Both names can be provided to the hasMany() method explicitly.

    Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
    

    If not specified, then LoopBack derives the relation name and foreign key as follows:

  • relation name: The plural form of the camel case of the model name, for example, ‘Order’ ⇒ ‘order’ ⇒ ‘orders’.
  • foreign key: The camel case of the declaring model name appended with ‘Id’, for example, ‘Customer’ ⇒ ‘customerId’
  • Methods added to the model for hasMany

    Once you define a hasMany relation, a method with the relation name is added to the declaring model class’s prototype automatically, for example: Customer.prototype.orders(...).

    Function Code snippet
    Find orders for the customer by the filter customer.orders(filter, function(err, orders) {

    });
    Build a new order for the customer with the customerId to be set to the id of the customer. No persistence is involved. var order = customer.orders.build(data);
    It’s the same as:
    var order = new Order({customerId: customer.id, …});
    Create a new order for the customer customer.orders.create(data, function(err, order) {

    });
    It’s the same as:
    Order.create({customerId: customer.id, …}, function(err, order) {

    });
    Remove all orders for the customer customer.orders.destroyAll(function(err) {

    });
    Find an order by id customer.orders.findById(orderId, function(err, order) {

    });
    Delete an order by id customer.orders.destroy(orderId, function(err) {

    });

    The hasManyThrough relation

    A “hasManyThrough” relation creates a many-to-many connection with another model through a third model. This relation indicates that the declaring model matches zero or more instances of the related model through the third model. For example, in an application for a medical practice where patients make appointments to see physicians, the relevant relation declarations could look like this:

     

    The “through” model, Appointment, has two foreign key properties, physicianId and patientId, that reference the primary keys in the declaring model, Physician, and the target model, Patient.

    Defining a hasManyThrough relation

    var Physician = ds.createModel('Physician', {name: String});
    var Patient = ds.createModel('Patient', {name: String});
    var Appointment = ds.createModel('Appointment', {
        physicianId: Number,
        patientId: Number,
        appointmentDate: Date
    });
    Physician.hasMany(Patient, {through: Appointment});
    Patient.hasMany(Physician, {through: Appointment});
    

    Methods added to the model for hasManyThrough

    Function Code snippet
    Find patients for the physician physician.patients(filter, function(err, patients) {

    });
    Build a new patient var patient = physician.patients.build(data);
    Create a new patient for the physician physician.patients.create(data, function(err, patient) {

    });
    Remove all patients for the physician physician.patients.destroyAll(function(err) {

    });
    Add an patient to the physician physician.patients.add(patient, function(err, patient) {

    });
    Remove an patient from the physician physician.patients.remove(patient, function(err) {

    });
    Find an patient by id physician.patients.findById(patientId, function(err, patient) {

    });

    The hasAndBelongsToMany relation

    A hasAndBelongsToMany relation creates a direct many-to-many connection with another model, with no intervening model. For example, in an application with assemblies and parts, where each assembly has many parts and each part appears in many assemblies, you could declare the models this way:

    Defining a hasAndBelongsToMany relation

    var Assembly = ds.createModel('Assembly', {name: String});
    var Part = ds.createModel('Part', {partNumber: String});
    Assembly.hasAndBelongsToMany(Part);
    Part.hasAndBelongsToMany(Assembly);
    

    Methods added to the model for hasAndBelongsToMany

    Function Code snippet
    Find parts for the assembly assembly.parts(filter, function(err, parts) {
    });
    Build a new part var part = assembly.parts.build(data);
    Create a new part for the assembly assembly.parts.create(data, function(err, part) {
    });
    Add a part to the assembly assembly.parts.add(part, function(err) {
    });
    Remove a part from the assembly assembly.parts.remove(part, function(err) {
    });
    Find a part by id assembly.parts.findById(partId, function(err, part) {
    });
    Delete a part by id assembly.parts.destroy(partId, function(err) {
    });

     

    Slicing and dicing the data graph

    Now we have covered the four types of model relations in LoopBack. As you have seen, a relation defines the connection between two models by connecting a foreign key property to a primary key property. For each relation type, a list of helper methods are mixed into the model class to help navigate and associate the model instances to load or build a data graph.

    Often, client applications want to pick and choose the relevant data from the graph using APIs, for example to get user information and recently-placed orders. LoopBack provides a few ways to express such requirements in queries.

    Inclusion

    To include related models in the response for a query, we can use the ‘include’ property of the query object or use the include method on the model class.

    The ‘include’ can be a string, an array, or an object. The valid formats are illustrated below using examples:

  • Load all user posts with only one additional request.
  • User.find({include: 'posts'}, function() {
    ...
    });
    

    It is the same as:

    User.find({include: ['posts']}, function() {
    ...
    });
    
  • Load all user posts and orders with two additional requests.
  • User.find({include: ['posts', 'orders']}, function() {
    ...
    });
    
  • Load all post owners (users), and all orders of each owner
  • Post.find({include: {owner: orders}}, function() {
    ...
    });
    
  • Load all post owners (users), and all friends and orders of each owner
  • Post.find({include: {owner: [freinds, orders]}}, function() {
    ...
    });
    
  • Load all post owners (users), and all posts and orders of each owner. The posts also include images.
  • Post.find({include: {owner: [{posts: images} , orders]}}, function() {
    ...
    });
    

    The ‘include’ method is also available for the model class, for example, the code snippet below will populate the list of user instances with posts.

    User.include(users, 'posts', function() {
    ...
    });
    

    Scope

    Scoping enables you to define a query as a method to the target model class or prototype. For example,

    User.scope(top10Vips, {where: {vip: true}, limit: 10});
    
    User.top10Vips(function(err, vips) {
    });
    

    You can create the same function using a custom method too:

    User.top10Vips = function(cb) {
    User.find({where: {vip: true}, limit: 10}, cb);
    }
    

    Exposing REST APIs for the graph

    Let’s take the following models as an example to demonstrate how the connected models can be accessed via REST APIs.

    var db = loopback.createDataSource({connector: 'memory'});
      Customer = db.createModel('customer', {
        name: String,
        age: Number
      });
      Review = db.createModel('review', {
        product: String,
        star: Number
      });
      Order = db.createModel('order', {
        description: String,
        total: Number
      });
    
      Customer.scope("youngFolks", {where: {age: {lte: 22}}});
      Review.belongsTo(Customer, {foreignKey: 'authorId', as: 'author'});
      Customer.hasMany(Review, {foreignKey: 'authorId', as: 'reviews'});
      Customer.hasMany(Order, {foreignKey: 'customerId', as: 'orders'});
      Order.belongsTo(Customer, {foreignKey: 'customerId'});
    

    The code is available at https://github.com/strongloop-community/loopback-example-datagraph. You can check it out using git and start to play with the REST APIs.

    git clone <a href="mailto:git@github.com">git@github.com</a>:strongloop-community/loopback-example-datagraph.git
    cd loopback-example-datagraph
    npm install
    node app
    

    The sample APIs is now available at http://localhost:3000.  Here are the specific endpoints:

  • List all customers
  • /api/customers

  • List all customers with the name property only
  • /api/customers?filter[fields][0]=name

  • Look up a customer by id
  • /api/customers/1

  • List a predefined scope ‘youngFolks’
  • /api/customers/youngFolks

  • List all reviews posted by a given customer
  • /api/customers/1/reviews

  • List all orders placed by a given customer
  • /api/customers/1/orders

  • List all customers including their reviews
  • /api/customers?filter[include]=reviews

  • List all customers including their reviews which also includes the author
  • /api/customers?filter[include][reviews]=author

  • List all customers whose age is 21, including their reviews which also includes the author
  • /api/customers?filter[include][reviews]=author&filter[where][age]=21

  • List first two customers including their reviews which also includes the author
  • /api/customers?filter[include][reviews]=author&filter[limit]=2

  • List all customers including their reviews and orders
  • /api/customers?filter[include]=reviews&filter[include]=orders

    References

  • LoopBack Model Relations REST API
  • Example: Relations.js
  • Example: Inclusion.js
  • Example: Datagraph
  • What’s next?