保存时,如何检查字段是否已更改?

在我的模型中我有:

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时,我如何获取一个新的图像?其次,是否有更好的方法来缓存远程映像?

221868 次浏览

虽然这实际上并没有回答你的问题,但我会以不同的方式来讨论这个问题。

成功保存本地副本后,只需清除remote_image字段。然后在保存方法中,只要remote_image不为空,就可以随时更新图像。

如果你想保持对url的引用,你可以使用一个不可编辑的布尔字段来处理缓存标志,而不是remote_image字段本身。

作为SmileyChris回答的扩展,您可以为last_updated的模型添加一个datetime字段,并在检查更改之前为您将让它达到的最大年龄设置某种限制

现在直接回答:检查字段的值是否已经改变的一种方法是在保存实例之前从数据库获取原始数据。想想这个例子:

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

本质上,你想要重写models.Model__init__方法,这样你就可以保留原始值的副本。这使得您不必再执行另一个DB查找(这总是一件好事)。

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


__original_name = None


def __init__(self, *args, **kwargs):
super().__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().save(force_insert, force_update, *args, **kwargs)
self.__original_name = self.name

最好的方法是使用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

我使用以下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)
>>>

请注意

请注意,此解决方案仅在当前请求的上下文中工作良好。因此,它主要适用于简单的情况。在并发环境中,多个请求可以同时操作同一个模型实例,您肯定需要一种不同的方法。

在我的解决方案是覆盖目标字段类的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
< p >劣势:
如果你想做任何(post_save)操作,比如在某些作业中使用已创建的对象(如果某些字段已更改),则无效

我扩展了@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字段将在同步/迁移时添加,该字段将存储对象

的状态

改进所有领域的@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") . xml)

你可以使用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

另一个迟来的答案,但如果你只是想看看一个新文件是否已经上传到一个文件字段,试试这个:(改编自Christopher Adams对链接http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/的评论在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

@ivanlivski的mixin很棒。

我把它扩展到

  • 确保它适用于Decimal字段。
  • 公开属性以简化使用

更新后的代码可在这里: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py < / p > 为了帮助刚接触Python或Django的人,我将给出一个更完整的示例。 这种特殊用法是从数据提供者获取一个文件,并确保数据库中的记录反映该文件。< / p >

我的模型对象:

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

注意,字段更改跟踪在django-model-utils中是可用的。

https://django-model-utils.readthedocs.org/en/latest/index.html < a href = " https://django-model-utils.readthedocs.org/en/latest/index.html " > < / >

用David Cramer的解决方案怎么样:

< a href = " http://cramer。io / 2010/12/06 / tracking-changes-to-fields-in-django / " rel = " http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/ nofollow”> < / >

我曾经这样成功地使用过:

@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!"

自从Django 1.8发布以来,你可以使用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!

从Django 1.8开始,就有了Serge提到的from_db方法。事实上,Django文档中包含了这样一个特定的用例:

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

下面是一个示例,展示如何记录从数据库加载的字段的初始值

这在Django 1.8中是适用的

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

如果你正在使用一个表单,你可以使用form的changed_data (文档):

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

最佳的解决方案可能是在保存模型实例之前不包括额外的数据库读取操作,也不包括任何进一步的django库。这就是为什么拉弗斯特的解决方案更可取。在管理站点的上下文中,可以简单地覆盖__abc0方法,并在那里调用表单的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-method:

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

我有点晚了,但我也找到了这个解决方案: Django脏域 < / p >

修改@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。这使得代码更经得起未来的考验,但更重要的是还包括了外键和edititable =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))
)

这是另一种方法。

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()。应该重写此方法,以便在模型上执行自定义验证。 此方法应用于提供自定义模型验证,并根据需要修改模型上的属性。例如,您可以使用它自动为字段提供一个值,或者进行需要访问多个字段的验证:"

如果你对重写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()

有一个属性__dict__,它将所有字段作为键,并将值作为字段值。所以我们可以比较其中两个

只需将模型的save函数更改为下面的函数即可

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'}}

很晚了,但是这是一个克里斯·帕拉特的回答的版本,通过使用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

我对@iperelivskiy的解决方案的看法:在大规模的情况下,为每个__init__创建_initial字典是昂贵的,而且大多数时候是不必要的。我稍微改变了mixin,这样它只在你显式地告诉它这样做时(通过调用instance.track_changes)才会记录更改:

from typing import KeysView, Optional
from django.forms import model_to_dict


class TrackChangesMixin:
_snapshot: Optional[dict] = None


def track_changes(self):
self._snapshot = self.as_dict


@property
def diff(self) -> dict:
if self._snapshot is None:
raise ValueError("track_changes wasn't called, can't determine diff.")
d1 = self._snapshot
d2 = self.as_dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if str(v) != str(d2[k])]
return dict(diffs)


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


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


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

我找到了这个包django-lifecycle。 它使用django信号定义@hook装饰器,非常健壮可靠。

有时我想检查多个共享这些字段的模型上相同特定字段的更改,因此我定义了这些字段的列表并使用一个信号。在这种情况下,只有当某些东西发生了变化,或者条目是新的时,地理编码才会寻址:

from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=SomeUserProfileModel)
@receiver(pre_save, sender=SomePlaceModel)
@receiver(pre_save, sender=SomeOrganizationModel)
@receiver(pre_save, sender=SomeContactInfoModel)
def geocode_address(sender, instance, *args, **kwargs):


input_fields = ['address_line', 'address_line_2', 'city', 'state', 'postal_code', 'country']


try:
orig = sender.objects.get(id=instance.id)
if orig:
changes = 0
for field in input_fields:
if not (getattr(instance, field)) == (getattr(orig, field)):
changes += 1
if changes > 0:
# do something here because at least one field changed...
my_geocoder_function(instance)
except:
# do something here because there is no original, or pass.
my_geocoder_function(instance)


写一次,加上“@receiver"当然胜过覆盖多个模型保存方法,但也许其他人有更好的想法。