web-dev-qa-db-fra.com

Comment actualiser les valeurs d'un objet dans Django?

J'ai un objet modèle dans Django. L'une des méthodes de l'objet utilise le verrouillage au niveau de la ligne pour garantir l'exactitude des valeurs, comme suit:

class Foo(model.Model):
    counter = models.IntegerField()

    @transaction.commit_on_success
    def increment(self):
        x = Foo.objects.raw("SELECT * from fooapp_foo WHERE id = %s FOR UPDATE", [self.id])[0]
        x.counter += 1
        x.save()

Le problème est que si vous appelez increment sur un objet foo, les valeurs de cet objet ne reflètent plus les valeurs de la base de données. J'ai besoin d'un moyen d'actualiser les valeurs dans l'objet, ou du moins de les marquer comme périmées afin qu'elles soient relues si nécessaire. Apparemment, il s’agit d’une fonctionnalité que les développeurs de Django refusent d’ajouter .

J'ai essayé d'utiliser le code suivant:

for field in self.__class__._meta.get_all_field_names():
    setattr(self, field, getattr(offer, field)) 

Malheureusement, j'ai un deuxième modèle avec la définition suivante:

class Bar(model.Model):
    foo = models.ForeignKey(Foo)

Cela provoque une erreur, car il apparaît dans la liste des champs, mais vous ne pouvez pas le modifier à getattr ou setattr.

J'ai deux questions:

  • Comment puis-je actualiser les valeurs sur mon objet?

  • Dois-je m'inquiéter de l'actualisation d'objets contenant des références à mon objet, tels que des clés étrangères?

25
Chris B.

Enfin, dans Django 1.8 , nous avons une méthode spécifique pour le faire. Cela s'appelle refresh_from_db et c'est une nouvelle méthode de la classe Django.db.models.Model.

Un exemple d'utilisation:

def update_result(self):
    obj = MyModel.objects.create(val=1)
    MyModel.objects.filter(pk=obj.pk).update(val=F('val') + 1)
    # At this point obj.val is still 1, but the value in the database
    # was updated to 2. The object's updated value needs to be reloaded
    # from the database.
    obj.refresh_from_db()

Si votre version de Django est inférieure à 1.8 mais que vous souhaitez disposer de cette fonctionnalité, modifiez votre modèle pour qu'il hérite de RefreshableModel:

from Django.db import models
from Django.db.models.constants import LOOKUP_SEP
from Django.db.models.query_utils import DeferredAttribute

class RefreshableModel(models.Model):

    class Meta:
        abstract = True

    def get_deferred_fields(self):
        """
        Returns a set containing names of deferred fields for this instance.
        """
        return {
            f.attname for f in self._meta.concrete_fields
            if isinstance(self.__class__.__dict__.get(f.attname), DeferredAttribute)
        }

    def refresh_from_db(self, using=None, fields=None, **kwargs):
        """
        Reloads field values from the database.
        By default, the reloading happens from the database this instance was
        loaded from, or by the read router if this instance wasn't loaded from
        any database. The using parameter will override the default.
        Fields can be used to specify which fields to reload. The fields
        should be an iterable of field attnames. If fields is None, then
        all non-deferred fields are reloaded.
        When accessing deferred fields of an instance, the deferred loading
        of the field will call this method.
        """
        if fields is not None:
            if len(fields) == 0:
                return
            if any(LOOKUP_SEP in f for f in fields):
                raise ValueError(
                    'Found "%s" in fields argument. Relations and transforms '
                    'are not allowed in fields.' % LOOKUP_SEP)

        db = using if using is not None else self._state.db
        if self._deferred:
            non_deferred_model = self._meta.proxy_for_model
        else:
            non_deferred_model = self.__class__
        db_instance_qs = non_deferred_model._default_manager.using(db).filter(pk=self.pk)

        # Use provided fields, if not set then reload all non-deferred fields.
        if fields is not None:
            fields = list(fields)
            db_instance_qs = db_instance_qs.only(*fields)
        Elif self._deferred:
            deferred_fields = self.get_deferred_fields()
            fields = [f.attname for f in self._meta.concrete_fields
                      if f.attname not in deferred_fields]
            db_instance_qs = db_instance_qs.only(*fields)

        db_instance = db_instance_qs.get()
        non_loaded_fields = db_instance.get_deferred_fields()
        for field in self._meta.concrete_fields:
            if field.attname in non_loaded_fields:
                # This field wasn't refreshed - skip ahead.
                continue
            setattr(self, field.attname, getattr(db_instance, field.attname))
            # Throw away stale foreign key references.
            if field.rel and field.get_cache_name() in self.__dict__:
                rel_instance = getattr(self, field.get_cache_name())
                local_val = getattr(db_instance, field.attname)
                related_val = None if rel_instance is None else getattr(rel_instance, field.related_field.attname)
                if local_val != related_val:
                    del self.__dict__[field.get_cache_name()]
        self._state.db = db_instance._state.db

