Skip to content

Latest commit

 

History

History
294 lines (229 loc) · 12.9 KB

model-relations.md

File metadata and controls

294 lines (229 loc) · 12.9 KB

Dito.js Model Relations

Just like Objection.js Models, Dito.js Models can be related to one-another through relations. The definition of such relations is done differently than in Objection.js though:

Instead of providing them in a static field called relationMappings, they are defined in a simplified and extended format in the static relations field.

Just like with Model Properties, the relations field is an object where the key is the name of the relation, and the value is an object holding its definition. Within this relation object, the following settings are available:

Keywords Description
relationstring The type of relation. Supported types are:
'belongsTo', 'hasMany', 'hasOne', 'manyToMany' and 'hasOneThrough'.
fromstring The model and property name from which the relation is to be built, as a string with both identifiers separated by '.', e.g.: 'FromModelClass.fromPropertyName'
tostring The model and property name to which the relation is to be built, as a string with both identifiers separated by '.', e.g.: 'ToModelClass.toPropertyName'
throughboolean | Object Controls whether a join model class and table is to be built automatically, or allows to specify one manually:
  • Setting it to true tells Dito.js to create the join table and model class automatically.
  • Providing an object allows for more fine-grained configuration of the join table and model class. See Join Models and Tables for details.
inverseboolean Controls whether the relation is the inverse of another relation. This information is only required when working with through relations. Without it, Dito.js wouldn't be able to tell which side of the relation is on the left-hand side, and which is on the right-hand side when automatically creating the join model class and table.
scopestring Optionally, a scope can be defined to be applied when loading the relation's models. The scope needs to be defined in the related model class' scopes definitions. See Model Scopes for more information on working with scopes.
nullableboolean Controls whether the auto-inserted foreign key property should be marked as nullable. This only makes sense on a 'belongsTo' relation, where the model class holds the foreign key, and only when the foreign key isn't already explicitly defined in the Model Properties.
ownerboolean Controls whether the relation owns the models that it holds, or whether it is simply relating to them, and a relation elsewhere is considered to be their owner.
This controls behavior in different parts of Dito.js
  • In migrations, the definition of the foreign key that refer to the model that owns the data calls .onDelete('CASCADE') in order to delete the owned data when their owner is deleted.
  • In graph inserts and upserts, the inset/upsert options and their correct relation expressions are automatically determined so that the operation creates/deletes or relates/unrelates in any given relation, reflecting the intent of the relation's owner setting.

Relation Types

Dito.js provides the same types of relations as Objection.js, but references them with a simplified string representation of each type instead of their actual class:

'belongsTo', 'hasMany', 'hasOne', 'manyToMany' and 'hasOneThrough'.

These relation types can be divided into three groups:

'belongsTo'

The 'belongsTo' relation is used for relations where the model class on which it is defined holds the foreign key. The reverse of a 'belongsTo' relation is either a 'hasMany' or 'hasOne' relation.

Here an example of model Book which belongs to a model Author:

class Book extends Model {
  static relations = {
    author: {
      relation: 'belongsTo',
      from: 'Book.authorId',
      to: 'Author.id'
    }
  }
}

Note: The from property is a foreign key defined on the own model, while the to property is the primary key on the related model. While it is most common to take the model's primary key ('id' in this case), one could relate to other properties as well.

'hasMany' and 'hasOne'

The opposite of a 'belongsTo' relation can either be a 'hasMany' or a 'hasOne' relation, depending on whether multiple 'belongsTo' relations to it are envisioned, or only a maximum of one, in which case it is a one-to-one relation.

One can then also define the inverse relationship from B to A although it is not required in this case. This generally is a hasMany relation, since many A can belong to the same B. In some cases it can also be a hasOne relation, if we are looking at a one-to-one relationship.

In the case of the above example of Author and Book, an author may have written multiple books, so a 'hasMany' relation is probably what we want to declare:

class Author extends Model {
  static relations = {
    books: {
      relation: 'hasMany',
      from: 'Author.id',
      to: 'Book.authorId'
    }
  }
}

Note: The from property is the primary key defined on the own model, while the to property is a foreign key on the related model. Again, one could relate from other properties than primary keys as well.

The difference between a 'hasMany' and a 'hasOne' relation is simply that when using queries, the 'hasOne' relation is limited to returning the first result as an object, while the 'hasMany' relation returns an array of all models.

Example: 'belongsTo''hasMany'

To better understand what the relation configurations in the examples above really do, here two example tables to go along with that configuration. Note that due to the use of the use of config.knex.normalizeDbNames, the column for the authorId property is called author_id. See Configuration for details on database name normalization.

Table author
id name
1 Shakespeare
2 Goethe
Table book
id name author_id
1 Werther 2
2 Hamlet 1
3 Faust 2
4 Othello 1

