Contents


The busy JavaScript developer's guide to Sails.js, Part 3

Modeling relationships in Sails

Use associations to connect model objects

Comments

Content series:

This content is part # of 4 in the series: The busy JavaScript developer's guide to Sails.js, Part 3

Stay tuned for additional content in this series.

This content is part of the series:The busy JavaScript developer's guide to Sails.js, Part 3

Stay tuned for additional content in this series.

Arrr, sailors! (Some of you are surely code pirates, and so I send you a hearty pirate's welcome.) In the previous article in this series, you built some basic models for the HTTP API example application, then let Sails blueprints automatically configure the app's CRUD operations. In a real-world application, you would need more than a few simple models to manage all your operations, however, especially because most data connects to other data. The systems that you build contain dozens of data relationships, and all of them need to be reflected in your models.

In this installment, you learn how to define models that reflect the relationships between data. Specifically, you:

  • Refactor the BlogEntry model so that it can manage not just blog entries but a wider variety of content types.
  • Explore the possibilities for modeling authors and authorship using two different types of data models in Sails.
  • Add comments and tags to entries in your rapidly expanding content management system.

Before I get started, let me quickly recap what I've covered so far.

Modeling in Sails

The example application that you've been working on is an HTTP API. So far, we've envisioned it as a blogging platform, which is a type of content management system (CMS). The HTTP API defines the CMS back end, in this case, powering a blog whose UI is left undefined. You've built a RESTful API that stores blog entries, comments, and RSS feeds. The API supports and responds to queries (for example, to sort and find particular blogs or entries).

In the previous article, you defined your first model, BlogEntry, which resides in the api/models/BlogEntry.js file in the project directory. Listing 1 shows you BlogEntry.

Listing 1. BlogEntry.js
module.exports = {
 
  attributes: {
    title: {
      type: 'string',
      required: true,
      defaultsTo: ''
    },
    body: {
      type: 'string',
      required: true,
      defaultsTo: ''
    }
  }
 
};

Refactoring a Sails model

What if you now decide that you want to extend the blog API to be more of a general-purpose CMS? You're early enough in the development process that it's still manageable, and the end result would be so much more flexible than just a blogging platform. It doesn't hurt that refactoring in Sails.js is relatively easy.

You start by changing the model's name from BlogEntry to something broader. (Incidentally, I've heard it said that the three most difficult things in programming are naming things and off-by-one errors. Bada-bing!) Because the model's type is obtained from its file name, renaming it from BlogEntry.js to Entry.js instructs Sails to update the model type. Do the same for the corresponding api/controllers/BlogEntryController.js file, changing it to EntryController.js, and that's it: You've refactored your model. Your HTTP API is now potentially useable for more than just weblogs.

Recall, however, that you're using sails-disk as your dev database adapter. sails-disk is a serialization format that gets stored to disk directly; thus, it has no tables, columns, or any other database-like infrastructure. Such simplicity makes sails-disk easy to use during development, but you need to replace it with something else when your code is closer to production. You might wonder how this seemingly easy refactoring will fare when the application is ready to go live.

Fortunately, each model object in Sails can hold a number of model properties. Setting the model properties enables Sails to match a model to the underlying database. You can learn about model settings from the Sails documentation. For now, all you care about is tableName. If you used this property against a real database, it would look something like this:

Listing 2. Entry.js
module.exports = {
 
  tableName: "blogentry",
    // this would map to a relational table by this name,
    // or a MongoDB collection, and so on
 
  attributes: {
    id: {
      type: 'integer',
      primaryKey: true
    },
    title: {
      type: 'string',
      required: true,
      defaultsTo: ''
    },
    body: {
      type: 'string',
      required: true,
      defaultsTo: ''
    }
  }
 
};

Here, you see Entry.js naming the table it is bound against, in this case "blogentry." Should particular fields in the model object need to correspond to named columns in the underlying database, you could annotate each field with a columnName property, naming the column in the table (or the field in the collection, depending on the type of datastore) that it should map to. Listing 3 shows an example.