class MyModel(RefreshableModel):
    # Your Model implementation
    pass

obj = MyModel.objects.create(val=1)
obj.refresh_from_db()
40
blindOSX

Je suppose que vous devez faire cela depuis la classe elle-même, sinon vous feriez quelque chose comme:

def refresh(obj):
    """ Reload an object from the database """
    return obj.__class__._default_manager.get(pk=obj.pk)

Mais faire cela en interne et remplacer self devient moche ...

13
DrMeers

Hmm. Il me semble que vous ne pouvez jamais être sûr que votre foo.counter est réellement à jour ... Et ceci est vrai pour tout type d'objet modèle, pas seulement ce type de compteurs ...

Disons que vous avez le code suivant:

    f1 = Foo.objects.get()[0]
    f2 = Foo.objects.get()[0]  #probably somewhere else!
    f1.increment() #let's assume this acidly increments counter both in db and in f1
    f2.counter # is wrong

À la fin de ceci, f2.counter sera maintenant faux.

Pourquoi actualiser les valeurs est-il si important? Pourquoi ne peut-il pas simplement récupérer une nouvelle instance chaque fois que cela est nécessaire?

    f1 = Foo.objects.get()[0]
    #stuff
    f1 = Foo.objects.get(pk=f1.id)

Mais si vous en avez vraiment besoin, vous pouvez créer vous-même une méthode d'actualisation ... comme vous l'avez indiqué dans votre question, mais vous devez ignorer les champs associés. Vous pouvez donc spécifier les listes de noms de champs sur lesquels vous souhaitez effectuer une itération (plutôt que _meta.get_all_fieldnames). . Vous pouvez également parcourir Foo._meta.fields pour obtenir des objets Field. Vous pouvez simplement vérifier la classe du champ. Si ce sont des instances de Django.db.fields.field.field.related.RelatedField, vous les ignorez. Vous pouvez, si vous le souhaitez, accélérer le processus en ne chargeant votre module et en enregistrant cette liste dans votre classe de modèle (utilisez un signal class_prepared )

1
Tim Diggins

Je vois pourquoi vous utilisez SELECT ... FOR UPDATE, mais une fois que vous avez publié ceci, vous devriez toujours interagir avec self.

Par exemple, essayez plutôt ceci:

@transaction.commit_on_success
def increment(self):
    Foo.objects.raw("SELECT id from fooapp_foo WHERE id = %s FOR UPDATE", [self.id])[0]
    self.counter += 1
    self.save()

La ligne est verrouillée, mais à présent l'interaction a lieu sur l'instance en mémoire, les modifications restent donc synchronisées.

0
Chris Pratt

Vous pouvez utiliser les expressions F de Django pour le faire.

Pour montrer un exemple, je vais utiliser ce modèle:

# models.py
from Django.db import models
class Something(models.Model):
    x = models.IntegerField()

Ensuite, vous pouvez faire quelque chose comme ceci:

    from models import Something
    from Django.db.models import F

    blah = Something.objects.create(x=3)
    print blah.x # 3

    # set property x to itself plus one atomically
    blah.x = F('x') + 1
    blah.save()

    # reload the object back from the DB
    blah = Something.objects.get(pk=blah.pk)
    print blah.x # 4
0
jterrace

J'avais un besoin similaire et, même si vous ne pouvez pas actualiser l'objet existant sans altérer potentiellement son intégrité, vous pouvez toujours appliquer les meilleures pratiques au moment de la mise en œuvre. En ce qui me concerne, j’inscrivais l’objet comme obsolète et empêchais tout accès ultérieur à celui-ci, comme illustré dans l’exemple ci-dessous:

