Подтвердить что ты не робот

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

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

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.

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

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

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

4b9b3361

Ответ 1

Наконец, в 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 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()

Ответ 2

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

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

Но выполнение этого внутри и замена self становится уродливым...

Ответ 3

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

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

    f1 = Foo.objects.get()[0]
    f2 = Foo.objects.get()[0]  #probably somewhere else!
    f1.increment() #let 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 signal)

Ответ 4

Я понимаю, почему вы используете SELECT ... FOR UPDATE, но как только вы его выпустили, вы все равно должны взаимодействовать с self.

Например, попробуйте вместо этого:

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

Строка заблокирована, но теперь взаимодействие происходит на экземпляре in-memory, поэтому изменения остаются в синхронизации.

Ответ 5

Вы можете использовать Django выражения 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

Ответ 6

Быстрый, уродливый и непроверенный:

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

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

РЕДАКТИРОВАТЬ. Является ли Django 1.4 SELECT FOR UPDATE support, чтобы решить эту проблему?

Ответ 7

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

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

Ответ 8

Это сочетает в себе лучший из двух ответов выше и добавляет современный синтаксис 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() и топать при изменении сразу после совершения первой транзакции.

Ответ 9

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

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

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