web-dev-qa-db-fra.com

les propriétés fonctionnent-elles sur les champs de modèle Django?

Je pense que la meilleure façon de poser cette question est avec du code ... puis-je le faire? ( éditer : REPONSE: non)

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)  

ou dois-je le faire comme ça:

Oui, vous devez le faire comme ceci:

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 : vous pouvez conserver le nom de la colonne sous la forme "foo" dans la base de données en transmettant un db_column au champ du modèle. Ceci est très utile lorsque vous travaillez sur un système existant et que vous ne voulez pas avoir à effectuer de migration de base de données sans raison.

35
Jiaaro

Un champ modèle est déjà une propriété, je dirais donc que vous devez le faire de la deuxième façon pour éviter un conflit de noms.

Lorsque vous définissez foo = property (..), elle remplace la ligne foo = models .., de sorte que ce champ ne sera plus accessible.

Vous devrez utiliser un nom différent pour la propriété et le champ. En fait, si vous le faites comme dans l'exemple 1, vous obtiendrez une boucle infinie lorsque vous tenterez d'accéder à la propriété telle qu'elle tente maintenant de se retourner.

EDIT: Peut-être devriez-vous également envisager de ne pas utiliser _foo comme nom de champ, mais plutôt foo, puis de définir un autre nom pour votre propriété, car les propriétés ne peuvent pas être utilisées dans QuerySet; vous devrez donc utiliser les noms de champs réels filtrer par exemple.

19
Andre Miller

Comme mentionné, une alternative correcte à la mise en œuvre de votre propre classe Django.db.models.Field, vous devez utiliser - db_column argument et un attribut de classe personnalisé (ou masqué). Je suis en train de réécrire le code dans l'édition de @Jiaaro en respectant des conventions plus strictes pour OOP en python (par exemple, si _foo doit être réellement masqué):

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} _ sera résolu en _MyModel__foo (vu par dir (..)} _) ainsi caché ( private ). Notez que cette forme permet également d’utiliser @property decorator , ce qui serait finalement un moyen plus agréable d’écrire du code lisible.

Encore une fois, Django créera la table * _MyModel avec deux champs foo et bar.

16
Yauhen Yakimovich

Les solutions précédentes souffrent car @property cause des problèmes dans admin et .filter (_foo). 

Une meilleure solution serait de remplacer setattr , sauf que cela peut poser des problèmes pour initialiser l'objet ORM à partir de la base de données. Cependant, il y a une astuce pour contourner cela, et c'est universel.

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

Le secret est ' attrname in self .__ dict__ '. Lorsque le modèle est initialisé à partir de nouveau ou hydraté à partir de_DICT_!

5
Justin Alexander

Cela dépend si votre property est un moyen de bout en bout ou une fin en soi.

Si vous souhaitez ce type de comportement de "substitution" (ou de "repli") lors du filtrage des ensembles de requêtes (sans avoir à les évaluer au préalable), je ne pense pas que les propriétés puissent faire l'affaire. Autant que je sache , les propriétés Python ne fonctionnent pas au niveau de la base de données, elles ne peuvent donc pas être utilisées dans les filtres de requête. Notez que can utilisez _foo dans le filtre (au lieu de foo), car il représente une colonne de table réelle, mais la logique de substitution de votre get_foo() ne s'appliquera pas.

Toutefois, si votre cas d'utilisation le permet, la classe Coalesce() de Django.db.models.functions ( docs ) peut vous aider. 

Coalesce() ... Accepte une liste d'au moins deux noms de champs ou d'expressions Et retourne la première valeur non nulle (notez qu'une chaîne vide N'est pas considérée comme une valeur nulle). ...

Cela implique que vous pouvez spécifier bar en tant que substitution pour foo à l'aide de Coalesce('bar','foo'). Cela retourne bar, sauf si bar est null, auquel cas il retourne foo. Identique à votre get_foo() (sauf que cela ne fonctionne pas pour les chaînes vides), mais au niveau de la base de données.

La question qui reste est de savoir comment mettre cela en œuvre. 

Si vous ne l'utilisez pas dans de nombreux endroits, simplement annotant le jeu de requêtes peut être plus simple. En utilisant votre exemple, sans les éléments de propriété:

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

Alors faites votre requête comme ceci:

from Django.db.models.functions import Coalesce

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

Maintenant, les éléments de votre ensemble de requêtes ont l'attribut magique bar_otherwise_foo, qui peut être filtré, par exemple queryset.filter(bar_otherwise_foo='what I want'), ou il peut être utilisé directement sur une instance, par exemple. print(queryset.all()[0].bar_otherwise_foo)

La requête SQL } résultante de queryset.query montre que Coalesce() fonctionne bien au niveau de la base de données:

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

Remarque: vous pouvez également appeler le champ de votre modèle _foo puis foo=Coalesce('bar', '_foo'), etc. Il serait tentant d'utiliser foo=Coalesce('bar', 'foo'), mais cela génère un ValueError: The annotation 'foo' conflicts with a field on the model.

Il doit exister plusieurs façons de créer une implémentation DRY, par exemple, en écrivant une recherche personnalisée , ou un gestionnaire personnalisé

Un gestionnaire personnalisé est facilement implémenté comme suit (voir exemple dans la documentation ):

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)

Désormais, chaque ensemble de requêtes pour MyModel aura automatiquement l'annotation bar_otherwise_foo, qui peut être utilisée comme décrit ci-dessus. 

Notez cependant que, par exemple, mettre à jour bar sur une instance ne mettra pas à jour l'annotation, car cela a été fait sur le jeu de requêtes. Le jeu de requêtes devra d'abord être réévalué, par ex. en obtenant l'instance mise à jour à partir du queryset.

Peut-être qu'une combinaison d'un gestionnaire personnalisé avec une annotation et d'une variable property de Python pourrait être utilisée pour obtenir le meilleur des deux mondes ( exemple sur CodeReview ).

0
djvg