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
时,我如何获取新的映像? 其次,有没有更好的方法来caching远程图像?
虽然有点晚了,但是让我为这个post中的其他人抛出这个解决scheme。 本质上,你想覆盖models.Model
的__init__
方法,以便保留原始值的副本。 这使得你不必做另一个数据库查询(这总是一件好事)。
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
我使用下面的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) >>>
注意
请注意,该解决scheme仅适用于当前请求。 因此它主要适用于简单的情况。 在多个请求可以同时操作同一个模型实例的并发环境中,您肯定需要一种不同的方法。
现在直接回答:检查字段值是否已更改的一种方法是在保存实例之前从数据库中提取原始数据。 考虑这个例子:
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 = []
最好的方法是用pre_save
信号。 在09年这个问题被问及答案时,可能不会有这样的select,但今天任何人都应该这样做:
@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
自Django 1.8发布以来,可以使用from_db classmethod来cachingremote_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-model-utils中可用。
https://django-model-utils.readthedocs.org/en/latest/index.html
如果您使用表单,则可以使用表单的changed_data ( docs ):
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 1.8开始,就有Serge提到的from_db
方法。 实际上,Django文档中包含了这个特定的用例:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
以下是显示如何logging从数据库加载的字段的初始值的示例
虽然这实际上并不能回答你的问题,但我会以不同的方式去解决这个问题。
成功保存本地副本后,只需清除remote_image
字段即可。 然后在保存方法中,只要remote_image
不为空,就可以随时更新图像。
如果您想保留对url的引用,则可以使用不可编辑的布尔值字段来处理caching标志,而不是remote_image
字段本身。
我有这种情况之前,我的解决scheme是重写目标字段类的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)操作(如某些作业中使用创build的对象)(如果某些字段已更改)
你可以使用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
改善@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
string(即getattr(person, "name")
最佳解决scheme可能是在保存模型实例之前不包含额外的数据库读取操作,也不包括任何其他的django-library。 这就是为什么laffuste的解决scheme是可取的。 在pipe理站点的上下文中,可以简单地覆盖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-method:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
另一个迟到的答案,但如果你只是想看看一个新的文件是否已经上传到文件字段,试试这个:(改编自克里斯托弗·亚当斯的评论链接http://zmsmith.com/2010/05/django – 检查 – 如果一个字段已经改变/在这里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
我已经扩展了@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字段将被添加当你同步/迁移,该字段将存储您的对象的状态
这在Django 1.8中适用于我
def clean(self): if self.cleaned_data['name'] != self.initial['name']: # Do something
我晚了一点晚,但我也发现这个解决scheme: Django肮脏的领域
使用has_changed()
方法检查表单数据是否已经从初始数据改变。
例如:
>>> data = {'subject': 'hello', ... 'message': 'Hi there', ... 'sender': 'foo@example.com', ... 'cc_myself': True} >>> f = ContactForm(data, initial=data) >>> f.has_changed() False
当表单提交时,Django重build它,以便可以完成比较。
>>> f = ContactForm(request.POST, initial=data) >>> f.has_changed() >>> "--True if request.POST and data are different"
如果request.POST中的数据与initial中的数据不同,则has_changed()将为True,否则为False。 通过为表单中的每个字段调用Field.has_changed()来计算结果。
Form.changed_data属性将显示哪些字段已经从initial
和request.POST
改变。
如果你已经有一个对象,你也可以使用instance=YourModel
而不是instance=YourModel
inital=data
。
你可以在这里看到更多。
作为SmileyChris的答案的扩展,您可以为last_updated的模型添加一个datetime字段,并设置某种限制的最大年龄,你会让它到达之前检查一个变化
@ivanlivski的mixin很棒。
我已经扩展到了
- 确保它适用于十进制字段。
- 公开属性以简化使用
更新后的代码可以在这里find: https : //github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
为了帮助刚接触Python或Django的人们,我将举一个更完整的例子。 这种特殊用法是从数据提供者处获取文件,并确保数据库中的logging反映文件。
我的模型对象:
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()
如何使用David Cramer的解决scheme:
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!"
修改@ 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)) )