6.3. 挂钩和操作

6.3.1. 使用数据流挂钩的示例

我们将使用一个非常简单的例子来展示钩子的用法。让我们从下面的模式开始。

class Person(EntityType):
    age = Int(required=True)

我们想在一个人的年龄上添加一个范围限制。让我们写一个钩子(假设山药不能处理这个问题,这是错误的)。应放入 mycube/hooks.py .如果这个文件增长太多,我们可以很容易地 mycube/hooks/... package 包含各种模块中的挂钩。

from cubicweb import ValidationError
from cubicweb.predicates import is_instance
from cubicweb.server.hook import Hook

class PersonAgeRange(Hook):
     __regid__ = 'person_age_range'
     __select__ = Hook.__select__ & is_instance('Person')
     events = ('before_add_entity', 'before_update_entity')

     def __call__(self):
         if 'age' in self.entity.cw_edited:
             if 0 <= self.entity.age <= 120:
                return
             msg = self._cw._('age must be between 0 and 120')
             raise ValidationError(self.entity.eid, {'age': msg})

在我们的例子中,基础 __select__ 增加了一个 is_instance 与所需实体类型匹配的选择器。

这个 events tuple用于指定在添加或更新实体之前应该调用钩子。

然后在钩子里 __call__ 方法,我们:

  • 检查是否编辑了“年龄”属性

  • 如果是,检查值是否在范围内

  • 如果没有,请正确引发验证错误

现在让我们用一个新的 Company 实体类型与 Person (在“mycube/schema.py”中)。

class Company(EntityType):
     name = String(required=True)
     boss = SubjectRelation('Person', cardinality='1*')
     subsidiary_of = SubjectRelation('Company', cardinality='*?')

我们想限制公司老板的最低(法定)年龄。让我们为这个写一个钩子,当 boss 关系已经建立(仍然假设我们不能在模式中指定这种类型的东西)。

class CompanyBossLegalAge(Hook):
     __regid__ = 'company_boss_legal_age'
     __select__ = Hook.__select__ & match_rtype('boss')
     events = ('before_add_relation',)

     def __call__(self):
         boss = self._cw.entity_from_eid(self.eidto)
         if boss.age < 18:
             msg = self._cw._('the minimum age for a boss is 18')
             raise ValidationError(self.eidfrom, {'boss': msg})

注解

我们使用 match_rtype 选择以选择正确的关系类型。

实体挂钩的本质区别在于没有自我实体,但是 self.eidfromself.eidto 表示主题和对象的挂钩属性 eid 关系。

假设我们想检查一下 subsidiary_of 关系。这在操作中最好实现,因为所有关系都可能在提交时设置。

from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype

def check_cycle(session, eid, rtype, role='subject'):
    parents = set([eid])
    parent = session.entity_from_eid(eid)
    while parent.related(rtype, role):
        parent = parent.related(rtype, role)[0]
        if parent.eid in parents:
            msg = session._('detected %s cycle' % rtype)
            raise ValidationError(eid, {rtype: msg})
        parents.add(parent.eid)


class CheckSubsidiaryCycleOp(Operation):

    def precommit_event(self):
        check_cycle(self.session, self.eidto, 'subsidiary_of')


class CheckSubsidiaryCycleHook(Hook):
    __regid__ = 'check_no_subsidiary_cycle'
    __select__ = Hook.__select__ & match_rtype('subsidiary_of')
    events = ('after_add_relation',)

    def __call__(self):
        CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)

就像钩子一样, ValidationError 可以在操作中提升。其他的例外通常是编程错误。

在上面的示例中,我们的钩子将在每次调用钩子时实例化一个操作,即每次调用钩子时 subsidiary_of 关系已设置。有另一种方法可以从钩子调度操作,使用 get_instance() 类方法。

class CheckSubsidiaryCycleHook(Hook):
    __regid__ = 'check_no_subsidiary_cycle'
    events = ('after_add_relation',)
    __select__ = Hook.__select__ & match_rtype('subsidiary_of')

    def __call__(self):
        CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)

class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):

    def precommit_event(self):
        for eid in self.get_data():
            check_cycle(self.session, eid, self.rtype)

这里,我们打电话来 add_data() 这样我们就可以简单地累积实体的EID,在一个 CheckSubsidiaryCycleOp 操作。值存储在与“check no_subsidiary_cycle”事务数据键关联的集合中。集初始化和操作创建由 add_data() .

在高级教程一章中可以找到一个更现实的例子。 步骤2:钩子中的安全传播 .

6.3.2. 实例间通信

如果您的应用程序由多个实例组成,您可能需要一些方法来在它们之间进行通信。CubicWeb提供了一种发布/订阅机制,使用 ØMQ. 为了使用它,请使用 add_subscription()repo.app_instances_bus 对象。这个 callback 将得到消息(作为列表)。可以通过呼叫发送消息 publish()repo.app_instances_bus .消息的第一个元素是用于筛选和调度消息的主题。

class FooHook(hook.Hook):
    events = ('server_startup',)
    __regid__ = 'foo_startup'

    def __call__(self):
        def callback(msg):
            self.info('received message: %s', ' '.join(msg))
        self.repo.app_instances_bus.add_subscription('hello', callback)
def do_foo(self):
    actually_do_foo()
    self._cw.repo.app_instances_bus.publish(['hello', 'world'])

这个 zmq-address-pub 配置变量包含实例用于发送消息的地址,例如 tcp://*:1234 . 这个 zmq-address-sub 变量包含要侦听的地址的逗号分隔列表,例如 tcp://localhost:1234, tcp://192.168.1.1:2345 .

6.3.3. 钩子书写技巧

6.3.3.1. 提醒

你不应该使用 entity.foo = 42 更新实体的符号。它不会像您期望的那样(更新数据库)。相反,使用 cw_set() 方法或直接访问实体的 cw_edited 属性,如果要为“在添加实体之前”或“在更新实体之前”事件编写挂钩。

6.3.3.2. 如何在前后事件之间进行选择?

before_* 钩子允许您访问旧的属性(或关系)值。您还可以截取和更新编辑后的值,以防在它们到达数据库之前修改实体。

否则问题是:我应该在实际修改之前还是之后做一些事情?如果答案是“无关紧要”,请使用“之后”事件。

6.3.3.3. 验证错误

当负责维护数据模型一致性的钩子检测到错误时,它必须使用名为 ValidationError .除了A(子类) ValidationError 是编程错误。提高它意味着中止当前的交易。

此异常用于向用户界面传递足够的信息。因此,它的构造函数不同于默认的异常构造函数。它接受位置:

  • 实体ID( 不是实体本身

  • 一种字典,其键表示属性(或关系)名称,并对与问题相关的面向最终用户的消息(因此正确翻译)进行值计算。

raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
                                  'temperature': self._cw._('too hot')})

6.3.3.4. 正在检查当前事务中创建/删除的对象

在钩子中,可以使用 added_in_transaction()deleted_in_transaction() 用于检查在挂钩事务期间是否已创建或删除EID的会话对象。

这对于在添加或删除某些实体时启用或禁用某些内容很有用。

if self._cw.deleted_in_transaction(self.eidto):
   return

6.3.3.5. 内联关系的特点

模式中定义为 inlined (见 关系类型 详细信息)与实体属性同时插入到数据库中。

这可能会产生一些副作用,例如,在同一RQL查询中创建实体和设置内联关系时,然后在 before_add_relation 时间,关系将已经存在于数据库中(否则不是这样)。