Page Contents

Related articles:

Overview

操作钩子不和一个具体分方法挂钩, 而是被所有指定创建,读取,更新,删除的方法所触发。 这些方法都是继承自PersistedModel的方法。 使用操作钩子你可以拦截CRUD操作。

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

这个属性用来区分是创建操作还是更新操作。

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 --- ---

prototype.delete

--- --- --- 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
find
findOne 
findById  
exists
count 

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可以修改查询,例如添加额外的查询条件。

上下文对象的属性

  • 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钩子在模型验证函数前被触发。

取决于不同的方法触发before save钩子,上下文会有下面不同的属性:

  • 所有针对单个模型的save
    • Model - 哪个模型被保存
    • instance - 哪个模型实例被保存
  • 可能对多个模型实例进行更新的操作
    • Model - 哪个模型被保存
    • where - 描述哪些实例会受到影响的where过滤器
    • data - 应用于更新的数据
    • currentInstance - 受影响的实例, 更多信息见Triggering with prototype.updateAttributes

ctx.isNewInstance

当ctx.instance是存的时候,before save钩子提供了ctx.isNewInstance,它的值如下:

  • 为true 表示是创建操作
  • 为false 表示是更新操作
  • 如果是updateOrCreateprototype.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:

  1. PersistedModel.updateAll 更新匹配查询条件的多个实例。LoopBack不需要从数据里面加载他们的数据, it’s up to the database to find these instances and apply necessary changes. 
  2. PersistedModel.updateAttributes 执行部分更新,只修改模型的部分属性。LoopBack会有一个模型实例,它需要知道哪些模型属性要被修改并且持续化到数据库里面去。Passing the operation payload in ctx.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 of Model class and contains updated values computed by datastore (for example, auto-generated ID).

</div>

  • 通过Model.updateAll对多个模型实例进行更新:
    • Model - 要被保存的模型的构造函数
    • where - 描述哪些模型实例要被更新的where过滤器
    • data- 要被应用于更新的那部分数据

</div>

after save钩子提供ctx.isNewInstance属性 whenever ctx.instance is set, with the following values:

  • ture代表是创建操作
  • false代表是更新操作
  • updateOrCreateprototype.save, 和 prototype.updateAttributes需要connector告知这是一个新建模型实例的操作还是一个针对已经存在的模型实例的操作。当connector提供这个信息的时候,ctx.isNewInstance的值是true或者false。当connector不支持isNewInstance的时候它的值是undefined。

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钩子在模型被从一个数据源中删除之前触发,在下面的命令执行后会触发这个钩子:

上下文属性

  • 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

after 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所触发:

这个钩子在connector获取数据之后,通过这些数据创建模型实例之前发生。因此可以使用这个钩子解密数据。注意:这个钩子是被真正的数据库数据所触发的,不是一个模型实例。

上下文属性

  • data - data被connector返回(是从数据中加载到的)

persist

持续化数据到数据库的方法都会触发这个钩子,方法如下:

不要把这个钩子和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:

上下文属性

  • data - 即将要被发送至connector的数据(即将要保存到数据里面去的数据)
  • currentInstance - 受影响的模型实例
  • isNewInstance - 如下.

ctx.isNewInstance的值如下:

  • True表示是创建操作
  • False表示是更新操作
  • 操作为updateOrCreate, prototype.save, prototype.updateAttributes, 和 updateAll operations时值为undefined

afterInitialize钩子

这个钩子在模型被初始化之后触发。

例子

/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