class MyModelManager(Manager):
    def get_the_token(self, my_obj):

        # you need to get that before marking the object stale :-)
        pk = my_obj.pk

        # I still want to do the update so long a pool_size > 0
        row_count = self.filter(pk=pk, pool_size__gt=0).update(pool_size=F('pool_size')-1)
        if row_count == 0:
            # the pool has been emptied in the meantime, deal with it
            raise Whatever

        # after this row, one cannot ask anything to the record
        my_obj._stale = True

        # here you're returning an up-to-date instance of the record
        return self.get(pk=pk)


class MyModel(Model):
    pool_size = IntegerField()

    objects = MyModelManager()

    def __getattribute__(self, name):
        try:
            # checking if the object is marked as stale
            is_stale = super(MyModel, self).__getattribute__('_stale'):

            # well, it is probably...
            if is_stale: raise IAmStale("you should have done obj = obj.get_token()")
        except AttributeError:
            pass

        # it is not stale...
        return super(MyModel, self).__getattribute__(name)

    def get_token(self):
        # since it's about an operation on the DB rather than on the object,
        # we'd rather do that at the manager level
        # any better way out there to get the manager from an instance?
        # self._meta.concrete_model.objects ?
        self.__class__.objects.get_the_token(self, my_obj)

(écrit à la volée, pardonnez les fautes de frappe :-))

0
lai

Rapide, moche et non testé:

from Django.db.models.fields.related import RelatedField

for field in self.__class__._meta.fields:
    if not isinstance(field, RelatedField):
        setattr(self, field.attname, getattr(offer, field)) 

bien que je pense que vous pouvez le faire en utilisant une autre approche _meta au cours de l'appel isinstance().

De toute évidence, nous savons tous les deux que cela devrait être évité si possible. Peut-être une meilleure approche serait-elle d’utiliser l’état du modèle interne?

EDIT - Est-ce que le support Django 1.4 SELECT FOR UPDATE va résoudre ce problème?

0
Matt Luongo

Ceci combine les meilleures des deux réponses ci-dessus et ajoute la syntaxe Django à jour:

Obtenez des données récentes et garantissez * qu'il reste frais pour votre transaction:

def refresh_and_lock(obj):
    """ Return an fresh copy with a lock."""
    return obj.__class__._default_manager.select_for_update().get(pk=obj.pk)

Cela fonctionnera seulement fonctionnera si tout qui change l’objet passe par select_for_update. D'autres processus qui obtiennent l'objet sans verrou verrouillent à save () au lieu de get (), et écrasent le changement juste après la validation de la première transaction.

0
Scott Smith

J'ai des processus de longue durée qui s'exécutent en parallèle. Une fois les calculs terminés, je souhaite mettre à jour les valeurs et enregistrer le modèle, mais je ne souhaite pas que tout le processus mette fin à une transaction. Donc, ma stratégie est quelque chose comme

model = Model.objects.get(pk=pk)

# [ do a bunch of stuff here]

# get a fresh model with possibly updated values
with transaction.commit_on_success():
    model = model.__class__.objects.get(pk=model.pk)
    model.field1 = results
    model.save()
0
Joseph Sheedy

Vous pouvez utiliser

refresh_from_db()

Par exemple:

obj.refresh_from_db()

https://docs.djangoproject.com/fr/1.11/ref/models/instances/#refreshing-objects-from-database

0
Sarath Ak

J'utilise une méthode comme celle-ci, car la nouvelle structure intégrée refresh_from_db ne permet pas d'actualiser les enfants dont les attributs ont été modifiés, ce qui pose souvent des problèmes. Cela efface le cache de toutes les clés étrangères.

def super_refresh_from_db(self):
    """ refresh_from_db only reloads local values and any deferred objects whose id has changed.
    If the related object has itself changed, we miss that.  This attempts to kind of get that back. """
    self.refresh_from_db()

    db = self._state.db
    db_instance_qs = self.__class__._default_manager.using(db).filter(pk=self.pk)

    db_instance = db_instance_qs.get()
    non_loaded_fields = db_instance.get_deferred_fields()
    for field in self._meta.concrete_fields:
        if field.attname in non_loaded_fields:
            # This field wasn't refreshed - skip ahead.
            continue

        if field.is_relation and field.get_cache_name() in self.__dict__:
            del self.__dict__[field.get_cache_name()]
0
Scott Stafford