Как обновить значения объекта в Django? - PullRequest
25 голосов
/ 20 марта 2012

У меня есть модельный объект в Джанго.Один из методов объекта использует блокировку на уровне строк для обеспечения точности значений, например:

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

Проблема в том, что если вы вызываете increment для объекта foo, значения объекта больше не отражаютзначения в базе данных.Мне нужен способ обновить значения в объекте или, по крайней мере, пометить их как устаревшие, чтобы они при необходимости обновлялись.По-видимому, это функциональность, которую разработчики Django отказываются добавлять .

. Я попытался использовать следующий код:

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

К сожалению, у меня есть вторая модель сследующее определение:

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

Это вызывает ошибку, потому что это появляется в списке полей, но вы не можете getattr или setattr это.

У меня есть два вопроса:

  • Как обновить значения на моем объекте?

  • Нужно ли беспокоиться об обновлении каких-либо объектовсо ссылками на мой объект, как внешние ключи?

Ответы [ 11 ]

40 голосов
/ 20 декабря 2014

Наконец, в Django 1.8 у нас есть специальный метод для этого. Он называется refresh_from_db , и это новый метод класса django.db.models.Model.

.

Пример использования:

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

Если ваша версия Django меньше 1.8, но вы хотите иметь эту функцию, измените вашу модель для наследования от 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()
13 голосов
/ 23 марта 2012

Полагаю, вам нужно сделать это из самого класса, иначе вы просто сделаете что-то вроде:

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

Но делать это внутренне и заменять self становится ужасно ...

1 голос
/ 21 марта 2012

Хм.Мне кажется, что вы никогда не можете быть уверены, что ваш любой foo.counter действительно обновлен ... И это верно для любого типа объекта модели, а не только для таких счетчиков ...

Допустим, у вас есть следующий код:

    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

В конце этого f2.counter теперь будет неправильным.

Почему обновление значений так важно - почему нельзя просто получитьназад новый экземпляр всякий раз, когда это необходимо?

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

Но если вам действительно нужно, вы можете создать метод обновления самостоятельно ... как вы указали в своем вопросе, но вам нужно пропустить соответствующие поля, чтобы вы могли простоукажите списки имен полей, по которым вы хотите выполнить итерацию (вместо _meta.get_all_fieldnames).Или вы можете перебрать Foo._meta.fields, это даст вам объекты Field, и вы можете просто проверить класс поля - я думаю, если они являются экземплярами django.db.fields.field.related.RelatedField, тогда вы пропускаете их,Вы можете, если хотите, ускорить это, выполнив это только при загрузке модуля и сохранении этого списка в классе модели (используйте сигнал class_prepared )

0 голосов
/ 27 августа 2018

Я использовал такой метод, потому что новая встроенная функция refresh_from_db не обновляет дочерние элементы, у которых были изменены их атрибуты, часто вызывая проблемы. Это очищает кеш любых внешних ключей.

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 голосов
/ 30 июля 2017

Вы можете использовать

refresh_from_db()

Например:

obj.refresh_from_db()

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

0 голосов
/ 07 августа 2014

У меня была похожая потребность, и, хотя вы не можете эффективно обновить существующий объект, не нарушая его целостность, вы все равно можете применять лучшие практики во время реализации.Что касается меня, я помечаю объект как устаревший и делаю так, чтобы предотвратить дальнейший доступ к нему, как показано в следующем примере:

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)

(написано на лету, простите за любые возможные опечатки: -))

0 голосов
/ 29 января 2014

Это объединяет лучшее из двух приведенных выше ответов и добавляет современный синтаксис django:

Получите свежие данные и гарантируйте * они остаются свежими для вашей транзакции:

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

Это будет только работать, если все , которое изменяет объект, проходит через select_for_update. Другие процессы, которые получают объект без блокировки, будут зависать при save () вместо get () и топить изменения сразу после первой транзакции.

0 голосов
/ 03 ноября 2012

У меня есть несколько запущенных процессов, которые работают параллельно.После завершения вычислений я хочу обновить значения и сохранить модель, но я не хочу, чтобы весь процесс связывал транзакцию.Так что моя стратегия выглядит примерно так:

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 голосов
/ 21 марта 2012

Быстрый, безобразный и непроверенный:

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

хотя я думаю, что вы могли бы сделать это, используя другой _meta подход к isinstance() вызову.

Очевидно, мы оба знаем, что этого следует избегать, если это возможно. Может быть, лучше подойти к внутреннему состоянию модели?

EDIT - Django 1.4 Поддержка SELECT FOR UPDATE решит эту проблему?

0 голосов
/ 20 марта 2012

Для этого можно использовать выражения F Джанго .

Чтобы показать пример, я буду использовать эту модель:

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

Затем вы можетесделать что-то вроде этого:

    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
...