You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:26:02