¿Funcionan las propiedades en los campos del modelo django?

Creo que la mejor manera de hacer esta pregunta es con algún código … ¿puedo hacer esto? ( editar : RESPUESTA: no)

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) 

O tengo que hacerlo así:

Sí, tienes que hacerlo así:

 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) 

nota : puede mantener el nombre de la columna como ‘foo’ en la base de datos pasando un db_column al campo del modelo. Esto es muy útil cuando está trabajando en un sistema existente y no quiere tener que realizar migraciones de base de datos sin ninguna razón.

Un campo modelo ya es propiedad, por lo que diría que debe hacerlo de la segunda forma para evitar un choque de nombres.

Cuando define foo = propiedad (..), en realidad reemplaza la línea foo = models .., por lo que ese campo ya no será accesible.

Deberá usar un nombre diferente para la propiedad y el campo. De hecho, si lo hace de la manera que lo tiene en el ejemplo # 1, obtendrá un bucle infinito cuando intente acceder a la propiedad, ya que ahora intenta devolverse.

EDITAR: Quizás también debería considerar no usar _foo como nombre de campo, sino foo, y luego definir otro nombre para su propiedad porque las propiedades no se pueden usar en QuerySet, por lo que deberá usar los nombres de campo reales cuando haga una filtro por ejemplo.

Como se mencionó, una alternativa correcta para implementar su propia clase django.db.models.Field, uno debe usar el argumento db_column y un atributo de clase personalizado (u oculto). Solo estoy reescribiendo el código en la edición de @Jiaaro siguiendo convenciones más estrictas para OOP en python (por ejemplo, si _foo debería estar realmente oculto):

 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 se resolverá en _MyModel__foo (como se ve por dir (..) ) por lo tanto oculto ( privado ). Tenga en cuenta que esta forma también permite el uso de @property decorator, que sería una forma más agradable de escribir código legible.

De nuevo, django creará la tabla * _MyModel con dos campos foo y bar .

Las soluciones anteriores sufren porque @property causa problemas en admin y .filter (_foo).

Una solución mejor sería anular setattr, excepto que esto puede causar problemas al inicializar el objeto ORM desde la base de datos. Sin embargo, hay un truco para solucionar esto, y es 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() 

El secreto es ‘ attrname in self .__ dict__ ‘. Cuando el modelo se inicializa ya sea desde nuevo o hidratado desde __dict__ !

Depende de si su property es un medio para un fin o un fin en sí mismo.

Si desea este tipo de comportamiento de “anulación” (o “repliegue”) al filtrar los conjuntos de consultas (sin tener que evaluarlos primero), no creo que las propiedades puedan hacer el truco. Por lo que sé , las propiedades de Python no funcionan en el nivel de la base de datos, por lo que no se pueden usar en los filtros Queryset. Tenga en cuenta que puede usar _foo en el filtro (en lugar de foo ), ya que representa una columna de la tabla real, pero entonces la lógica de anulación de su get_foo() no se aplicará.

Sin embargo, si su caso de uso lo permite, la clase Coalesce() de django.db.models.functions ( docs ) podría ayudar.

Coalesce() … Acepta una lista de al menos dos nombres de campo o expresiones y devuelve el primer valor no nulo (tenga en cuenta que una cadena vacía no se considera un valor nulo). …

Esto implica que puede especificar la bar como una anulación para foo usando Coalesce('bar','foo') . Esta bar devoluciones, a menos que la bar sea null , en cuyo caso devuelve foo . Igual que su get_foo() (excepto que no funciona para cadenas vacías), pero en el nivel de la base de datos.

La pregunta que queda es cómo implementar esto.

Si no lo usa en muchos lugares, la anotación del conjunto de consultas puede ser la más sencilla. Usando tu ejemplo, sin las cosas de la propiedad:

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

Luego haga su consulta de esta manera:

 from django.db.models.functions import Coalesce queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo')) 

Ahora los elementos en su queryset tienen el atributo magic bar_otherwise_foo , que puede filtrarse, por ejemplo queryset.filter(bar_otherwise_foo='what I want') , o se puede usar directamente en una instancia, por ejemplo, print(queryset.all()[0].bar_otherwise_foo)

La consulta SQL resultante de queryset.query muestra que Coalesce() hecho funciona en el nivel de la base de datos:

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

Nota: también puede llamar a su campo de modelo _foo luego foo=Coalesce('bar', '_foo') , etc. Sería tentador usar foo=Coalesce('bar', 'foo') , pero eso genera un ValueError: The annotation 'foo' conflicts with a field on the model.

Debe haber varias formas de crear una implementación DRY, por ejemplo, escribiendo una búsqueda personalizada o un administrador personalizado (ized) .

Un administrador personalizado se implementa fácilmente de la siguiente manera (ver ejemplo en documentos ):

 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) 

Ahora, cada consulta para MyModel tendrá automáticamente la anotación bar_otherwise_foo , que se puede usar como se describe anteriormente.

Sin embargo, tenga en cuenta que, por ejemplo, la actualización de la bar en una instancia no actualizará la anotación, ya que se realizó en el conjunto de consultas. El queryset deberá ser reevaluado primero, por ejemplo, obteniendo la instancia actualizada del queryset.

Tal vez una combinación de un administrador personalizado con anotación y una property Python podría usarse para obtener lo mejor de ambos mundos ( ejemplo en CodeReview ).