Related articles:
Overview
操作钩子不和一个具体分方法挂钩, 而是被所有指定创建,读取,更新,删除的方法所触发。 这些方法都是继承自PersistedModel的方法。 使用操作钩子你可以拦截CRUD操作。
Note:
使用操作钩子代理使用过时的模型钩子。
API非常简单:方法是Model.observe(_name_, _observer_)
, name
是操作钩子的名字,例如”before save”, observer
是一个函数,它有两个参数(context, callback)。
Child models inherit observers,你可以为一个hook注册多个observers。
下面的表格概括了被PersistedModel CRUD方法触发的操作钩子。(打叉的代表支持)
Method → Operation hook ↓ |
find findOne findById |
exists | count | create | upsert | findOrCreate | deleteAll deleteById |
updateAll | prototype .save |
prototype .delete |
prototype .updateAttributes |
---|---|---|---|---|---|---|---|---|---|---|---|
access | X | X | X | X | X | X | X | ||||
before save | X | X | X | X | X | X | |||||
after save | X | X | X | X | X | X | |||||
before delete | X | X | |||||||||
after delete | X | X | |||||||||
loaded | X | X | X | X | X | X | X | X | |||
persist | X | X | X | X | X | X |
操作钩子的上下文对象(context)
操作钩子的上下文对象和通过 Model.beforeRemote、
Model.afterRemote注册给远程钩子的上下文对象没有任何关系。
关于远程钩子更多信息请见Remote hooks。注意了操作钩子的上下文对象和 loopback.getCurrentContext()提供的上下文对象完全不相关。
操作和钩子的共同属性
目标模型
context.Model是操作相关的Model。例如
Product.find()
context.Model = Product。
操作选项
上下文对象有一个options属性, 通过它钩子可以获取到调用者设置的options。例如:
var FILTERED_PROPERTIES = ['immutable', 'birthday'];
MyModel.observe('before save', function filterProperties(ctx, next) {
if (ctx.options && ctx.options.skipPropertyFilter) return next();
if (ctx.instance) {
FILTERED_PROPERTIES.forEach(function(p) {
ctx.instance.unsetAttribute(p);
});
} else {
FILTERED_PROPERTIES.forEach(function(p) {
delete ctx.data[p];
});
}
next();
});
// 不会更新到
MyModel.updateOrCreate({
id: 1,
immutable: 'new value'
}, cb);
// 会更新到
MyModel.updateOrCreate({
id: 2,
immutable: 'new value'
}, {
skipPropertyFilter: true
}, cb);
Shared hookState property
The ctx.hookState
property is preserved across all hooks invoked for a single operation.
For example, both “access”, “before save” and “after save” invoked for Model.create()
have the same object passed in ctx.hookState
.
This way the hooks can pass state date between “before” and “after” hook.
Hook and operation specific properties
Besides the common properties listed above, each hook provides additional properties identifying the model instance(s) affected by the operation and the changes applied. The general rule is that the context provides either an instance
property or a pair of data
and where
properties.
instance
当操作作用于单个实例,对模型所有的属性执行update/create/delete的时候就会存在这个属性。例如 PersistedModel.create()
.
where + data
当操作作用于多个实例(例如 PersistedModel.updateAll())或者执行一个针对模型的部分属性进行的更新操作(例如 PersistedModel.prototype.updateAttributes()),上下文会提供一个where用来显示哪些纪录会受到影响,另外还会提供一个data简单对象,它包含了要修改的数据。
isNewInstance
这个属性用来区分是创建操作还是更新操作。
Important:
只有某些connector支持ctx.isNewInstance。有些connector没有定义。详见
检查是否支持ctx.isNewInstance。
currentInstance
这个属性被针对单个实例进行的一些修改的操作提供。它包含了受影响的模型实例,它时只读的,你不应该去修改它。
检查是否支持ctx.isNewInstance
ctx.isNewInstance
只支持memory, MongoDB, 和MySQL connectors。你可以在”after save”钩子中测试你的connector是否支持isNewInstance。
例如:
MyModel.observe('after save', function(ctx, next) {
console.log('supports isNewInstance?', ctx.isNewInstance !== undefined);
next();
});
// It's important to provide a value for the id property
// Include also values for any required properties
MyModel.updateOrCreate({
id: 123
}, console.log);
进入受影响的实例
只作用于单个实例的操作(例如,所有的CRUD操作,除了 PersistedModel.deleteAll
和 PersistedModel.updateAll
) 通常在上下文对象中提供了受影响的实例。然而,针对不同的操作这个实例被不同的属性提供,一个是可以修改的ctx.instance,一个是只读的ctx.currentInstance:
before save | persist | after save | before delete | after delete | |
---|---|---|---|---|---|
create |
ctx.instance |
ctx.currentInstance |
ctx.instance |
--- | --- |
findOrCreate |
ctx.instance |
ctx.currentInstance |
ctx.instance |
--- | --- |
updateOrCreate |
n/a* | ctx.currentInstance |
ctx.instance |
--- | --- |
updateAll |
n/a | n/a | n/a | --- | --- |
prototype.save |
ctx.instance |
ctx.currentInstance |
ctx.instance |
--- | --- |
prototype.updateAttributes |
ctx.currentInstance |
ctx.currentInstance |
ctx.instance |
--- | --- |
|
--- | --- | --- | ctx.instance |
ctx.instance |
deleteAll |
--- | --- | --- | n/a | n/a |
updateOrCreate
在before save钩子中不提供任何任何实例。因为我们不知道这个到底是更新还是创建。 we cannot tell whether there is any existing “currentInstance” affected by the operation.
钩子
LoopBack提供下面几种操作钩子:
下面的表格力列出了PersistedModel方法会触发哪些钩子。
Method name | Hooks invoked |
---|---|
all |
access, loaded |
create | before save, after save, loaded, persist |
upsert (aka updateOrCreate) | access, before save, after save, loaded, persist |
findOrCreate | access, before save*, after save*, loaded, persist |
deleteAll (aka destroyAll) deleteById (aka destroyById) |
access, before delete, after delete |
updateAll | access, before save, after save, persist |
prototype.save | before save, after save, persist, loaded |
prototype.delete | before delete, after delete |
prototype.updateAttributes | before save, after save, loaded, persist |
* 当findOrCreate
找到了一个存在的模型,save钩子不会被触发。However, connectors providing atomic implementation may trigger before save
hook even when the model is not created, since they cannot determine in advance whether the model will be created or not.
access
当查询模型的时候access
钩子会被触发。 Observers可以修改查询,例如添加额外的查询条件。
Note:
实例方法不会触发access钩子,因为这个钩子已经被从数据库加载模型实例的方法触发了。
例如,当你通过REST AP调用实例方法的时候,会调用两个命令:静态方法findById()(出发access钩子),另外一个是真正请求的实例方法。
上下文对象的属性
Model
- 是查询的那个模型query
- 查询包含的字段,如where,
include,
order
等。
例子:
MyModel.observe('access', function logQuery(ctx, next) {
console.log('Accessing %s matching %s', ctx.Model.modelName, ctx.query.where);
next();
});
MyModel.observe('access', function limitToTenant(ctx, next) {
ctx.query.where.tenantId = loopback.getCurrentContext().tenantId;
next();
});
before save
before save钩子在一个模型实例被创建或更新前触发。当下面的方法调用的时候会触发before save钩子:
* 当findOrCreate找到了存在的模型,before save钩子不会被触发。
However, connectors providing atomic implementation may trigger before save
hook even when the model is not created, since they cannot determine in advance whether the model will be created or not.
这个before save钩子在模型验证函数前被触发。
Tip:
因为before save钩子在验证起被调用前触发,所以你可以使用它给一些属性当值是空的时候赋上默认值。
取决于不同的方法触发before save钩子,上下文会有下面不同的属性:
- 所有针对单个模型的save
Model
- 哪个模型被保存instance
- 哪个模型实例被保存
- 可能对多个模型实例进行更新的操作
Model
- 哪个模型被保存where
- 描述哪些实例会受到影响的where过滤器data
- 应用于更新的数据currentInstance
- 受影响的实例, 更多信息见Triggering with prototype.updateAttributes。
ctx.isNewInstance
当ctx.instance是存的时候,before save钩子提供了ctx.isNewInstance,它的值如下:
- 为true 表示是创建操作
- 为false 表示是更新操作
- 如果是
updateOrCreate
,prototype.save
,prototype.updateAttributes
, 和updateAll
operations它的值是undefined
在“before save”钩子中操纵模型数据
上面讲到了,上下文对象提供了instance属性,data属性和where属性。在ctx.instance中暴露出一个完整的模型实例,能让我们调用这个模型实例的人和方法(包括自定义方法,例如可以在钩子里面调用oreder.recalculateShippingAndTaxes())。That’s why LoopBack CRUD operation provide the instance in as many cases as possible.
There are two notable exception when it is not feasible to provide the instance object:
PersistedModel.updateAll
更新匹配查询条件的多个实例。LoopBack不需要从数据里面加载他们的数据, it’s up to the database to find these instances and apply necessary changes.PersistedModel.updateAttributes
执行部分更新,只修改模型的部分属性。LoopBack会有一个模型实例,它需要知道哪些模型属性要被修改并且持续化到数据库里面去。Passing the operation payload inctx.data
- 是一个包含了哪些要修改的属性的简单对象 - 这样就很容易在钩子里面实现对要修改属性的添加/删除。可以通过ctx.curerntInstance获得这个要修改的模型实例,注意了ctx.currentInstance是只读的。
Examples
MyModel.observe('before save', function updateTimestamp(ctx, next) {
if (ctx.instance) {
ctx.instance.updated = new Date();
} else {
ctx.data.updated = new Date();
}
next();
});
MyModel.observe('before save', function computePercentage(ctx, next) {
if (ctx.instance) {
ctx.instance.percentage = 100 * ctx.instance.part / ctx.instance.total;
} else if (ctx.data.part && ctx.data.total) {
ctx.data.percentage = 100 * ctx.data.part / ctx.data.total;
} else if (ctx.data.part || ctx.data.total) {
// either report an error or fetch the missing properties from DB
}
next();
});
删除不需要的属性
删除(unset)一个模型实例的属性,, it is not enough the set its value to undefined and/or delete the property. One has to call unsetAttribute(name)
instead. However, don’t forget to handle the case where the context has a data property instead! Since the data object is a plain object, you can remove properties the usual way via delete operator.
Example:
MyModel.observe('before save', function removeUnwantedField(ctx, next) {
if (ctx.instance) {
ctx.instance.unsetAttribute('unwantedField');
} else {
delete ctx.data.unwantedField;
}
next();
});
This completely removes the field and prevents inserting spurious data into the database.
after save
after save钩子在模型修改成功并 持续化到数据库后触发,下面的方法会触发after save钩子:
* 当findOrCreate
找到了存在的模型,after save钩子不会被触发。However, connectors providing atomic implementation may trigger before save
hook even when the model is not created, since they cannot determine in advance whether the model will be created or not.
依赖于触发这个钩子的不同方法,上下文对象提供不同的属性:
- 对单个模型的更新:
Model
- 要被保存的模型的构造函数-
instance
- 要被保存的模型实例。The value is an instance ofModel
class and contains updated values computed by datastore (for example, auto-generated ID).Note:
The after save hook returns the changes made to
ctx.instance
to the caller (REST client), but does not persist them to the database!
</div>
- 通过Model.updateAll对多个模型实例进行更新:
Model
- 要被保存的模型的构造函数where
- 描述哪些模型实例要被更新的where过滤器-
data
- 要被应用于更新的那部分数据Important:
你不能完全依据 “where”来判断哪些模型实例受到了影响。想想下面这个调用:
MyModel.updateAll({ color: 'yellow' }, { color: 'red' }, cb);
“after save”被触发饿,但是可能没有纪录会匹配到
{ color: 'yellow' }
.
</div>
after save钩子提供ctx.isNewInstance
属性 whenever ctx.instance
is set, with the following values:
- ture代表是创建操作
- false代表是更新操作
updateOrCreate
,prototype.save
, 和prototype.updateAttributes需要connector告知这是一个新建模型实例的操作还是一个针对已经存在的模型实例的操作。当connector提供这个信息的时候,
ctx.isNewInstance
的值是true或者false。当connector不支持isNewInstance的时候它的值是undefined。
Important:
只有某些connector支持ctx.isNewInstance。有些connector没有定义。详见
检查是否支持ctx.isNewInstance。
Examples:
MyModel.observe('after save', function(ctx, next) {
if (ctx.instance) {
console.log('Saved %s#%s', ctx.Model.modelName, ctx.instance.id);
} else {
console.log('Updated %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
}
next();
});
before delete
before delete钩子在模型被从一个数据源中删除之前触发,在下面的命令执行后会触发这个钩子:
- destroyAll() (
deleteAll()
) - destroyById() (
deleteById()
) - prototype.destroy() (
prototype.delete()
)
Important:
before delete钩子不会收到一个要被删除的实例的id列表,因为有些数据库不提供这样的信息。然而,当你只是删除一个模型实例的时候,这个钩子会有一个ctx.instance,它的值就是被删除的那个模型实例。
上下文属性
Model
- 模型的构造函数where
- 描述哪些实例要被删除的where过滤器
例子:
MyModel.observe('before delete', function(ctx, next) {
console.log('Going to delete %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
next();
});
可以基于某些条件拒绝删除,通过调用有error参数的next()可以放弃删除操作。例如:
if (subscriptions.length > 0) {
//Stop the deletion of this Client
var err = new Error("Client has an active subscription, cannot delete");
err.statusCode = 400;
console.log(err.toString());
next(err);
} else {
next();
}
after delete
Important:
after delete钩子不会收到一个要被删除的实例的id列表,因为有些数据库不提供这样的信息。然而,当你只是删除一个模型实例的时候,这个钩子会有一个ctx.instance,它的值就是被删除的那个模型实例。
after delete钩子在模型被从一个数据源中删除之后触发,在下面的命令执行后会触发这个钩子:
- destroyAll() (
deleteAll()
) - destroyById() (
deleteById()
) - prototype.destroy() (
prototype.delete()
)
上下文属性
Model
- 模型的构造函数where
- 描述哪些实例要被删除的where过滤器
例子:
MyModel.observe('after delete', function(ctx, next) {
console.log('Deleted %s matching %j',
ctx.Model.pluralModelName,
ctx.where);
next();
});
loaded
这个钩子被下面的PersistedModel所触发:
- find()
- findOne()
- findById()
- exists()
- count()
- create()
- upsert() (same as
updateOrCreate()
) - findOrCreate()*
- prototype.save()
- prototype.updateAttributes()
Important:
By default, create
and updateAttributes
do not apply database updates to the model instance returned to the callback, therefore any changes made by “loaded” hooks are discarded. To change this behavior, set a per-model option updateOnLoad: true
.
这个钩子在connector获取数据之后,通过这些数据创建模型实例之前发生。因此可以使用这个钩子解密数据。注意:这个钩子是被真正的数据库数据所触发的,不是一个模型实例。
上下文属性
data
- data被connector返回(是从数据中加载到的)
persist
持续化数据到数据库的方法都会触发这个钩子,方法如下:
- create()
- upsert() (same as
updateOrCreate()
) - findOrCreate()*
- prototype.save()
- prototype.updateAttributes()
- updateAll()
不要把这个钩子和before save钩子混淆了:
- before save – 使用这个钩子观察(操作)模型实例 (例如,当设置了国家代码但是没有设置国家名,那么添加业务逻辑去设置国家名,也就是在这你能修改模型的任何字段)。
- persist – 使用这个钩子观察(操作)即将要持续化到数据库里面去的数据(例如,在一个值被持续化到数据库的时候加密它,也就是你这能操作这些即将持续化去数据库的数据,不能添加任何其他属性及其值)。
During create
the updates applied through persist
hook are reflected into the database, but the same updates are NOT reflected in the instance
object obtained in callback of create
.
Secondly, for connectors implementing atomic findOrCreate
, a new instance of the object is created every time, even if an existing record is later found in the database. So:
ctx.data.id
和ctx.currentInstance.id
都设置成一个new ID.ctx.isNewInstance
的值为true
上下文属性
data
- 即将要被发送至connector的数据(即将要保存到数据里面去的数据)currentInstance
- 受影响的模型实例isNewInstance
- 如下.
ctx.isNewInstance的值如下:
- True表示是创建操作
- False表示是更新操作
- 操作为updateOrCreate, prototype.save, prototype.updateAttributes, 和 updateAll operations时值为undefined
afterInitialize钩子
Important:
afterInitialize严格来说不是一个操作钩子。事实上它是唯一一个没有过时的
模型钩子。
它是一个同步方法没有对应的callback函数:你不需要在你的逻辑代码之后调用next()。
这个钩子在模型被初始化之后触发。
例子
/common/models/coffee-shop.js
...
CoffeeShop.afterInitialize = function() {
//your logic goes here
};
...
大多数的操作会在真正是想一个操作之前初始化一个模型,但是也有少数的操作不会触发这个初始化事件,例如HTTP请求exists,count,批量更新。
迁移指南
下面的表格列出了哪个新的钩子用来替代过时了的模型钩子:
Model hook | Operation hook to use instead |
---|---|
beforeValidate |
before save |
afterValidate | persist |
beforeCreate | before save |
afterCreate | after save |
beforeSave | before save |
afterSave | after save |
beforeUpdate | before save |
afterUpdate | after save |
beforeDestroy | before delete |
afterDestroy | after delete |