46

I think the best way to ask this question is with some code... can I do this:

class MyModel(models.Model):    
    foo = models.CharField(max_length = 20)    
    bar = models.CharField(max_length = 20)  

    def get_foo(self):  
        if self.bar:  
            return self.bar  
        else:  
            return self.foo  

    def set_foo(self, input):  
        self.foo = input  

    foo = property(get_foo, set_foo)  

or do I have to do it like this:

class MyModel(models.Model):
    _foo = models.CharField(max_length = 20, db_column='foo')
    bar = models.CharField(max_length = 20)

    def get_foo(self):
        if self.bar:
            return self.bar
        else:
            return self._foo

    def set_foo(self, input):
        self._foo = input

    foo = property(get_foo, set_foo)

note: you can keep the column name as 'foo' in the database by passing a db_column to the model field. This is very helpful when you are working on an existing system and you don't want to have to do db migrations for no reason

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Jiaaro
  • 74,485
  • 42
  • 169
  • 190
  • 6
    I think this is a good question but you should post your 'answer' as an actual answer. http://meta.stackexchange.com/questions/17845/etiquette-for-answering-your-own-question. – Michael Bylstra Oct 24 '12 at 06:02
  • 2
    If I rename the field but keep the column name with `db_column`, it still wants to generate a migration to remove the field and add a new one when I run `makemigrations`. I can delete this migration, but it will always keep re-appearing whenever I run `makemigrations` in the future. Any way to prevent this? – morningstar Nov 07 '17 at 16:54
  • In essence, this is a duplicate of [Django empty field fallback](https://stackoverflow.com/q/1342679). – djvg Dec 07 '18 at 11:22
  • The [filter by property](https://stackoverflow.com/q/1205375) question is also similar, although a bit more generic. – djvg Dec 07 '18 at 20:09

4 Answers4

26

A model field is already property, so I would say you have to do it the second way to avoid a name clash.

When you define foo = property(..) it actually overrides the foo = models.. line, so that field will no longer be accessible.

You will need to use a different name for the property and the field. In fact, if you do it the way you have it in example #1 you will get an infinite loop when you try and access the property as it now tries to return itself.

EDIT: Perhaps you should also consider not using _foo as a field name, but rather foo, and then define another name for your property because properties cannot be used in QuerySet, so you'll need to use the actual field names when you do a filter for example.

Daniel Holmes
  • 1,952
  • 2
  • 17
  • 28
Andre Miller
  • 15,255
  • 6
  • 55
  • 53
  • 2
    regarding the edit, that is good advice - I am maintaining an existing system and because the name of the field must be changed (or of the property) we abandoned this approach. The technique might turn out to be useful in the future though :) – Jiaaro Sep 23 '09 at 13:52
  • wasn't sure if it would be accessible via closure or something like that - admittedly, I do not know much about closures – Jiaaro Dec 08 '09 at 19:41
  • 2
    I have provided the case for '__foo' below. Using foo as a Field and another_foo as wrapping property will expose foo for direct manipulation. Although a developer in python as well as in X language should know what he is doing - there is a reason hidden variables are accepted to be a part of the language. – Yauhen Yakimovich Sep 10 '12 at 20:15
20

As mentioned, a correct alternative to implementing your own django.db.models.Field class, one should use the db_column argument and a custom (or hidden) class attribute. I am just rewriting the code in the edit by @Jiaaro following more strict conventions for OOP in python (e.g. if _foo should be actually hidden):

class MyModel(models.Model):
    __foo = models.CharField(max_length = 20, db_column='foo')
    bar = models.CharField(max_length = 20)

    @property
    def foo(self):
        if self.bar:
            return self.bar
        else:
            return self.__foo

    @foo.setter
    def foo(self, value):
        self.__foo = value

__foo will be resolved into _MyModel__foo (as seen by dir(..)) thus hidden (private). Note that this form also permits using of @property decorator which would be ultimately a nicer way to write readable code.

Again, django will create _MyModel table with two fields foo and bar.

djvg
  • 11,722
  • 5
  • 72
  • 103
Yauhen Yakimovich
  • 13,635
  • 8
  • 60
  • 67
  • this is odd, but in my case, django 1.4.5 on postgres I'm getting a column foo in this case... I haven't used setter and getter and the property is read-only – Evgeny Mar 02 '13 at 22:03
  • Could you post the code? I am confused. In my understanding properties mechanism is provided by decorator "@property", but if you have not used it then what you call "read-only property" must be an object field *.foo* (attribute in python). Can you or can you not actually assign anything to "my_model.foo = 'baz' ?? – Yauhen Yakimovich Mar 03 '13 at 22:09
  • How would you access foo? `MyMode.objects.order_by('__foo')`? – agconti Aug 26 '13 at 16:18
  • 1
    @agconti database query methods like order_by(), filter(), and get() won't work with method properties because they don't directly map to database table columns. It is their strength and weakness. – Nostalg.io Dec 17 '14 at 22:43
  • 2
    Just a note:--> Django 2.0 gives an error: fields.E002) Field names must not contain "__"' – Sajjan Kumar Aug 29 '18 at 07:27
  • @sajjjan-kumar: Make that Django>=1.9. `_foo` (single underscore) *is* allowed though. – djvg Dec 07 '18 at 17:56
