web-dev-qa-db-ja.com

Django:保存するとき、フィールドが変更されたかどうかをどのように確認できますか?

私のモデルでは:

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を変更したときに新しい画像を取得するにはどうすればよいですか?次に、リモート画像をキャッシュするより良い方法はありますか?

250
Paul Tarjan

少し遅れていますが、この投稿に出くわした他の人のためにこの解決策を捨てさせてください。基本的に、元の値のコピーを保持するために、__init__models.Modelメソッドをオーバーライドする必要があります。これにより、別のDBルックアップを行う必要がなくなります(これは常に良いことです)。

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
381
Josh

私は次のミックスインを使用します:

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

注意

このソリューションは、現在のリクエストのコンテキストでのみ機能することに注意してください。したがって、主に単純な場合に適しています。複数のリクエストが同じモデルインスタンスを同時に操作できる同時環境では、間違いなく異なるアプローチが必要です。

175
iperelivskiy

そして今、直接の答えのために:フィールドの値が変更されたかどうかを確認する1つの方法は、インスタンスを保存する前にデータベースから元のデータを取得することです。この例を考えてみましょう:

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のcleanメソッドまたはsaveメソッドで検出できます。

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 = []
133
zgoda

最良の方法は、pre_saveシグナルを使用することです。この質問が尋ねられて回答された2009年のオプションではなかったかもしれませんが、今日これを見ている人はこのようにするべきです:

@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
132
Chris Pratt

Django 1.8がリリースされたため、from_db classmethodを使用して、remote_imageの古い値をキャッシュできます。次に、saveメソッドで、フィールドの古い値と新しい値を比較して、値が変更されたかどうかを確認できます。

@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!
52
Serge

フィールド変更の追跡はDjango-model-utilsで利用できることに注意してください。

https://Django-model-utils.readthedocs.org/en/latest/index.html

16
Lee Hinde

フォームを使用している場合は、フォームのchanged_datadocs )を使用できます。

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
13
laffuste

私はパーティーに少し遅れましたが、この解決策も見つけました: Django Dirty Fields

5
Fred Campos

Django 1.8現在、Sergeが言及しているようにfrom_dbメソッドがあります。実際、Djangoドキュメントには、この特定の使用例が例として含まれています。

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

以下は、データベースからロードされるフィールドの初期値を記録する方法を示す例です

5

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
Robert Kajic

これはDjango 1.8で動作します

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

別の遅い回答ですが、新しいファイルがファイルフィールドにアップロードされたかどうかを確認しようとしている場合は、これを試してください:(リンクに関するChristopher Adamsのコメントから適応 http://zmsmith.com/2010/05/Django-check-if-a-field-has-changed / ザックのコメントはこちら)

更新されたリンク: 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
3
Aaron McMillin

最適なソリューションは、おそらくモデルインスタンスを保存する前に追加のデータベース読み取り操作を含まず、Djangoライブラリを追加しないソリューションです。これが、laffusteのソリューションが望ましい理由です。管理サイトのコンテキストでは、上記のSionの答えのように、save_model-methodをオーバーライドし、そこでフォームのhas_changedメソッドを呼び出すだけです。次のようなものに到達します。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-method:

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

3
user3061675

私の解決策がターゲットフィールドクラスの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
MYaser

これは実際にはあなたの質問に答えるものではありませんが、別の方法でこれについて説明します。

ローカルコピーを正常に保存したら、remote_imageフィールドをクリアするだけです。その後、saveメソッドで、remote_imageが空でない場合はいつでも画像をいつでも更新できます。

URLへの参照を保持したい場合は、remote_imageフィールド自体ではなく、編集不可のブールフィールドを使用してキャッシュフラグを処理できます。

2
SmileyChris

すべてのフィールドの@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")

2
Hassek

別の方法があります。

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()が実行する2番目のステップは、Model.clean()を呼び出すことです。このメソッドは、モデルでカスタム検証を実行するためにオーバーライドする必要があります。このメソッドは、カスタムモデル検証を提供し、必要に応じてモデルの属性を変更するために使用する必要がありますたとえば、フィールドの値を自動的に提供したり、複数のフィールドにアクセスする必要がある検証を行うために使用できます。 "

1
Gonzalo

@livskiyのmixinを次のように拡張しました。

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
MYaser

David Cramerのソリューションを使用してはどうですか。

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!"
1
Sion

@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のpublicメソッドget_fieldsを使用します。これにより、コードの将来の証明になりますが、さらに重要なのは、外部キーとeditable = 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))
    )
0
theicfire

smileyChrisの答えの拡張として、last_updatedのモデルにdatetimeフィールドを追加し、変更を確認する前に到達できる最大年齢に何らかの制限を設定することができます

0
Jiaaro

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

@ivanlivskiのmixinは素晴らしいです。

私はそれを拡張しました

  • 10進数フィールドで機能することを確認してください。
  • プロパティを公開して使用を簡素化する

更新されたコードはこちらから入手できます。 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
sknutsonsf