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

372

在我的模型中,我有:

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 被修改时,我如何获取新图像?其次,有更好的缓存远程图像的方法吗?

28个回答

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)操作,例如在某些作业中使用创建的对象(如果某个字段已更改),则不会有用。


1
如何使用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!"

3
如果你忘记了在 save 方法中添加 super(Mode, self).save(*args, **kwargs),那么就相当于禁用了保存函数,因此请务必记得添加这个代码。 - max
文章的链接已经过时,这是新链接:https://cra.mr/2010/12/06/tracking-changes-to-fields-in-django - GoTop
很遗憾,这个答案是不完整的。@track_data是什么?链接已经不再解释任何关于它的内容,而是重定向到首页,这就是为什么依赖链接中的内容是不好的,因为它们并不是永久的。 - Teekin

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。这样做可以使代码更具有未来性,更重要的是还包括外键和不可编辑字段。
供参考,这是.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
这里有另一种做法。
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()。应该重写此方法以对模型执行自定义验证。 此方法应该用于提供自定义模型验证,并在必要时修改模型上的属性。例如,您可以使用它自动为字段提供值,或者执行需要访问多个字段的验证:"

0
作为SmileyChris答案的扩展,您可以向模型添加一个datetime字段来保存last_updated,并设置某种限制,以防止其在检查更改之前达到最大年龄。

0
有时候我想要检查多个模型上相同特定字段的变化,所以我定义了一个包含这些字段的列表,并使用信号。在这种情况下,只有当地址发生变化或者新条目时才进行地理编码:
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”,肯定比覆盖多个模型保存方法更好,但也许有些人有更好的想法。


0

来自 @ivanlivski 的 mixin 很棒。

我对它进行了扩展,以确保它与 Decimal 字段一起使用。

  • 公开属性以简化使用

更新后的代码在这里可用: 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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接