Opened 18 months ago

Last modified 18 months ago

#28822 new New feature

Add DBCalculatedField to model to annotate models automatically — at Initial Version

Reported by: Ilya Owned by: nobody
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords:
Cc: Ryan Hiebert, Shai Berger Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Models are ultimate source of business knowledge. We must encourage people to use them for it instead of spread logic over views, managers
and "utility" methods.

We have customer model and there is knowledge: People are allowed to drink when they are 18+

So, we implement this logic in model to use it in templates and other tools:

class Customer(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    age = models.IntegerField()

    @property
    def allowed_to_drink(self):
        return self.age >= 18

Cool, but what if want to query database for only people who are allowed to drink?

models.Customer.objects.filter(age__gt=18)

We now have this logic written twice in two different places.

One can use managers with annotate:

class DrinkersManager(models.Manager):
    def get_queryset(self):
        return models.query.QuerySet(self.model, using=self._db).annotate(
            allowed_to_drink=ExpressionWrapper(Q(age__gt=18), models.BooleanField()))


class Customer(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    age = models.IntegerField()

    objects = DrinkersManager()

We now can do both: use it as .filter(allowed_to_drink=False) and use it for templates: customer.allowed_to_drink.

But why do we define all "physical" fields in model and "calculated" field as keyword argument (!) inside of different (!!) class?
Why do we need class at all?

What we suggest:

cl

ass Customer(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    age = models.IntegerField()
    allowed_to_drink = models.DBCalculatedField(Q(age__gt=18) models.BooleanField())


You just add this field and all queries to this model are annotated automatically, so you will have model field and query field.

I believe this syntax is much more clear and we have consensus about in on group:
[https://groups.google.com/forum/#!topic/django-developers/ADSuUUuZp3Q
]
We may also have

    full_name = models.DBCalculatedField(F('first_name') + ' ' + F('last_name'), models.CharField()) 

And for local calculation (may be used by people who want to use this logic without of db or before saving)

    allowed_to_drink = models.DBCalculatedField(Q(age__gt=18) models.BooleanField(), local=lambda c: c.age > 18) 

# or 
@allowed_to_drink.local
def allowed_to_drink_local(self):
    return self.age > 18

Since knowledge is expressed by Django expression language it is possible to generate "local calculation" automatically
(you just need to evalute this simple language), but many people in group believe it is not safe since DB may use different logic which may be
hard to mimic (expecially in database-agnostic way). Tool for "automatic local calculation" may be created as external lib, not part of Django itself (one tool for each database probably)

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top