Skip to content

Instantly share code, notes, and snippets.

@fabien
Last active February 3, 2016 18:26
Show Gist options
  • Save fabien/7d4ed2c9fcd815e0703f to your computer and use it in GitHub Desktop.
Save fabien/7d4ed2c9fcd815e0703f to your computer and use it in GitHub Desktop.

Polymorphic relations:

Conceptually similar to: http://guides.rubyonrails.org/association_basics.html#polymorphic-associations

Example models: Picture, Author, Reader.

HasMany:

The usual options apply, for example: as: 'photos' to specify a different relation name/accessor.

Author.hasMany(Picture, { polymorphic: 'imageable' });
Reader.hasMany(Picture, { polymorphic: { // alternative syntax
  as: 'imageable', // if not set, default to: reference
  foreignKey: 'imageableId', // defaults to 'as + Id'
  discriminator: 'imageableType' // defaults to 'as + Type'
} });

BelongsTo

Because the related model is dynamically defined, it cannot be declared upfront.

So instead of passing in the related model(name), the name of the polymorphic relation is specified.

Picture.belongsTo('imageable', { polymorphic: true }); 

// Alternatively, use an object for setup:

Picture.belongsTo('imageable', { polymorphic: {
  foreignKey: 'imageableId',
  discriminator: 'imageableType'
} });

HasAndBelongsToMany

This requires an explicit 'through' model, in this case: PictureLink

The relations Picture.belongsTo(PictureLink) and Picture.belongsTo('imageable', { polymorphic: true }); will be setup automatically.

The same is true for the needed properties on PictureLink.

Author.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' });
Reader.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' });

// Optionally, define inverse hasMany relations (invert: true):

Picture.hasMany(Author, { through: PictureLink, polymorphic: 'imageable', invert: true });
Picture.hasMany(Reader, { through: PictureLink, polymorphic: 'imageable', invert: true });

HasOne:

As can be seen here, you can specify as: 'avatar' to explicitly set the name of the relation. If not set, it will default to the polymorphic name.

Picture.belongsTo('imageable', { polymorphic: true });

Author.hasOne(Picture, { as: 'avatar', polymorphic: 'imageable' });
Reader.hasOne(Picture, { polymorphic: { as: 'imageable' } });

Embedded models/relations:

Conceptually similar to: http://mongoosejs.com/docs/subdocs.html

Embedded models are usually not persisted separately, however, this can be done for the purpose of denormalizing or 'freezing' data.

Example models: TodoList, TodoItem

// the accessor name defaults to: 'singular + List' (todoItemList)

TodoList.embedsMany(TodoItem, { as: 'items' });

TodoList.create({ name: 'Work' }, function(err, list) {
  list.items.build({ content: 'Do this', priority: 5 });
  list.items.build({ content: 'Do that', priority: 1 });
  list.save(function(err, list) {
    console.log(err, list);
  });
});

TodoList.findOne(function(err, list) {
  console.log(list.todoItems[0].content); // `Do this`
  console.log(list.items.at(1).content); // `Do that`
  list.items.find({ where: { priority: 1 } }, function(err, item) {
     console.log(item.content); // `Do that`
  });
});

Advanced example: embed with belongsTo

Embedded models can have relations, just like any other model. This is quite a powerful technique that can simplify certain setups, and allows denormalization of data. An added benefit is that the ordering of items is explictly defined by the internal array.

In the following example, a setup similar to a has-many-through is created. Instead of a seperately persisted through-model, an embedded model called Link is used.

See the tests cases for an example of a polymorphic embed/belongsTo setup.

Models: Category, Product, Link

The new relation options: scope and properties come in to play here as well.

var Category = db.define('Category', { name: String });
var Product = db.define('Product', { name: String });
var Link = db.define('Link', { notes: String });

Category.embedsMany(Link, { 
  as: 'items', // rename (default: productList)
  scope: { include: 'product' }
});

Link.belongsTo(Product, { 
  foreignKey: 'id', // re-use the actual product id
  properties: { id: 'id', name: 'name' }, // denormalize, transfer id
  // new option: invertProperties see: 
  // https://github.com/strongloop/loopback-datasource-juggler/pull/219 
  options: { invertProperties: true }
});

Category.create({ name: 'Category B' }, function(err, cat) {
  category = cat;
  var link = cat.items.build({ notes: 'Some notes...' });
  link.product.create({ name: 'Product 1' }, function(err, p) {
    cat.links[0].id.should.eql(p.id);
    cat.links[0].name.should.equal('Product 1'); // denormalized
    cat.links[0].notes.should.equal('Some notes...');
    cat.items.at(0).should.equal(cat.links[0]);
    done();
  })
});

Category.findById(category.id, function(err, cat) {
  cat.name.should.equal('Category B');
  cat.links.toObject().should.eql([
    {id: 5, name: 'Product 1', notes: 'Some notes...'}
  ]);
  cat.items.at(0).should.equal(cat.links[0]);
  cat.items(function(err, items) { // alternative access
    items.should.be.an.array;
    items.should.have.length(1);
    items[0].product(function(err, p) {
      p.name.should.equal('Product 1'); // actual value
      done();
    });
  });
});

Relation options:

There are basically three new options that pertain to most relation types:

scope

The scope (object|fn) option will apply to all filtering/conditions on the related scope.

The object or returned object (in case of the fn call) can have all the usual filter options:

where, order, include, limit, offset, ...

These options are merged into the default filter, which means that the where part will be AND-ed. The other options usually override the defaults (standard mergeQuery behavior).

When scope is a function, it will receive the current instance, as well as the default filter object.

// only allow products of type: 'shoe', always include products
Category.hasMany(Product, { as: 'shoes', scope: { where: { type: 'shoe' }, include: 'products' });

Product.hasMany(Image, { scope: function(inst, filter) { // inst is a category
  return { type: inst.type }; // match category type with product type.
});

properties

The properties option can be specified as follows:

  1. as an object: the keys will refer to the instance, the value will be the attribute key on the related model (mapping)
  2. as a function: the resulting object (key/values) will be merged into the related model directly
// 1. transfer the type to the product, and denormalize the category name into categoryName on creation
Category.hasMany(Product, { as: 'shoes', properties: { type: 'type', category: 'categoryName' });

// 2. the same as above, but using a callback function
Product.hasMany(Image, { properties: function(inst) { // inst is a category
  return { type: inst.type, categoryName: inst.name };
});

scopeMethods

Finally, scopeMethods allows you to add custom scope methods. Note that these are not remoting enabled by default, you have to set this up yourself (fn.shared = true, ...).

Again, the option can be either an object or a function (advanced use).

var reorderFn = function(ids, cb) {
  // `this` refers to the RelationDefinition
  console.log(this.name); // `images` (relation name)
  // do some reordering here & save
  cb(null, [3, 2, 1]);
};

// manually declare remoting params
reorderFn.shared = true;
reorderFn.accepts = { arg: 'ids', type: 'array', http: { source: 'body' } };
reorderFn.returns = { arg: 'ids', type: 'array', root: true };
reorderFn.http = { verb: 'put', path: '/images/reorder' };

Product.hasMany(Image, { scopeMethods: { reorder: reorderFn } });
@crandmck
Copy link

@fabien /cc @raymondfeng
Would you be so kind as to review the docs at http://docs.strongloop.com/display/LB/Creating+model+relations#Creatingmodelrelations-Relationoptions ? They basically duplicate the above info for the new relation options.

I have one specific question, re:

There are basically three new options that pertain to most relation types:

"Most"? Which ones DON'T they apply to?

I would appreciate any other examples of suggestions for clarifying these docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment