Як зберігати, як можна перевірити, чи змінилося поле?


293

У своїй моделі я:

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
 used when the alias is made")
    image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")


    def save(self, *args, **kw):
        if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
            try :
                data = utils.fetch(self.remote_image)
                image = StringIO.StringIO(data)
                image = Image.open(image)
                buf = StringIO.StringIO()
                image.save(buf, format='PNG')
                self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
            except IOError :
                pass

Що чудово спрацьовує вперше remote_imageзміни.

Як я можу отримати нове зображення, коли хтось змінив remote_imageпсевдонім? По-друге, чи є кращий спосіб кешування віддаленого зображення?

Відповіді:


423

По суті, ви хочете змінити __init__метод, models.Modelщоб ви зберегли копію вихідного значення. Це робить це так, що вам не доведеться робити інший пошук БД (що завжди добре).

class Person(models.Model):
    name = models.CharField()

    __original_name = None

    def __init__(self, *args, **kwargs):
        super(Person, self).__init__(*args, **kwargs)
        self.__original_name = self.name

    def save(self, force_insert=False, force_update=False, *args, **kwargs):
        if self.name != self.__original_name:
            # name changed - do something here

        super(Person, self).save(force_insert, force_update, *args, **kwargs)
        self.__original_name = self.name

24
замість того, щоб перезаписати ініт, я б використав post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo

22
Перевірити методи рекомендується в документації Django: docs.djangoproject.com/en/dev/topics/db/models/…
полковник Спонз

10
@callum, так що якщо ви внесете зміни в об'єкт, збережіть його, а потім внесе додаткові зміни та зателефонуйте save()на нього ПРОТИ, він все одно буде працювати правильно.
philfreo

17
@Josh не виникне проблем, якщо у вас є кілька серверів додатків, які працюють проти однієї бази даних, оскільки вона відстежує лише зміни в пам'яті
Jens Alm

13
@lajarre, я думаю, що ваш коментар трохи вводить в оману. Документи пропонують бути обережними, коли ви робите це. Вони не рекомендують проти цього.
Джош

199

Я використовую наступний mixin:

from django.forms.models import model_to_dict


class ModelDiffMixin(object):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """

    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self.__initial = self._dict

    @property
    def diff(self):
        d1 = self.__initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        super(ModelDiffMixin, self).save(*args, **kwargs)
        self.__initial = self._dict

    @property
    def _dict(self):
        return model_to_dict(self, fields=[field.name for field in
                             self._meta.fields])

Використання:

>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>

Примітка

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


4
Дійсно ідеально, і не виконуйте зайвих запитів. Дуже дякую !
Стефан

28
+1 для використання міксину. +1 без додаткового звернення до БД. +1 за безліч корисних методів / властивостей. Мені потрібно мати можливість здійснити подачу заявки кілька разів.
Джейк

так. Плюс один для використання Mixin і без додаткового удару.
David S

2
Mixin чудовий, але ця версія має проблеми при використанні разом із .only (). Виклик Model.objects.only ('id') призведе до нескінченної рекурсії, якщо Model має щонайменше 3 поля. Щоб вирішити це, нам слід видалити відкладені поля із збереження в початковій і трохи
_dict

19
Подібно до відповіді Джоша, цей код буде оманливо працювати нормально на вашому однопроцесорному тестувальному сервері, але в момент, коли ви розгорнете його на будь-якому сервері багатообробної обробки, він дасть неправильні результати. Ви не можете знати, чи змінюєте ви значення в базі даних, не запитуючи її.
rspeer

154

Найкращий спосіб - за допомогою pre_saveсигналу. Можливо, не було варіанту ще у09 році, коли на це запитання було запропоновано та відповіли, але кожен, хто бачить це сьогодні, повинен зробити це так:

@receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something

6
Чому це найкращий спосіб, якщо метод, описаний вище Джошем, не передбачає додаткового звернення до бази даних?
joshcartme

36
1) цей метод є злому, сигнали в основному розроблені для таких випадків, як 2) цей метод вимагає внесення змін до вашої моделі, цей не 3), як ви можете прочитати в коментарях до цієї відповіді, він має побічні ефекти, які може бути потенційно проблематичним, це рішення не має
Кріс Пратт

2
Цей спосіб чудовий, якщо ви лише піклуєтесь про внесення змін безпосередньо перед збереженням. Однак це не спрацює, якщо ви хочете негайно відреагувати на зміну. Я останній раз зустрічався з останнім сценарієм (і зараз працюю над одним таким випадком).
Джош

5
@Josh: Що ви маєте на увазі під "негайно реагувати на зміну"? Яким чином це не дозволяє вам "реагувати"?
Кріс Пратт

2
Вибачте, я забув обсяг цього питання і мав на увазі зовсім іншу проблему. Однак, я думаю, що сигнали - це хороший спосіб перейти сюди (тепер, коли вони доступні). Однак я вважаю, що багато людей вважають переоцінку збереження "хаком". Я не вірю, що це так. Як підказує ця відповідь ( stackoverflow.com/questions/170337/… ), я вважаю, що переоцінка є найкращою практикою, коли ви не працюєте над змінами, "характерними для конкретної моделі". При цьому я не маю наміру нікому нав'язувати цю віру.
Джош

138

А тепер для прямої відповіді: один із способів перевірити, чи змінилося значення поля, - це отримати вихідні дані з бази даних перед збереженням екземпляра. Розглянемо цей приклад:

class MyModel(models.Model):
    f1 = models.CharField(max_length=1)

    def save(self, *args, **kw):
        if self.pk is not None:
            orig = MyModel.objects.get(pk=self.pk)
            if orig.f1 != self.f1:
                print 'f1 changed'
        super(MyModel, self).save(*args, **kw)

Те саме стосується роботи з формою. Ви можете виявити це методом очищення або збереження ModelForm:

class MyModelForm(forms.ModelForm):

    def clean(self):
        cleaned_data = super(ProjectForm, self).clean()
        #if self.has_changed():  # new instance or existing updated (form has data to save)
        if self.instance.pk is not None:  # new instance only
            if self.instance.f1 != cleaned_data['f1']:
                print 'f1 changed'
        return cleaned_data

    class Meta:
        model = MyModel
        exclude = []

24
Рішення Джоша набагато зручніше для баз даних. Додатковий дзвінок, щоб перевірити, що змінилося, коштує дорого.
дд.

4
Одне додаткове читання перед тим, як писати, не так вже й дорого. Також метод відстеження змін не працює, якщо є кілька запитів. Хоча це постраждає від перегонів між переходом і збереженням.
dalore

1
Не pk is not Noneприпиняйте говорити людям перевіряти, що це не застосовується, наприклад, якщо ви використовуєте UUIDField. Це просто погана порада.
користувач3467349

2
@dalore ви можете уникнути умови гонки, прикрасивши метод збереження за допомогою@transaction.atomic
Френка Папа,

2
@dalore, хоча вам потрібно переконатися, що рівень ізоляції транзакцій достатній. У postgresql за замовчуванням читається скоєне, але повторне читання необхідне .
Френк Папе

58

Оскільки Django 1.8 випущений, ви можете використовувати class_ethod from_db для кешування старого значення remote_image. Тоді в методі збереження ви можете порівняти старе і нове значення поля, щоб перевірити, чи змінилося значення.

@classmethod
def from_db(cls, db, field_names, values):
    new = super(Alias, cls).from_db(db, field_names, values)
    # cache value went from the base
    new._loaded_remote_image = values[field_names.index('remote_image')]
    return new

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    if (self._state.adding and self.remote_image) or \
        (not self._state.adding and self._loaded_remote_image != self.remote_image):
        # If it is first save and there is no cached remote_image but there is new one, 
        # or the value of remote_image has changed - do your stuff!

1
Спасибі - ось посилання на документи: docs.djangoproject.com/en/1.8/ref/models/instances / ... . Я вважаю, що це все ще спричиняє вищезазначене питання, коли база даних може змінюватися між тим, коли це оцінюється, і коли проводиться порівняння, але це хороший новий варіант.
trpt4him

1
Замість того, щоб шукати значення (що є O (n) на основі кількості значень), чи не було б це зробити швидше і зрозуміліше new._loaded_remote_image = new.remote_image?
сміливо

1
На жаль, я маю змінити свій попередній (тепер видалений) коментар. У той час як from_dbвикликаються refresh_from_db, атрибути на примірнику (тобто завантажені або попередній) не оновлюється. В результаті, я не можу знайти причину , чому це краще , ніж , __init__як вам все ще потрібно обробляти 3 випадки: __init__/ from_db, refresh_from_db, і save.
claytond


18

Якщо ви використовуєте форму, ви можете використовувати_файли- зміни_дані ( документи ):

class AliasForm(ModelForm):

    def save(self, commit=True):
        if 'remote_image' in self.changed_data:
            # do things
            remote_image = self.cleaned_data['remote_image']
            do_things(remote_image)
        super(AliasForm, self).save(commit)

    class Meta:
        model = Alias


5

Що стосується Джанго 1.8, то тут from_dbСергій згадує метод. Насправді, документи Django включають цей конкретний випадок використання як приклад:

https://docs.djangoproject.com/en/dev/ref/models/instance/#customizing-model-loading

Нижче наводиться приклад, який показує, як записати початкові значення полів, завантажених з бази даних


5

Це працює для мене в Django 1.8

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something

4

Ви можете використовувати django-model-changes, щоб зробити це без додаткового пошуку бази даних:

from django.dispatch import receiver
from django_model_changes import ChangesMixin

class Alias(ChangesMixin, MyBaseModel):
   # your model

@receiver(pre_save, sender=Alias)
def do_something_if_changed(sender, instance, **kwargs):
    if 'remote_image' in instance.changes():
        # do something

4

Ще одна пізня відповідь, але якщо ви просто намагаєтеся побачити, чи був завантажений новий файл у поле файлу, спробуйте це: (адаптовано з коментаря Крістофера Адамса за посиланням http://zmsmith.com/2010/05/django -check-if-a-field-змінився / в коментарі zach тут)

Оновлене посилання: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed/

def save(self, *args, **kw):
    from django.core.files.uploadedfile import UploadedFile
    if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
        # Handle FileFields as special cases, because the uploaded filename could be
        # the same as the filename that's already there even though there may
        # be different file contents.

        # if a file was just uploaded, the storage model with be UploadedFile
        # Do new file stuff here
        pass

Це дивовижне рішення для перевірки завантаження нового файлу. Набагато краще, ніж перевірка імені проти бази даних, оскільки ім'я файлу може бути однаковим. Ви можете використовувати його і в pre_saveприймачі. Дякуємо, що поділилися цим!
DataGreed

1
Ось приклад оновлення тривалості звуку в базі даних, коли файл оновлювався за допомогою мутагену для читання аудіоінформації - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed

3

Оптимальним рішенням, мабуть, є таке, яке не включає в себе додаткову операцію зчитування бази даних перед збереженням екземпляра моделі, а також будь-яку подальшу бібліотеку django. Ось чому рішення лаффуста є кращими. У контексті веб-сайту адміністратора можна просто змінити save_model-метод та застосувати has_changedметод форми там, як у відповіді Sion вище. Ви досягаєте щось подібне, спираючись на приклад налаштування Sion, але використовуючи changed_dataдля отримання всіх можливих змін:

class ModelAdmin(admin.ModelAdmin):
   fields=['name','mode']
   def save_model(self, request, obj, form, change):
     form.changed_data #output could be ['name']
     #do somethin the changed name value...
     #call the super method
     super(self,ModelAdmin).save_model(request, obj, form, change)
  • Заміна save_model:

https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

  • Вбудований changed_data-метод для поля:

https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data


2

Хоча це насправді не відповідає на ваше запитання, я б про це пішов по-іншому.

Просто очистіть remote_imageполе після успішного збереження локальної копії. Тоді у способі збереження ви завжди можете оновлювати зображення, коли remote_imageвоно не порожнє.

Якщо ви хочете зберегти посилання на URL, ви можете використовувати булеве поле, яке не можна редагувати, для обробки прапора кешування, а не remote_imageсамого поля.


2

У мене була така ситуація, перш ніж моє рішення було замінити pre_save()метод класу цільового поля, воно буде викликатися лише в тому випадку, якщо поле було змінено
корисно на прикладі FileField:

class PDFField(FileField):
    def pre_save(self, model_instance, add):
        # do some operations on your file 
        # if and only if you have changed the filefield

недолік:
не корисно, якщо ви хочете виконати будь-яку операцію (post_save), як-от використання створеного об'єкта в якійсь роботі (якщо певне поле змінилося)


2

поліпшення @josh відповіді для всіх полів:

class Person(models.Model):
  name = models.CharField()

def __init__(self, *args, **kwargs):
    super(Person, self).__init__(*args, **kwargs)
    self._original_fields = dict([(field.attname, getattr(self, field.attname))
        for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])

def save(self, *args, **kwargs):
  if self.id:
    for field in self._meta.local_fields:
      if not isinstance(field, models.ForeignKey) and\
        self._original_fields[field.name] != getattr(self, field.name):
        # Do Something    
  super(Person, self).save(*args, **kwargs)

просто для уточнення, getattr працює на те, щоб отримати поля, схожі person.nameна рядки (тобтоgetattr(person, "name")


І це ще не робить зайвих запитів на db?
andilabs

Я намагався реалізувати ваш код. Це добре працює, редагуючи поля. Але зараз у мене є проблема із вставленням нового. Я отримую DoesNotExist за моє поле ФК у класі. Деякі натяки, як це вирішити, будуть вдячні.
andilabs

Я щойно оновив код, тепер він пропускає зовнішні ключі, тому вам не потрібно буде отримувати ці файли з додатковими запитами (дуже дорого), і якщо об'єкт не існує, він пропустить зайву логіку.
Хассек

1

Я розширив міксин @livskiy наступним чином:

class ModelDiffMixin(models.Model):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """
    _dict = DictField(editable=False)
    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self._initial = self._dict

    @property
    def diff(self):
        d1 = self._initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        object_dict = model_to_dict(self,
               fields=[field.name for field in self._meta.fields])
        for field in object_dict:
            # for FileFields
            if issubclass(object_dict[field].__class__, FieldFile):
                try:
                    object_dict[field] = object_dict[field].path
                except :
                    object_dict[field] = object_dict[field].name

            # TODO: add other non-serializable field types
        self._dict = object_dict
        super(ModelDiffMixin, self).save(*args, **kwargs)

    class Meta:
        abstract = True

