@@ -309,6 +309,174 @@ limited for m2m fields. Also see below under optimization examples.
309
309
as much as possible.
310
310
311
311
312
+ Multi Table Inheritance
313
+ -----------------------
314
+
315
+ .. |br | raw :: html
316
+
317
+ <br />
318
+
319
+
320
+ Multi table inheritance works with computed fields with some restrictions you have to be aware of.
321
+ The following requires basic knowledge about multi table inheritance in Django and its similarities
322
+ to o2o relations on accessor level (also see official Django docs).
323
+
324
+ Neighboring Models
325
+ ^^^^^^^^^^^^^^^^^^
326
+
327
+ Let's illustrate dealing with updates from neighboring models with an example.
328
+ (Note: The example can also be found in `example.test_full ` under `tests/test_multi_table_example.py `)
329
+
330
+ .. code-block :: python
331
+
332
+ from django.db import models
333
+ from computedfields.models import ComputedFieldsModel, computed
334
+
335
+ class User (ComputedFieldsModel ):
336
+ forname = models.CharField(max_length = 32 )
337
+ surname = models.CharField(max_length = 32 )
338
+
339
+ @computed (models.CharField(max_length = 64 ), depends = [
340
+ [' self' , [' forname' , ' surname' ]]
341
+ ])
342
+ def fullname (self ):
343
+ return ' {} , {} ' .format(self .surname, self .forname)
344
+
345
+ class EmailUser (User ):
346
+ email = models.CharField(max_length = 32 )
347
+
348
+ @computed (models.CharField(max_length = 128 ), depends = [
349
+ [' self' , [' email' , ' fullname' ]],
350
+ [' user_ptr' , [' fullname' ]] # trigger updates from User type as well
351
+ ])
352
+ def email_contact (self ):
353
+ return ' {} <{} >' .format(self .fullname, self .email)
354
+
355
+ class Work (ComputedFieldsModel ):
356
+ subject = models.CharField(max_length = 32 )
357
+ user = models.ForeignKey(User, on_delete = models.CASCADE )
358
+
359
+ @computed (models.CharField(max_length = 64 ), depends = [
360
+ [' self' , [' subject' ]],
361
+ [' user' , [' fullname' ]],
362
+ [' user.emailuser' , [' fullname' ]] # trigger updates from EmailUser type as well
363
+ ])
364
+ def descriptive_assigment (self ):
365
+ return ' "{} " is assigned to "{} "' .format(self .subject, self .user.fullname)
366
+
367
+ In the example there are two surprising `depends ` rules:
368
+
369
+ 1. ``['user_ptr', ['fullname']] `` on ``EmailUser.email_contact ``
370
+ 2. ``['user.emailuser', ['fullname']] `` on ``Work.descriptive_assigment ``
371
+
372
+ Both are needed to expand the update rules in a way, that parent or derived models are also respected
373
+ for the field updates. While the first rule extends updates to the parent model `User `
374
+ (ascending in the model inheritance), the second one expands updates to a descendant.
375
+
376
+ *Why do I have to create those counter-intuitive rules? *
377
+
378
+ Currently the resolver does not expand on multi table inheritance automatically.
379
+ Furthermore it might not be wanted in all circumstances, that parent or derived models
380
+ trigger updates on other ends. Thus it has to be set explicitly (might change with future versions,
381
+ if highly demanded).
382
+
383
+ *When do I have to place those additional rules? *
384
+
385
+ In general the resolver updates computed fields only from model-field associations,
386
+ that were explicitly given in `depends ` rules. Therefore it will not catch changes on
387
+ parent or derived models.
388
+
389
+ In the example above without the first rule any changes to an instance of `User ` will not
390
+ trigger a recalculation of ``EmailUser.email_contact ``. This is most likely unwanted behavior for this
391
+ particular example, as anyone would expect, that changing parts of the name should update the email contact
392
+ information here.
393
+
394
+ Without the second rule, ``Work.descriptive_assigment `` will not be updated from changes of an
395
+ `EmailUser ` instance, which again is probably unwanted, as anyone would expect `EmailUser ` to behave
396
+ like a `User ` instance here.
397
+
398
+ *How to derive those rules? *
399
+
400
+ To understand, how to construct those additional rules, we have to look first at the rules,
401
+ they are derived from:
402
+
403
+ - first one is derived from ``['self', ['email', 'fullname']] ``
404
+ - second one is derived from ``['user', ['fullname']] ``
405
+
406
+ **Step 1 - check, whether the path ends on multi table model **
407
+
408
+ Looking at the relation paths (left side of the rules), both have something in common - they both end
409
+ on a model with multi table inheritance (`self ` in 1. pointing to `EmailUser ` model,
410
+ `user ` in 2. pointing to `User ` model). So whenever a relation ends on a multi table model,
411
+ there is a high chance, that you might want to apply additional rules for neighboring models.
412
+
413
+ **Step 2 - derive new relational path from model inheritance **
414
+
415
+ Next question is, whether you want to expand ascending or descending or both in the model inheritance:
416
+
417
+ - For ascending expansion append the o2o field name denoting the parent model.
418
+ - For descending expansion append reverse o2o relation name pointing to the derived model.
419
+
420
+ (Note: If a relation expands on `self ` entries, `self ` has to removed from the path.)
421
+
422
+ At this point it is important to know, how Django denotes multi table relations on model field level.
423
+ By default the o2o field is placed on the descendent model as `modelname_ptr `, while the reverse relation
424
+ gets the child modelname on the ancestor model as `modelname ` (all lowercase).
425
+
426
+ In the example above ascending from `EmailUser ` to `User ` creates a relational path `user_ptr `,
427
+ while descending from `User ` to `EmailUser ` needs a relational path of `emailuser `.
428
+
429
+ **Step 3 - apply fieldnames on right side **
430
+
431
+ For descending rules you can just copy over the field names on the right side. For the descent from
432
+ `User ` to `EmailUser ` we finally get:
433
+
434
+ - ``['user.emailuser', ['fullname']] ``
435
+
436
+ to be added to `depends ` on ``Work.descriptive_assigment ``.
437
+
438
+ For ascending rules you should be careful not to copy over field names on the right side, that are defined on
439
+ descendent models. After removing `email ` from the field names we finally get for the ascent from `EmailUser `
440
+ to `User `:
441
+
442
+ - ``['user_ptr', ['fullname']] ``
443
+
444
+ to be added to `depends ` on ``EmailUser.email_contact ``.
445
+
446
+ Up-Pulling Fields
447
+ ^^^^^^^^^^^^^^^^^
448
+
449
+ The resolver has a special rule for handling dependencies to fields on derived multi table models.
450
+ Therefore it is possible to create a computed field on the parent model, that conditionally
451
+ updates from different descendent model fields, example:
452
+
453
+ .. code-block :: python
454
+
455
+ class Base (ComputedFieldsModel ):
456
+ @computed (models.CharField(max_length = 32 ), depends = [
457
+ [' a' , [' type' ]], # pull type field from A descendant
458
+ [' b' , [' type' ]], # pull type field from B descendant
459
+ [' b.c' , [' subtype' ]] # pull subtype field from C descendant
460
+ ])
461
+ def type (self ):
462
+ if hasattr (self , ' a' ):
463
+ return a.type
464
+ if hasattr (self , ' b' ):
465
+ if hasattr (self , ' b' ):
466
+ return self .b.c.subtype
467
+ return b.type
468
+ return ' '
469
+
470
+ class A (Base ):
471
+ type = models.CharField(max_length = 32 , default = ' a' )
472
+ class B (Base ):
473
+ type = models.CharField(max_length = 32 , default = ' b' )
474
+ class C (B ):
475
+ subtype = models.CharField(max_length = 32 , default = ' sub-c' )
476
+
477
+ Note that you have to guard the attribute access yourself in the method code.
478
+
479
+
312
480
Forced Update of Computed Fields
313
481
--------------------------------
314
482
0 commit comments