10

The previous solutions suffer because @property causes problems in admin, and .filter(_foo).

A better solution would be to override setattr except that this can cause problems initializing the ORM object from the DB. However, there is a trick to get around this, and it's universal.

class MyModel(models.Model):
    foo = models.CharField(max_length = 20)
    bar = models.CharField(max_length = 20)

    def __setattr__(self, attrname, val):
        setter_func = 'setter_' + attrname
        if attrname in self.__dict__ and callable(getattr(self, setter_func, None)):
            super(MyModel, self).__setattr__(attrname, getattr(self, setter_func)(val))
        else:
            super(MyModel, self).__setattr__(attrname, val)

    def setter_foo(self, val):
        return val.upper()

The secret is 'attrname in self.__dict__'. When the model initializes either from new or hydrated from the __dict__!

Justin Alexander
  • 2,004
  • 3
  • 21
  • 25
2

It depends whether your property is a means-to-an-end or an end in itself.

If you want this kind of "override" (or "fallback") behavior when filtering querysets (without first having to evaluate them), I don't think properties can do the trick. As far as I know, Python properties do not work at the database level, so they cannot be used in queryset filters. Note that you can use _foo in the filter (instead of foo), as it represents an actual table column, but then the override logic from your get_foo() won't apply.

However, if your use-case allows it, the Coalesce() class from django.db.models.functions (docs) might help.

Coalesce() ... Accepts a list of at least two field names or expressions and returns the first non-null value (note that an empty string is not considered a null value). ...

This implies that you can specify bar as an override for foo using Coalesce('bar','foo'). This returns bar, unless bar is null, in which case it returns foo. Same as your get_foo() (except it doesn't work for empty strings), but on the database level.

The question that remains is how to implement this.

If you don't use it in a lot of places, simply annotating the queryset may be easiest. Using your example, without the property stuff:

class MyModel(models.Model):
    foo = models.CharField(max_length = 20)
    bar = models.CharField(max_length = 20)

Then make your query like this:

from django.db.models.functions import Coalesce

queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo'))

Now the items in your queryset have the magic attribute bar_otherwise_foo, which can be filtered on, e.g. queryset.filter(bar_otherwise_foo='what I want'), or it can be used directly on an instance, e.g. print(queryset.all()[0].bar_otherwise_foo)

The resulting SQL query from queryset.query shows that Coalesce() indeed works at the database level:

SELECT "myapp_mymodel"."id", "myapp_mymodel"."foo", "myapp_mymodel"."bar",
    COALESCE("myapp_mymodel"."bar", "myapp_mymodel"."foo") AS "bar_otherwise_foo" 
FROM "myapp_mymodel"

Note: you could also call your model field _foo then foo=Coalesce('bar', '_foo'), etc. It would be tempting to use foo=Coalesce('bar', 'foo'), but that raises a ValueError: The annotation 'foo' conflicts with a field on the model.

There must be several ways to create a DRY implementation, for example writing a custom lookup, or a custom(ized) Manager.

A custom manager is easily implemented as follows (see example in docs):

class MyModelManager(models.Manager):
    """ standard manager with customized initial queryset """
    def get_queryset(self):
        return super(MyModelManager, self).get_queryset().annotate(
            bar_otherwise_foo=Coalesce('bar', 'foo'))


class MyModel(models.Model):
    objects = MyModelManager()
    foo = models.CharField(max_length = 20)
    bar = models.CharField(max_length = 20)

Now every queryset for MyModel will automatically have the bar_otherwise_foo annotation, which can be used as described above.

Note, however, that e.g. updating bar on an instance will not update the annotation, because that was made on the queryset. The queryset will need to be re-evaluated first, e.g. by getting the updated instance from the queryset.

Perhaps a combination of a custom manager with annotation and a Python property could be used to get the best of both worlds (example at CodeReview).

djvg
  • 11,722
  • 5
  • 72
  • 103