Django 1.8执行makemigrations重复生成迁移问题排查与解决
问题原因及解决方案
这是Django迁移系统的常见坑——你的自定义FileValidator类缺少相等性判断方法,导致Django每次检测模型时都认为验证器实例发生了变化,从而重复生成Alter field类型的迁移文件。
为什么会出现这种情况?
Django的迁移系统在对比模型变更时,会深度检查字段的所有属性,包括验证器。默认情况下,Python对象的相等性是基于内存地址判断的:每次运行makemigrations或重启项目时,FileValidator(...)都会创建一个全新的实例,Django会判定“这个字段的验证器和上次的不一样”,于是生成新的迁移文件。
怎么解决?
给FileValidator类添加__eq__方法,让它根据验证器的核心配置参数(allowed_extensions、allowed_mimetypes、min_size、max_size)来判断两个实例是否逻辑相等,而不是单纯对比内存地址。
修复后的完整代码
# -*- coding: utf-8 -*- import magic from os.path import splitext from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat DEFAULT_FILE_MAX_SIZE = 10 * 1024 * 1024 # 10Mo # Photo, like profile picture # Only allow web-friendly extensions/mime types, since they'll be displayed on web pages # TODO Size is huge, should be optimised ALLOWED_PHOTO_EXT = [ 'jpg', 'jpeg', 'png', ] ALLOWED_PHOTO_MIME_TYPES = [ 'image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png', ] ALLOWED_PHOTO_MAX_SIZE = 10 * 1024 * 1024 # 10Mo # Any document # Allow a wide range of extensions and mime types # TODO Size is huge, should be optimised ALLOWED_DOCUMENT_EXT = [ 'jpg', 'jpeg', 'png', 'tif', 'bmp', 'pdf', 'doc', 'dot', 'docx', 'dotx', 'xls', 'xlt', 'xla', 'xlsx', 'xltx', 'pptx', 'potx', 'ppsx', ] ALLOWED_DOCUMENT_MIME_TYPES = [ 'image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png', 'image/tiff', 'image/bmp', 'application/pdf', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', # XXX PPTX can be detected as ZIP for some reasons ] ALLOWED_DOCUMENT_MAX_SIZE = 10 * 1024 * 1024 # 10Mo @deconstructible class FileValidator(object): """ Validator for files, checking the size, extension and mimetype. Initialization parameters: allowed_extensions: iterable with allowed file extensions ie. ('txt', 'doc') allowed_mimetypes: iterable with allowed mimetypes ie. ('image/png', ) min_size: minimum number of bytes allowed ie. 100 max_size: maximum number of bytes allowed ie. 24*1024*1024 for 24 MB Usage example:: MyModel(models.Model): myfile = FileField(validators=FileValidator(max_size=24*1024*1024), ...) """ extension_message = _("Extension '%(extension)s' not allowed. Allowed extensions are: '%(allowed_extensions)s.'") mime_message = _("MIME type '%(mimetype)s' is not valid. Allowed types are: %(allowed_mimetypes)s.") min_size_message = _('The current file %(size)s, which is too small. The minimum file size is %(allowed_size)s.') max_size_message = _('The current file %(size)s, which is too large. The maximum file size is %(allowed_size)s.') def __init__(self, *args, **kwargs): self.allowed_extensions = kwargs.pop('allowed_extensions', None) self.allowed_mimetypes = kwargs.pop('allowed_mimetypes', None) self.min_size = kwargs.pop('min_size', 0) self.max_size = kwargs.pop('max_size', DEFAULT_FILE_MAX_SIZE) def __call__(self, value): """ Check the extension, content type and file size. """ # Check the extension ext = splitext(value.name)[1][1:].lower() if self.allowed_extensions and ext not in self.allowed_extensions: message = self.extension_message % { 'extension': ext, 'allowed_extensions': ', '.join(self.allowed_extensions) } raise ValidationError(message) # Check the content type mimetype = magic.from_buffer(value.read(1024), mime=True) if self.allowed_mimetypes and mimetype not in self.allowed_mimetypes: message = self.mime_message % { 'mimetype': mimetype, 'allowed_mimetypes': ', '.join(self.allowed_mimetypes) } raise ValidationError(message) # Reset file pointer after reading (prevents issues with subsequent file operations) value.seek(0) # Check the file size (use value.size instead of len(value) for reliability) filesize = value.size if self.max_size and filesize > self.max_size: message = self.max_size_message % { 'size': filesizeformat(filesize), 'allowed_size': filesizeformat(self.max_size) } raise ValidationError(message) elif filesize < self.min_size: message = self.min_size_message % { 'size': filesizeformat(filesize), 'allowed_size': filesizeformat(self.min_size) } raise ValidationError(message) def __eq__(self, other): """Override equality check to prevent duplicate migrations""" if not isinstance(other, FileValidator): return False return ( self.allowed_extensions == other.allowed_extensions and self.allowed_mimetypes == other.allowed_mimetypes and self.min_size == other.min_size and self.max_size == other.max_size )
额外优化说明
- 修复了原代码的一个隐藏问题:读取文件内容检测MIME类型后,添加
value.seek(0)重置文件指针,避免后续处理文件时出现读取异常。 - 将
len(value)替换为value.size,这是Django文件对象获取大小的标准方式,兼容性和可靠性更强。
内容的提问来源于stack exchange,提问作者Vadorequest
相关产品推荐
相关产品推荐

