不要怂,就是干,撸起袖子干!

Commit 208d865b by Jan Aagaard Meier

📝 Docs for scopes and assocation scopes

1 parent efc4649d
...@@ -188,14 +188,153 @@ UserProjects = sequelize.define('UserProjects', { ...@@ -188,14 +188,153 @@ UserProjects = sequelize.define('UserProjects', {
}) })
``` ```
## Scopes
This section concerns association scopes. For a definition of assocation scopes vs. scopes on associated models, see [Scopes](docs/scopes).
Association scopes allow you to place a scope (a set of default attributes for `get` and `create`) on the association. Scopes can be placed both on the associated model (the target of the association), and on the through table for n:m relations.
#### 1:m
Assume we have tables Comment, Post and Image. A comment can be associated to either an image or a post via `commentable_id` and `commentable` - we say that Post and Image are `Commentable`
```js
this.Comment = this.sequelize.define('comment', {
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentable_id: Sequelize.INTEGER
}, {
instanceMethods: {
getItem: function() {
return this['get' + this.get('commentable').substr(0, 1).toUpperCase() + this.get('commentable').substr(1)]();
}
}
});
this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'post'
}
});
this.Comment.belongsTo(this.Post, {
foreignKey: 'commentable_id',
as: 'post'
});
this.Image.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'image'
}
});
this.Comment.belongsTo(this.Image, {
foreignKey: 'commentable_id',
as: 'image'
});
```
Note that the Image -> Comment and Post -> Comment relations define a scope, `commentable: 'image'` and `commentable: 'post'` respectively. This scope is automatically applied when using the association functions:
```js
Image.getComments()
SELECT * FROM comments WHERE commentable_id = 42 AND commentable = 'image';
Image.createComment({
title: 'Awesome!'
})
INSERT INTO comments (title, commentable_id, commentable) VALUES ('Awesome!', 'image', 42);
Image.addComment(comment);
UPDATE comments SET commentable_id = 42, commentable = 'image'
```
The `getItem` utility function on `Comment` completes the picture - it simply converts the `commentable` string into a call to etiher `getImage` or `getPost`, providing an abstraction over whether a comment belongs to a post or an image.
#### n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several item
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
```js
ItemTag = sequelize.define('item_tag', {
tag_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable'
},
taggable: {
type: DataTypes.STRING,
unique: 'item_tag_taggable'
},
taggable_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable',
references: null
}
});
Tag = sequelize.define('tag', {
name: DataTypes.STRING
});
Post.belongsToMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
foreignKey: 'taggable_id',
constraints: false
});
Tag.belongsToMany(Post, {
through: {
model: ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
```
Notice that the scoped column (`taggable`) is now on the through model (`ItemTag`).
We could also define a more restrictive association, for example to get all pending tags for a post by applying a scope of both the through model (`ItemTag`) and the target model (`Tag`):
```js
Post.hasMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
scope: {
status: 'pending'
},
as: 'pendingTags',
foreignKey: 'taggable_id',
constraints: false
});
Post.getPendingTags();
```
```sql
SELECT `tag`.* INNER JOIN `item_tags` AS `item_tag`
ON `tag`.`id` = `item_tag`.`tagId`
AND `item_tag`.`taggable_id` = 42
AND `item_tag`.`taggable` = 'post'
WHERE (`tag`.`status` = 'pending');
```
`constraints: false` disables references constraints on the `taggable_id` column. Because the column is polymorphic, we cannot say that it `REFERENCES` a specific table.
## Naming strategy ## Naming strategy
By default sequelize will use the model name (the name passed to `sequelize.define`) to figure out the name of the model when used in associations. For example, a model named `user` will add the functions `get/set/add User` to instances of the associated model, and a property named `.user` in eager loading, while a model named `User` will add the same functions, but a property named `.User` (notice the upper case U) in eager loading. By default sequelize will use the model name (the name passed to `sequelize.define`) to figure out the name of the model when used in associations. For example, a model named `user` will add the functions `get/set/add User` to instances of the associated model, and a property named `.user` in eager loading, while a model named `User` will add the same functions, but a property named `.User` (notice the upper case U) in eager loading.
As we've already seen, you can alias models in associations using `as`. In single associations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias: As we've already seen, you can alias models in associations using `as`. In single associations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias:
```js ```js
User.belongsToMany(Project, { as: { singular: 'task', plural: 'tasks' }}) User.belongsToMany(Project, { as: { singular: 'task', plural: 'tasks' }})
// Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes. // Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes.
``` ```
......
# Definition
Scoping allows you to define commonly used queries that you can easily use later. Scopes can include all the same attributes as regular finders, `where`, `include`, `limit` etc.
Scopes are defined in the model definition and can be finder objects, or functions returning finder objects - except for the default scope, which can only be an object:
```js
var Project = sequelize.define('project', {
// Attributes
}, {
defaultScope: {
where: {
active: true
}
},
scopes: {
deleted: {
where: {
deleted: true
}
},
activeUsers: {
include: [
{ model: User, where: { active: true }}
]
}
random: function () {
return {
where: {
someNumber: Math.random()
}
}
},
accessLevel: function (value) {
return {
where: {
accessLevel: {
$gte: value
}
}
}
}
}
});
```
The default scope is always applied. This means, that with the model definition above, `Project.findAll()` will create the following query:
```sql
SELECT * FROM projects WHERE active = true
```
The default scope can be removed by calling `.unscoped()`, `.scope(null)`, or by invoking another scope:
```js
Project.scope('deleted').findAll(); // Removes the default scope
```
```sql
SELECT * FROM projects WHERE deleted = true
```
# Usage
Scopes are applied by calling `.scope` on the model definition, passing the name of one or more scopes. `.scope` returns a fully functional model instance with all the regular methods: `.findAll`, `.update`, `.count`, `.destroy` etc. You can save this model instance and reuse it later:
```js
var DeletedProjects = Project.scope('deleted');
DeletedProjects.findAll();
// some time passes
// let's look for deleted projects again!
DeletedProjects.findAll();
```
Scopes apply to `.find`, `.findAll`, `.count`, `.update` and `.destroy`.
Scopes which are functions can be invoked in two ways. If the scope does not take any arguments it can be invoked as normally. If the scope takes arguments, pass an object:
```js
Project.scope('random', { method: ['accessLevel', 19]}).findAll();
```
```sql
SELECT * FROM projects WHERE someNumber = 42 AND accessLevel >= 19
```
## Merging
Several scopes can be applied simultaneously by passing an array of scopes to `.scope`, or by passing the scopes as consequtive arguments.
```js
// These two are equivalent
Project.scope('deleted', 'activeUsers').findAll();
Project.scope(['deleted', 'activeUsers']).findAll();
```
```sql
SELECT * FROM projects
INNER JOIN users ON projects.userId = users.id
AND users.active = true
```
If you want to apply another scope alongside the default scope, pass the key `defaultScope` to `.scope`:
```js
Project.scope('defaultScope', 'deleted').findAll();
```
```sql
SELECT * FROM projects WHERE active = true AND deleted = true
```
When invoking several scopes, keys from subsequent scopes will overwrite previous ones (similar to [_.assign](https://lodash.com/docs#assign)). Consider two scopes:
```js
{
scope1: {
where: {
firstName: 'bob',
age: {
$gt: 20
}
},
limit: 2
},
scope2: {
where: {
age: {
$gt: 30
}
},
limit: 10
}
}
```
Calling `.scope('scope1', 'scope2')` will yield the following query
```sql
WHERE firstName = 'bob' AND age > 30 LIMIT 10
```
Note how `limit` and `age` are overwritten by `scope2`, whíle `firstName` is preserved. `limit`, `offset`, `order`, `paranoid`, `lock` and `raw` are overwritten, while `where` and `include` are shallowly merged. This means that identical keys in the where objects, and subsequent includes of the same model will both overwrite each other.
The same merge logic applies when passing a find object directly to findAll on a scoped model:
```js
Project.scope('deleted').findAll({
where: {
firstName: 'john'
}
})
```
```sql
WHERE deleted = true AND firstName = 'john'
```
Here the `deleted` scope is merged with the finder. If we were to pass `where: { firstName: 'john', deleted: false }` to the finder, the `deleted` scope would be overwritten.
# Associations
Sequelize has two different but related scope concepts in relation to associations. The difference is subtle but important:
* **Assocation scopes** Allow you to specify default attributes when getting and setting associations - useful when implementing polymorphic associations. This scope is only invoked on the association between the two models, when using the `get`, `set`, `add` and `create` associated model functions
* **Scopes on associated models** Allows you to apply default and other scopes when fetching associations, and allows you to pass a scoped model when creating associtaions. These scopes both apply to regular finds on the model and to find through the association.
As an example, consider the models Post and Comment. Comment is associated to several other models (Image, Video etc.) and the association between Comment and other models is polymorphic, which means that Comment stores a `commentable` column, in addition to the foreign key `commentable_id`.
The polymorphic association can be implemented with an _association scope_ :
```js
this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
scope: {
commentable: 'post'
}
});
```
When calling `post.getComments()`, this will automatically add `WHERE commentable = 'post'`. Similarly, when adding new comments to a post, `commentable` will automagically be set to `'post'`. The association scope is meant to live in the background without the programmer having to worry about it - it cannot be disabled. For a more complete polymorphic example, see [Association scopes](docs/associations/#scopes)
Consider then, that Post has a default scope which only shows active posts: `where: { active: true }`. This scope lives on the associated model (Post), and not on the association like the `commentable` scope did. Just like the default scope is applied when calling `Post.findAll()`, it is also applied when calling `User.getPosts()` - this will only return the active posts for that user.
To disable the default scope, pass `scope: null` to the getter: `User.getPosts({ scope: null })`. Similarly, if you want to apply other scopes, pass an array like you would to `.scope`:
```js
User.getPosts({ scope: ['scope1', 'scope2']});
```
If you want to create a shortcut method to a scope on an associated model, you can pass the scoped model to the association. Consider a shortcut to get all deleted posts for a user:
```js
var Post = sequelize.define('post', attributes, {
defaultScope: {
where: {
active: true
}
},
scopes: {
deleted: {
where: {
deleted: true
}
}
}
});
User.hasMany(Post); // regular getPosts association
User.hasMany(Post.scope('deleted'), { as: 'deletedPosts' });
```
```js
User.getPosts(); // WHERE active = true
User.getDeletedPosts(); // WHERE deleted = true
```
...@@ -16,7 +16,9 @@ pages: ...@@ -16,7 +16,9 @@ pages:
- ['docs/getting-started.md', 'Documentation', 'Getting Started'] - ['docs/getting-started.md', 'Documentation', 'Getting Started']
- ['docs/schema.md', 'Documentation', 'Working with table schemas'] - ['docs/schema.md', 'Documentation', 'Working with table schemas']
#- ['docs/usage.md', 'Documentation', 'Usage'] #- ['docs/usage.md', 'Documentation', 'Usage']
- ['docs/models.md', 'Documentation', 'Models'] - ['docs/models-definition.md', 'Documentation', 'Models - Definition']
- ['docs/models-usage.md', 'Documentation', 'Models - Usage']
- ['docs/scopes.md', 'Documentation', 'Scopes']
- ['docs/instances.md', 'Documentation', 'Instances'] - ['docs/instances.md', 'Documentation', 'Instances']
- ['docs/associations.md', 'Documentation', 'Relations/Associations'] - ['docs/associations.md', 'Documentation', 'Relations/Associations']
- ['docs/hooks.md', 'Documentation', 'Hooks'] - ['docs/hooks.md', 'Documentation', 'Hooks']
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!