Listing 3. Mapping fields to columns
module.exports = {
 
  tableName: "blogentry",
    // this would map to a relational table by this name,
    // or a MongoDB collection, and so on
 
  attributes: {
    id: {
      type: 'integer',
      primaryKey: true,
      columnName: 'blogentry_pk'
    },
    title: {
      type: 'string',
      required: true,
      defaultsTo: '',
      columnName: 'blogtitle'
    },
    body: {
      type: 'string',
      required: true,
      defaultsTo: ''
    }
  }
 
};

You need to make a few additional changes when we add more entity types to the system, but for now, this will suffice.

Associations in Sails

Most kinds of published content have one or more authors to store and display, so you need a model for that. Listing 4 shows a first attempt.

Listing 4. Representing an author in the CMS
module.exports = {
  autoPK: false,
  attributes: {
    fullName: {
      type: 'string',
      required: true
    },
    bio: {
      type: 'string'
    },
    username: {
      type: 'string',
      unique: true,
      required: true
    },
    email: {
      type: 'email',
      required: true
    }
  }
};

Author.js is a straightforward data type so far, and it represents the attributes of an author decently: full name, short biography, username, and so on. What's missing from the model is the notion of authorship: that an author created an entry and, thus, each entry is written by an author. This is somewhat more complex to model than the concepts you've tackled so far. In fact, it brings you into the realm of Sails associations, which are different from simple attributes.

Comments and tags

Authorship isn't the only association you need to model for this application, so let's take a look at two simpler models before we tackle that big one. Each entry has comments and a set of tags that can be used to describe it. Tagging results in a "tag cloud" for visual display and produces a kind of metadata-based subject-classification system. By modeling these types, you can you practice with associations. Just keep in mind that you need a dozen or so more for a real-world CMS.

Recall that one of the rules of programming, including programming with Sails, is Keep it simple. In that spirit, the data model for comments is essentially just the body of the comment and the optional email of the individual leaving the comment, as shown in Listing 5.

Listing 5. Comment.js
module.exports = {
 
  attributes: {
    body: {
      type: 'string',
      required: true
    },
    commenterEmail: {
      type: 'email'
    }
  }
};

Similarly, the data model for tag is just the tag's name (typically metadata about the content such as "Java" or "Sails") with no need for additional adornment. Listing 6 shows the data model that enables tagging content in your CMS.

Listing 6. Tag.js
module.exports = {
 
  attributes: {
    name: {
      type: 'string'
    }
  }
};

Having defined a couple of data models, you have the basics you need to define many more. When you want to add a model, just create a file in the api/model directory. Alternatively, enter the command: sails generate model ... and Sails adds a placeholder for you.

Now it's time to start defining the relationships between your data models and the data that they contain.

Explicit relationships

Rather than the implicit approach that some database systems use, Sails uses explicit models of relationships. For a relational database system, for example, Sails would have you model an association between two tables using the data— the primary key defined in one table, whose value is used as a foreign key value in a row in another table — rather than defining it structurally in the database schema.

Relational database adherents will note that most database systems support structurally-defined relationships. By this, I mean that in an RDBMS, you would use database constraints to ensure that any value inserted as a foreign key value also existed in the relating table. In the case of Sails, we use data — not some kind of physical structure — to represent that relationship. By way of contrast to the relational model, consider a document-oriented database like MongoDB or CouchDB. In a document-oriented system you would embed an array as a member of a document, as opposed to associating a collection of values to other data elements.

Implicit modeling works well when you are modeling for a particular kind of data structure; it doesn't work so well when your data could be organized using a range of database types. Sails needs to understand relationships explicitly in order to know how to model and structure statements or queries for the given database type — whether it be RDBMS, NoSQL, or something else. While that might impose some unfamiliar requirements on your model objects, they're not too stringent; you just need to learn to think more explicitly about data and how it is connected.

