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