а DictField:

class DictField(models.TextField):
    __metaclass__ = models.SubfieldBase
    description = "Stores a python dict"

    def __init__(self, *args, **kwargs):
        super(DictField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        if not value:
            value = {}

        if isinstance(value, dict):
            return value

        return json.loads(value)

    def get_prep_value(self, value):
        if value is None:
            return value
        return json.dumps(value)

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

його можна використовувати, розширивши його у своїх моделях, поле _dict буде додано під час синхронізації / міграції, і це поле збереже стан ваших об'єктів


1

Як щодо використання рішення Девіда Крамера:

http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/

Я мав успіх, використовуючи його так:

@track_data('name')
class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.CharField(max_length=5)

    def save(self, *args, **kwargs):
        if self.has_changed('name'):
            print 'name changed'

    # OR #

    @classmethod
    def post_save(cls, sender, instance, created, **kwargs):
        if instance.has_changed('name'):
            print "Hooray!"

2
Якщо ви забудете super (Mode, self) .save (* args, ** kwargs), то ви відключите функцію збереження, тому не забудьте поставити це у спосіб збереження.
макс

Посилання на статтю застаріле, це нове посилання: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop

1

Модифікація відповіді @ ivanperelivskiy:

@property
def _dict(self):
    ret = {}
    for field in self._meta.get_fields():
        if isinstance(field, ForeignObjectRel):
            # foreign objects might not have corresponding objects in the database.
            if hasattr(self, field.get_accessor_name()):
                ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
            else:
                ret[field.get_accessor_name()] = None
        else:
            ret[field.attname] = getattr(self, field.attname)
    return ret

Для цього використовується загальнодоступний метод django 1.10 get_fields. Це робить код більш надійним у майбутньому, але важливіше також включає зовнішні ключі та поля, де можна редагувати = False.

Для довідки, ось реалізація .fields

@cached_property
def fields(self):
    """
    Returns a list of all forward fields on the model and its parents,
    excluding ManyToManyFields.

    Private API intended only to be used by Django itself; get_fields()
    combined with filtering of field properties is the public API for
    obtaining this field list.
    """
    # For legacy reasons, the fields property should only contain forward
    # fields that are not private or with a m2m cardinality. Therefore we
    # pass these three filters as filters to the generator.
    # The third lambda is a longwinded way of checking f.related_model - we don't
    # use that property directly because related_model is a cached property,
    # and all the models may not have been loaded yet; we don't want to cache
    # the string reference to the related_model.
    def is_not_an_m2m_field(f):
        return not (f.is_relation and f.many_to_many)

    def is_not_a_generic_relation(f):
        return not (f.is_relation and f.one_to_many)

    def is_not_a_generic_foreign_key(f):
        return not (
            f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
        )

    return make_immutable_fields_list(
        "fields",
        (f for f in self._get_fields(reverse=False)
         if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
    )

1

Ось ще один спосіб зробити це.

class Parameter(models.Model):

    def __init__(self, *args, **kwargs):
        super(Parameter, self).__init__(*args, **kwargs)
        self.__original_value = self.value

    def clean(self,*args,**kwargs):
        if self.__original_value == self.value:
            print("igual")
        else:
            print("distinto")

    def save(self,*args,**kwargs):
        self.full_clean()
        return super(Parameter, self).save(*args, **kwargs)
        self.__original_value = self.value

    key = models.CharField(max_length=24, db_index=True, unique=True)
    value = models.CharField(max_length=128)

Відповідно до документації: перевірка об'єктів

"Другий крок full_clean () виконує - викликати Model.clean (). Цей метод повинен бути скасований, щоб здійснити спеціальну перевірку на вашій моделі. Цей метод слід використовувати для надання перевірки користувальницької моделі та при необхідності змінити атрибути на вашій моделі. Наприклад, ви можете використовувати його для автоматичного надання значення для поля або для перевірки, яка вимагає доступу до більш ніж одного поля: "


1

Є атрибут __dict__, який містить усі поля як ключі, а значення - як значення поля. Тож ми можемо просто порівняти два з них

Просто змініть функцію збереження моделі на функцію нижче

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
    if self.pk is not None:
        initial = A.objects.get(pk=self.pk)
        initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
        initial_json.pop('_state'), final_json.pop('_state')
        only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
        print(only_changed_fields)
    super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

Приклад використання:

class A(models.Model):
    name = models.CharField(max_length=200, null=True, blank=True)
    senior = models.CharField(choices=choices, max_length=3)
    timestamp = models.DateTimeField(null=True, blank=True)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is not None:
            initial = A.objects.get(pk=self.pk)
            initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
            initial_json.pop('_state'), final_json.pop('_state')
            only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
            print(only_changed_fields)
        super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

дає вихід лише з тими полями, які були змінені

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}

1

Дуже пізно до гри, але це версія відповіді Кріса Пратта, яка захищає від перегонових умов, при цьому жертвуючи продуктивністю, використовуючи transactionблок іselect_for_update()

@receiver(pre_save, sender=MyModel)
@transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.select_for_update().get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something

0

як розширення відповіді SmileyChris, ви можете додати поле дати до моделі для моделі last_update і встановити якесь обмеження для максимального віку, до якого ви дозволите йому дійти, перш ніж перевірити зміну


0

Міксин від @ivanlivski чудовий.

Я продовжив це до

  • Переконайтеся, що він працює з полями Десяткові.
  • Розкрийте властивості для спрощення використання

Оновлений код доступний тут: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Для того, щоб допомогти людям, що не знають про Python чи Django, я наведу більш повний приклад. Це особливе використання - взяти файл у постачальника даних і забезпечити, щоб записи в базі даних відображали файл.

Об'єкт моєї моделі:

class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
    station_name = models.CharField(max_length=200)
    nearby_city = models.CharField(max_length=200)

    precipitation = models.DecimalField(max_digits=5, decimal_places=2)
    # <list of many other fields>

   def is_float_changed (self,v1, v2):
        ''' Compare two floating values to just two digit precision
        Override Default precision is 5 digits
        '''
        return abs (round (v1 - v2, 2)) > 0.01

Клас, який завантажує файл, має такі методи:

class UpdateWeather (object)
    # other methods omitted

    def update_stations (self, filename):
        # read all existing data 
        all_stations = models.Station.objects.all()
        self._existing_stations = {}

        # insert into a collection for referencing while we check if data exists
        for stn in all_stations.iterator():
            self._existing_stations[stn.id] = stn

        # read the file. result is array of objects in known column order
        data = read_tabbed_file(filename)

        # iterate rows from file and insert or update where needed
        for rownum in range(sh.nrows):
            self._update_row(sh.row(rownum));

        # now anything remaining in the collection is no longer active
        # since it was not found in the newest file
        # for now, delete that record
        # there should never be any of these if the file was created properly
        for stn in self._existing_stations.values():
            stn.delete()
            self._num_deleted = self._num_deleted+1


    def _update_row (self, rowdata):
        stnid = int(rowdata[0].value) 
        name = rowdata[1].value.strip()

        # skip the blank names where data source has ids with no data today
        if len(name) < 1:
            return

        # fetch rest of fields and do sanity test
        nearby_city = rowdata[2].value.strip()
        precip = rowdata[3].value

        if stnid in self._existing_stations:
            stn = self._existing_stations[stnid]
            del self._existing_stations[stnid]
            is_update = True;
        else:
            stn = models.Station()
            is_update = False;

        # object is new or old, don't care here            
        stn.id = stnid
        stn.station_name = name;
        stn.nearby_city = nearby_city
        stn.precipitation = precip

        # many other fields updated from the file 

        if is_update == True:

            # we use a model mixin to simplify detection of changes
            # at the cost of extra memory to store the objects            
            if stn.has_changed == True:
                self._num_updated = self._num_updated + 1;
                stn.save();
        else:
            self._num_created = self._num_created + 1;
            stn.save()

0

Якщо ви не знайдете інтересу до переважаючого saveметоду, можете зробити

  model_fields = [f.name for f in YourModel._meta.get_fields()]
  valid_data = {
        key: new_data[key]
        for key in model_fields
        if key in new_data.keys()
  }

  for (key, value) in valid_data.items():
        if getattr(instance, key) != value:
           print ('Data has changed')

        setattr(instance, key, value)

 instance.save()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.