Some observations:

  • from always points to the own model of a relation, while to points to the related model.
  • Book.author is a 'belongsTo' relation, therefore its from setting points to the foreign key in its own table.
  • Author.books is a 'hasMany' relation, therefore its from setting points to its own primary key, while the to setting points to the foreign key in the related table.

'manyToMany' and 'hasOneThrough'

The 'manyToMany' relation allows relating many models of one class with many models of another class. This relation requires a join model and table, and Dito.js can automatically create them if desired. This is achieved by setting through to true. The inverse setting is used during the creation of the migrations, to tell which side of the relation is on the left-hand side, and which is on the right-hand side when determining the name of the generated join model class and table.

Going back to our example above, we can imagine a new Category class that can be associated with the Book model through two 'manyToMany' relations that go both ways:

class Book extends Model {
  static relations = {
    categories: {
      relation: 'manyToMany',
      from: 'Book.id',
      to: 'Category.id',
      through: true,
      inverse: false
    }
  }
}

class Category extends Model {
  static relations = {
    books: {
      relation: 'manyToMany',
      from: 'Category.id',
      to: 'Book.id',
      through: true,
      inverse: true
    }
  }
}

Note: Setting through: true and using opposite settings of from and to as well as inverse in either relation automatically negotiates the naming of the created join table for us. Because the inverse is on Category, the resulting join model class will be called BookCategory while the join table will be named book_category (assuming config.knex.normalizeDbNames is true).

The 'hasOneThrough' relation is a special case of a 'manyToMany' relation, with the same difference as 'hasOne' has from 'hasMany': It only returns one result in queries, instead of an array of all related models. It is rare to require join tables for such relations though, as usually a foreign key would suffice.

Join Models and Tables

For the cases where the automatically generated join table and model doesn't offer enough control, Dito.js allows more fine-grained configuration of the join table and model class by providing an object for the trough setting:

Keywords Description
through.fromstring The model and property name or table and column name of an existing join model class or join table from which the through relation is to be built, as a string with both identifiers separated by '.', e.g.: 'FromModelClass.fromPropertyName'
through.tostring The model and property name or table and column name of an existing join model class or join table to which the through relation is to be built, as a string with both identifiers separated by '.', e.g.: 'toModelClass.toPropertyName'
through.extraArray List additional columns to be added to the related model. See Extra Properties for details.
  • To work with an existing join model class, provide both the through.from and through.to reference just as with the main relation definition:

    through: {
      from: 'joinModelClass.fromPropertyName',
      to: 'joinModelClass.toPropertyName'
    }
  • To work with a join table directly without a model class, use the table/column references instead:

    through: {
      from: 'join_table_name.from_column_name',
      to: 'join_table_name.to_column_name'
    }

Note: Dito.js has no reliable way to distinguish the two formats. The approach currently is to check if the specified table/model reference is indeed a model, in which case the first scenario is assumed. If th model isn't known, no error is thrown, and direct database references are assumed.

Extra Properties

When working with a join model class or table, extra columns from it can be added to the related model, as if it was define on its own table. The then appear as additional properties on the related model:

through: {
  extra : ['extraProperty']
}

Relation Accessors

Dito.js models give access to their relations through special accessors for each relation, accessible through modelClass.$relationName as well as modelInstance.$relationName. These accessors offer easier access to related queries and join model classes.

Just like Dito.js model classes, relation accessors also offer QueryBuilder short-cuts. Read more on these short-cuts in Model Queries – Short-Cuts to Query Methods.

RelationAccessor.query()

When called on a model instance's relation accessor, this is a short-cut to modelInstance.$relatedQuery('relationName'). When called on model class' relation accessor, it is a short-cut to the static modelClass.relatedQuery('relationName').

RelationAccessor.joinModelClass

Returns a reference to the joinModelClass if the relation uses a through definition.

Relation Accessor Example

The following example of an Objection.js' style related query can be simplified using Dito.js' relation accessors.

So this:

model.$relatedQuery('relationName').where()

becomes:

model.$relationName.query().where()

and because relation accessors also offer QueryBuilder short-cuts, this can be further shortened to:

model.$relationName.where()

Differences to Objection.js

  • Objection.js doesn't inherit and merge relation definitions.

  • In Dito.js, model and property references are used in the definition of relations, while Objection.js uses table and column references in its relationMappings format, and requires references to the actual modelClass along with them, which can cause issues with circular references. Dito.js translates its relations automatically to Objection.js' relationMappings format, and is able to create modelClass references for them automatically from the application's list of registered models.

  • Dito.js models give access to their relations through special relation accessors. See Relation Accessors for more information about these accessors.