One-to-many relationships

To get started, think about the entry-to-author relationship. While an author can write many entries, each entry has just one author. Unsurprisingly, Sails calls this a one-to-many relationship. The association between author and entries is also bidirectional, in that it should be possible to both retrieve all of the entries for a given author and see the author for any given entry. (As it turns out, Sails will, by default, automatically pull this additional data as part of a query and send it to the client.)

Defining a one-to-many relationship requires modifying the model objects on both ends of the association. You need to define the field that is the collection on the "one" side of the association, and the field that connects the "many" side of the association to that "one." This is a little awkward to describe in prose, but much simpler to see in code. Listing 7 shows the modified Author model.

Listing 7. Author.js with associated entries
module.exports = {
  attributes: {
    fullName: {
      type: 'string',
      required: true
    },
    bio: {
      type: 'string'
    },
    username: {
      type: 'string',
      unique: true,
      required: true
    },
    email: {
      type: 'email',
      required: true
    },
    entries: {
      collection: 'entry',
      via: 'author'
    }
  }
};

The refactored code in Listing 7 says that the Author has a field and entries that form a collection of Entry objects. The Entry type points back to the Author instance through the "author" field on the Entry object. All of that means that the Entry type needs to look as shown in Listing 8.

Listing 8. Entry type
module.exports = {
 
  attributes: {
    title: {
      type: 'string',
      required: true,
      defaultsTo: ''
    },
    body: {
      type: 'string',
      required: true,
      defaultsTo: ''
    },
    comments: {
      collection: 'comment',
      via: 'owner'
    },
    author: {
      model: 'author'
    }
  }
 
};

Note that in Listing 7 and Listing 8, the type being referenced — in the collection field of Author's entries field, and in the model field of Entry's author field — is lowercase.

This is because Sails puts the model type from the filename in lowercase, which is known as the type's identity. The lowercase type ( entries and author in the previous listings) become the prefix to the generated blueprints routes. That lowercased type also becomes the model's official name in the Sails system.

You see the concept of identity come up again when I discuss controllers in the next article in this series. Sails needs to be able to determine at the controller level whether a controller and model are of the same identity. It uses that information to generate the correct default blueprint routes. For now, simply note that Sails requires that the type used as the value of a model field should be in lowercase.

Linking model objects

Let's return to the one-to-many association between Author and Entry. What's left is to understand how to use that association in your code, both by defining it and knowing what to expect when either model object is retrieved from the database. Fortunately for us, Sails is very flexible about how we link model objects.

When you create an Author instance, Sails generates a unique primary key for it, which is defined in the id field. You can use the new id as the value of the association field, and Sails connects up the two objects automatically, as shown in Listing 9.

Listing 9. Connecting objects
Author.create({
    fullName: "Fred Flintstone",
    bio: "Lives in Bedrock, blogs in cyberspace",
    username: "fredf",
    email: "fred@flintstone.com"
}).exec(function (err, author) {
    Entry.create({
        title: "Hello",
        body: "Yabba dabba doo!",
        author: author.id
    }).exec(function (err, created) {
        Entry.create({
            title: "Quit",
            body: "Mr Slate is a jerk",
            author: author
        }).exec(function (err, created) {
            return res.send("Database seeded");
        });
    });
});

The code in Listing 9 is classic Node.js, which is to say "callbacks galore." The first call creates an Author instance, using the values you've passed in. When the callback in exec() fires, you obtain and set the Author's ID value as the value of the author field in your newly created Entry object. Or, to put it more simply: Link the Entry to the Author by setting the author field to reference the correct Author.

Another way to link model objects

If the previous approach doesn't suit you, Sails offers an alternative: Instead of grabbing the Author's id field, you could instead pass the Author object itself, in its entirety. Either way, the net result is the same.

Directly passing in the object might appeal more to developers who are more accustomed to thinking in terms of the physical storage model, whereas passing in the id more accurately reflects the linkage between objects. If you're used to "thinking in objects," passing the object confirms that the objects are now linked, but abstracts the details of how they are linked.

Associations revealed

However you get them there, once objects are set in the database, Sails goes to great lengths to transparently show the associations between them. Listing 10 shows what you'd see if you ran the code in Listing 9 and then paid a visit to http://localhost:1337/author (the Blueprints default route to fetch all authors from the system).

Listing 10. The returned author query
[
  {
    "entries": [
      {
        "title": "Hello",
        "body": "Yabba dabba doo!",
        "author": 6,
        "createdAt": "2016-02-16T21:15:55.722Z",
        "updatedAt": "2016-02-16T21:15:55.722Z",
        "id": 6
      },
      {
        "title": "Quit",
        "body": "Mr Slate is a jerk",
        "author": 6,
        "createdAt": "2016-02-16T21:15:55.725Z",
        "updatedAt": "2016-02-16T21:15:55.725Z",
        "id": 7
      }
    ],
    "fullName": "Fred Flintstone",
    "bio": "Lives in Bedrock, blogs in cyberspace",
    "username": "fredf",
    "email": "fred@flintstone.com",
    "createdAt": "2016-02-16T21:15:55.716Z",
    "updatedAt": "2016-02-16T21:15:55.716Z",
    "id": 6
  }
]

Similarly, visiting the corresponding route for Entry objects looks like Listing 11.

Listing 11. The returned entries query
[
  {
    "comments": [],
    "author": {
      "fullName": "Fred Flintstone",
      "bio": "Lives in Bedrock, blogs in cyberspace",
      "username": "fredf",
      "email": "fred@flintstone.com",
      "createdAt": "2016-02-16T21:15:55.716Z",
      "updatedAt": "2016-02-16T21:15:55.716Z",
      "id": 6
    },
    "title": "Hello",
    "body": "Yabba dabba doo!",
    "createdAt": "2016-02-16T21:15:55.722Z",
    "updatedAt": "2016-02-16T21:15:55.722Z",
    "id": 6
  },
  {
    "comments": [],
    "author": {
      "fullName": "Fred Flintstone",
      "bio": "Lives in Bedrock, blogs in cyberspace",
      "username": "fredf",
      "email": "fred@flintstone.com",
      "createdAt": "2016-02-16T21:15:55.716Z",
      "updatedAt": "2016-02-16T21:15:55.716Z",
      "id": 6
    },
    "title": "Quit",
    "body": "Mr Slate is a jerk",
    "createdAt": "2016-02-16T21:15:55.725Z",
    "updatedAt": "2016-02-16T21:15:55.725Z",
    "id": 7
  }
]

Even though entries and authors are stored independently, Sails uses its knowledge that these model objects are associated to "populate" the appropriate fields and make it appear that they are one flat object. Because Sails is intended as a back-end HTTP API implementation library, you want to keep the number of round trips from the application UI (mobile or web) to the database to a minimum. "Flattening" the data structure allows you to get a complete picture of Author's details in one (potentially large) round trip. This makes for a more efficient system that performs and scales better.

We've explored just one association model — one-to-many — but Sails supports them all: one-to-one, many-to-many, and some less traditional variations on those themes. For the most part, they all work similarly: Define the appropriate fields on the model objects, assign the object or its ID to the association field, and let Sails take care of the rest.

Conclusion

At this point, you should have a pretty good sense for how to model entities in Sails. In the next installment, we'll move on to getting more "activity" into the system, by way of Sails controllers. The default blueprints routes are nice, and they work for basic CRUD-style access, but most systems need to define custom routes that more closely reflect the object model. We'll dig into all of that next time. For now, it's time to say (once again) bon voyage!


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Open source
ArticleID=1029417
ArticleTitle=The busy JavaScript developer's guide to Sails.js, Part 3: Modeling relationships in Sails
publish-date=06152016