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.eidfrom 和 self.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 时间,关系将已经存在于数据库中(否则不是这样)。