doc/book/en/development/devrepo/hooks.rst
branchstable
changeset 5220 42f854b6083d
parent 5202 4a77da652759
child 5221 b851558456bb
equal deleted inserted replaced
5219:35d44017c72b 5220:42f854b6083d
    34 .. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
    34 .. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
    35 
    35 
    36 Data hooks can serve the following purposes:
    36 Data hooks can serve the following purposes:
    37 
    37 
    38 * enforcing constraints that the static schema cannot express
    38 * enforcing constraints that the static schema cannot express
    39   (spanning several entities/relations, specific value ranges, exotic
    39   (spanning several entities/relations, exotic value ranges and
    40   cardinalities, etc.)
    40   cardinalities, etc.)
    41 
    41 
    42 * implement computed attributes (an example could be the maintenance
    42 * implement computed attributes
    43   of a relation representing the transitive closure of another relation)
       
    44 
    43 
    45 Operations are Hook-like objects that are created by Hooks and
    44 Operations are Hook-like objects that are created by Hooks and
    46 scheduled to happen just before (or after) the `commit` event. Hooks
    45 scheduled to happen just before (or after) the `commit` event. Hooks
    47 being fired immediately on data operations, it is sometime necessary
    46 being fired immediately on data operations, it is sometime necessary
    48 to delay the actual work down to a time where all other Hooks have run
    47 to delay the actual work down to a time where all other Hooks have
    49 and the application state converges towards consistency. Also while
    48 run, for instance a validation check which needs that all relations be
    50 the order of execution of Hooks is data dependant (and thus hard to
    49 already set on an entity. Also while the order of execution of Hooks
    51 predict), it is possible to force an order on Operations.
    50 is data dependant (and thus hard to predict), it is possible to force
       
    51 an order on Operations.
    52 
    52 
    53 Operations are subclasses of the Operation class in `server/hook.py`,
    53 Operations are subclasses of the Operation class in `server/hook.py`,
    54 implementing `precommit_event` and other standard methods (wholly
    54 implementing `precommit_event` and other standard methods (wholly
    55 described later in this chapter).
    55 described later in this chapter).
    56 
    56 
    88 * before_add_relation
    88 * before_add_relation
    89 
    89 
    90 * before_delete_relation
    90 * before_delete_relation
    91 
    91 
    92 This is an occasion to remind us that relations support the add/delete
    92 This is an occasion to remind us that relations support the add/delete
    93 operation, but no delete.
    93 operation, but no update.
    94 
    94 
    95 Non data events also exist. These are called SYSTEM HOOKS.
    95 Non data events also exist. These are called SYSTEM HOOKS.
    96 
    96 
    97 * server_startup
    97 * server_startup
    98 
    98 
   107 * session_open
   107 * session_open
   108 
   108 
   109 * session_close
   109 * session_close
   110 
   110 
   111 
   111 
   112 Using Hooks
   112 Using dataflow Hooks
   113 -----------
   113 --------------------
       
   114 
       
   115 XXX blabla
       
   116 
       
   117 Validation Errors
       
   118 ~~~~~~~~~~~~~~~~~
       
   119 
       
   120 When a condition is not met in a Hook/Operation, it must raise a
       
   121 `ValidationError`. Raising anything but a (subclass of)
       
   122 ValidationError is a programming error. Raising a ValidationError
       
   123 entails aborting the current transaction.
       
   124 
       
   125 The ValidationError exception is used to convey enough information up
       
   126 to the user interface. Hence its constructor is different from the
       
   127 default Exception constructor. It accepts, positionally:
       
   128 
       
   129 * an entity eid,
       
   130 
       
   131 * a dict whose keys represent attribute (or relation) names and values
       
   132   an end-user facing message (hence properly translated) relating the
       
   133   problem.
       
   134 
   114 
   135 
   115 We will use a very simple example to show hooks usage. Let us start
   136 We will use a very simple example to show hooks usage. Let us start
   116 with the following schema.
   137 with the following schema.
   117 
   138 
   118 .. sourcecode:: python
   139 .. sourcecode:: python
   148 Hooks being AppObjects like views, they have a __regid__ and a
   169 Hooks being AppObjects like views, they have a __regid__ and a
   149 __select__ class attribute. The base __select__ is augmented with an
   170 __select__ class attribute. The base __select__ is augmented with an
   150 `implements` selector matching the desired entity type. The `events`
   171 `implements` selector matching the desired entity type. The `events`
   151 tuple is used by the Hook.__select__ base selector to dispatch the
   172 tuple is used by the Hook.__select__ base selector to dispatch the
   152 hook on the right events. In an entity hook, it is possible to
   173 hook on the right events. In an entity hook, it is possible to
   153 dispatch on any entity event at once if needed.
   174 dispatch on any entity event (e.g. 'before_add_entity',
   154 
   175 'before_update_entity') at once if needed.
   155 Like all appobjects, hooks have the self._cw attribute which
   176 
   156 represents the current session. In entity hooks, a self.entity
   177 Like all appobjects, hooks have the `self._cw` attribute which
       
   178 represents the current session. In entity hooks, a `self.entity`
   157 attribute is also present.
   179 attribute is also present.
   158 
   180 
   159 When a condition is not met in a Hook, it must raise a
       
   160 ValidationError. Raising anything but a (subclass of) ValidationError
       
   161 is a programming error.
       
   162 
       
   163 The ValidationError exception is used to convey enough information up
       
   164 to the user interface. Hence its constructor is different from the
       
   165 default Exception constructor.It accepts, positionally:
       
   166 
       
   167 * an entity eid,
       
   168 
       
   169 * a dict whose keys represent attributes and values a message relating
       
   170   the problem; such a message will be presented to the end-users;
       
   171   hence it must be properly translated.
       
   172 
   181 
   173 A relation hook
   182 A relation hook
   174 ~~~~~~~~~~~~~~~
   183 ~~~~~~~~~~~~~~~
   175 
   184 
   176 Let us add another entity type with a relation to person (in
   185 Let us add another entity type with a relation to person (in
   203 
   212 
   204 The essential difference with respect to an entity hook is that there
   213 The essential difference with respect to an entity hook is that there
   205 is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes
   214 is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes
   206 which represent the subject and object eid of the relation.
   215 which represent the subject and object eid of the relation.
   207 
   216 
   208 
   217 Using Operations
   209 # XXX talk about
   218 ----------------
   210 
   219 
   211 dict access to entities in before_[add|update]
   220 Let's augment our example with a new `subsidiary_of` relation on Company.
   212 set_operation
   221 
       
   222 .. sourcecode:: python
       
   223 
       
   224    class Company(EntityType):
       
   225         name = String(required=True)
       
   226         boss = SubjectRelation('Person', cardinality='1*')
       
   227         subsidiary_of = SubjectRelation('Company', cardinality='*?')
       
   228 
       
   229 Base example
       
   230 ~~~~~~~~~~~~
       
   231 
       
   232 We would like to check that there is no cycle by the `subsidiary_of`
       
   233 relation. This is best achieved in an Operation since all relations
       
   234 are likely to be set at commit time.
       
   235 
       
   236 .. sourcecode:: python
       
   237 
       
   238     def check_cycle(self, session, eid, rtype, role='subject'):
       
   239         parents = set([eid])
       
   240         parent = session.entity_from_eid(eid)
       
   241         while parent.related(rtype, role):
       
   242             parent = parent.related(rtype, role)[0]
       
   243             if parent.eid in parents:
       
   244                 msg = session._('detected %s cycle' % rtype)
       
   245                 raise ValidationError(eid, {rtype: msg})
       
   246             parents.add(parent.eid)
       
   247 
       
   248     class CheckSubsidiaryCycleOp(Operation):
       
   249 
       
   250         def precommit_event(self):
       
   251             check_cycle(self.session, self.eidto, 'subsidiary_of')
       
   252 
       
   253 
       
   254     class CheckSubsidiaryCycleHook(Hook):
       
   255         __regid__ = 'check_no_subsidiary_cycle'
       
   256         events = ('after_add_relation',)
       
   257         __select__ = Hook.__select__ & match_rtype('subsidiary_of')
       
   258 
       
   259         def __call__(self):
       
   260             CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
       
   261 
       
   262 The operation is instantiated in the Hook.__call__ method.
       
   263 
       
   264 An operation always takes a session object as first argument
       
   265 (accessible as `.session` from the operation instance), and optionally
       
   266 all keyword arguments needed by the operation. These keyword arguments
       
   267 will be accessible as attributes from the operation instance.
       
   268 
       
   269 Like in Hooks, ValidationError can be raised in Operations. Other
       
   270 exceptions are programming errors.
       
   271 
       
   272 Notice how our hook will instantiate an operation each time the Hook
       
   273 is called, i.e. each time the `subsidiary_of` relation is set.
       
   274 
       
   275 Using set_operation
       
   276 ~~~~~~~~~~~~~~~~~~~
       
   277 
       
   278 There is an alternative method to schedule an Operation from a Hook,
       
   279 using the `set_operation` function.
       
   280 
       
   281 .. sourcecode:: python
       
   282 
       
   283    class CheckSubsidiaryCycleHook(Hook):
       
   284        __regid__ = 'check_no_subsidiary_cycle'
       
   285        events = ('after_add_relation',)
       
   286        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
       
   287 
       
   288        def __call__(self):
       
   289            set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto,
       
   290                          CheckCycleOp, rtype=self.rtype)
       
   291 
       
   292    class CheckSubsidiaryCycleOp(Operation):
       
   293 
       
   294        def precommit_event(self):
       
   295            for eid in self._cw.transaction_data['subsidiary_cycle_detection']:
       
   296                check_cycle(self.session, eid, self.rtype)
       
   297 
       
   298 Here, we call set_operation with a session object, a specially forged
       
   299 key, a value that is the actual payload of an individual operation (in
       
   300 our case, the object of the subsidiary_of relation) , the class of the
       
   301 Operation, and more optional parameters to give to the operation (here
       
   302 the rtype which do not vary accross operations).
       
   303 
       
   304 The body of the operation must then iterate over the values that have
       
   305 been mapped in the transaction_data dictionary to the forged key.
       
   306 
       
   307 This mechanism is especially useful on two occasions (not shown in our
       
   308 example):
       
   309 
       
   310 * massive data import (reduced memory consumption within a large
       
   311   transaction)
       
   312 
       
   313 * when several hooks need to instantiate the same operation (e.g. an
       
   314   entity and a relation hook).
       
   315 
       
   316 Operation: a small API overview
       
   317 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   318 
       
   319 .. autoclass:: cubicweb.server.hook.Operation
       
   320 .. autoclass:: cubicweb.server.hook.LateOperation
       
   321 .. autofunction:: cubicweb.server.hook.set_operation
       
   322 
       
   323 Hooks writing rules
       
   324 -------------------
       
   325 
       
   326 Remainder
       
   327 ~~~~~~~~~
       
   328 
       
   329 Never, ever use the `entity.foo = 42` notation to update an entity. It
       
   330 will not work.
       
   331 
       
   332 How to choose between a before and an after event ?
       
   333 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   334 
       
   335 Before hooks give you access to the old attribute (or relation)
       
   336 values. By definition the database is not yet updated in a before
       
   337 hook.
       
   338 
       
   339 To access old and new values in an before_update_entity hook, one can
       
   340 use the `server.hook.entity_oldnewvalue` function which returns a
       
   341 tuple of the old and new values. This function takes an entity and an
       
   342 attribute name as parameters.
       
   343 
       
   344 In a 'before_add|update_entity' hook the self.entity contains the new
       
   345 values. One is allowed to further modify them before database
       
   346 operations, using the dictionary notation.
       
   347 
       
   348 .. sourcecode:: python
       
   349 
       
   350    self.entity['age'] = 42
       
   351 
       
   352 This is because using self.entity.set_attributes(age=42) will
       
   353 immediately update the database (which does not make sense in a
       
   354 pre-database hook), and will trigger any existing
       
   355 before_add|update_entity hook, thus leading to infinite hook loops or
       
   356 such awkward situations.
       
   357 
       
   358 Beyond these specific cases, updating an entity attribute or relation
       
   359 must *always* be done using `set_attributes` and `set_relations`
       
   360 methods.
       
   361 
       
   362 (Of course, ValidationError will always abort the current transaction,
       
   363 whetever the event).
       
   364 
       
   365 Peculiarities of inlined relations
       
   366 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   367 
       
   368 Some relations are defined in the schema as `inlined` (see
       
   369 :ref:`RelationType` for details). In this case, they are inserted in
       
   370 the database at the same time as entity attributes.
       
   371 
       
   372 Hence in the case of before_add_relation, such relations already exist
       
   373 in the database.
       
   374 
       
   375 Edited attributes
       
   376 ~~~~~~~~~~~~~~~~~
       
   377 
       
   378 On udpates, it is possible to ask the `entity.edited_attributes`
       
   379 variable whether one attribute has been updated.
       
   380 
       
   381 .. sourcecode:: python
       
   382 
       
   383   if 'age' not in entity.edited_attribute:
       
   384       return
       
   385 
       
   386 Deleted in transaction
       
   387 ~~~~~~~~~~~~~~~~~~~~~~
       
   388 
       
   389 The session object has a deleted_in_transaction method, which can help
       
   390 writing deletion Hooks.
       
   391 
       
   392 .. sourcecode:: python
       
   393 
       
   394    if self._cw.deleted_in_transaction(self.eidto):
       
   395       return
       
   396 
       
   397 Given this predicate, we can avoid scheduling an operation.
       
   398 
       
   399 Disabling hooks
       
   400 ~~~~~~~~~~~~~~~
       
   401 
       
   402 It is sometimes convenient to disable some hooks. For instance to
       
   403 avoid infinite Hook loops. One uses the `hooks_control` context
       
   404 manager.
       
   405 
       
   406 This can be controlled more finely through the `category` Hook class
       
   407 attribute.
       
   408 
       
   409 .. sourcecode:: python
       
   410 
       
   411    with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, <category>):
       
   412        # ... do stuff
       
   413 
       
   414 .. autoclass:: cubicweb.server.session.hooks_control