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

Django回退迁移时无法删除数据行的异常问题排查

Django迁移反向回退时删除操作触发类型匹配错误

问题描述

给MediaType模型添加preview的TextChoice选项,通过RunPython的converter方法批量创建对应MediaType及关联的MediaSize数据,正向迁移执行正常。但反向回退迁移时,执行reverter删除name为preview的MediaType实例,触发矛盾的类型错误:ValueError: Cannot query "MediaSize object (0a803f4c-f9e0-4b2d-ae2a-c02d7b9e12b6)": Must be "MediaSize" instance.

错误日志

Unapplying applications.0012_alter_mediatype_name...Traceback (most recent call last):
  File "/usr/local/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 39, in <module>
    cli.main()
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 430, in main
    run()
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/root/.vscode-server/extensions/ms-python.python-2022.20.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "/code/ews/manage.py", line 22, in <module>
    main()
  File "/code/ews/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 446, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 440, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 414, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 460, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 98, in wrapped
    res = handle_func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/core/management/commands/migrate.py", line 290, in handle
    post_migrate_state = executor.migrate(
  File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 137, in migrate
    state = self._migrate_all_backwards(plan, full_plan, fake=fake)
  File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 215, in _migrate_all_backwards
    self.unapply_migration(states[migration], migration, fake=fake)
  File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 275, in unapply_migration
    state = migration.unapply(state, schema_editor)
  File "/usr/local/lib/python3.10/site-packages/django/db/migrations/migration.py", line 191, in unapply
    operation.database_backwards(
  File "/usr/local/lib/python3.10/site-packages/django/db/migrations/operations/special.py", line 201, in database_backwards
    self.reverse_code(from_state.apps, schema_editor)
  File "/code/ews/applications/migrations/0012_alter_mediatype_name.py", line 45, in reverter
    ).delete()
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 826, in delete
    collector.collect(del_query)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/deletion.py", line 343, in collect
    field.remote_field.on_delete(self, field, sub_objs, self.using)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/deletion.py", line 23, in CASCADE
    collector.collect(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/deletion.py", line 321, in collect
    sub_objs = self.related_objects(related_model, [field], batch)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/deletion.py", line 404, in related_objects
    return related_model._base_manager.using(self.using).filter(predicate)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1071, in filter
    return self._filter_or_exclude(False, args, kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1089, in _filter_or_exclude
    clone._filter_or_exclude_inplace(negate, args, kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1096, in _filter_or_exclude_inplace
    self._query.add_q(Q(*args, **kwargs))
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1502, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1532, in _add_q
    child_clause, needed_inner = self.build_filter(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1358, in build_filter
    return self._add_q(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1532, in _add_q
    child_clause, needed_inner = self.build_filter(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1415, in build_filter
    self.check_related_objects(join_info.final_field, value, join_info.opts)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1229, in check_related_objects
    self.check_query_object_type(v, opts, field)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1203, in check_query_object_type
    raise ValueError(
ValueError: Cannot query "MediaSize object (0a803f4c-f9e0-4b2d-ae2a-c02d7b9e12b6)": Must be "MediaSize" instance.

排查尝试

  • 在迁移外执行相同删除代码,可正常运行;
  • 迁移中途停止后单独删除MediaSize实例,仍触发相同错误;
  • 有无调试器均报错;
  • 迁移中可更新保存实例,但删除操作必报错;
  • 将AlterField和RunPython操作拆分为两个独立迁移文件并分别执行时,反向回退正常;合并在一个迁移中则报错。

迁移代码

from django.db import migrations, models


def converter(apps, schema_editor):
    AppExtension = apps.get_model("applications", "AppExtension")
    MediaType = apps.get_model("applications", "MediaType")
    MediaSize = apps.get_model("applications", "MediaSize")
    all_extensions = AppExtension.objects.all()
    media_types = []
    media_sizes = []
    for extension in all_extensions:
        mediatype_extension = MediaType(extension=extension, name="preview")
        media_types.append(mediatype_extension)
        media_sizes.append(
            MediaSize(
                media_type=mediatype_extension,
                name="intrinsic",
                width=1920,
                height=960,
            )
        )
        media_sizes.append(
            MediaSize(
                media_type=mediatype_extension,
                name="optimized",
                width=960,
                height=480,
            )
        )
    MediaType.objects.bulk_create(media_types)
    MediaSize.objects.bulk_create(media_sizes)


def reverter(apps, schema_editor):
    MediaType = apps.get_model("applications", "MediaType")
    all_extensions = AppExtension.objects.all()
    MediaType.objects.filter(
        name="preview",
    ).delete()


class Migration(migrations.Migration):

    dependencies = [
        ("applications", "0011_alter_mediatype_name"),
    ]

    operations = [
        migrations.AlterField(
            model_name="mediatype",
            name="name",
            field=models.CharField(
                choices=[
                    ("thumbnail", "Thumbnail"),
                    ("icon", "Icon"),
                    ("app-icon", "App Icon"),
                    ("preview", "Preview"),
                ],
                default="thumbnail",
                help_text="Name of this media type",
                max_length=32,
                verbose_name="Name",
            ),
        ),
        migrations.RunPython(converter, reverse_code=reverter),
    ]

模型代码

class MediaType(UUIDModel):
    class TypeChoices(models.TextChoices):
        THUMBNAIL = "thumbnail", "Thumbnail"
        ICON = "icon", "Icon"
        APP_ICON = "app-icon", "App Icon"
        PREVIEW = "preview", "Preview"

    extension = models.ForeignKey(
        AppExtension,
        on_delete=models.CASCADE,
        related_name="media_types",
        verbose_name=_("AppExtension"),
        help_text="AppExtension this MediaType belongs to",
    )

    name = models.CharField(
        max_length=32,
        verbose_name=_('Name'),
        help_text=f"Name of this media type",
        choices=TypeChoices.choices,
        default=TypeChoices.THUMBNAIL,
    )

    @property
    def sizes_dict(self) -> dict:
        if self.sizes.exist():
            return {size.name: size.get_tuple() for size in self.sizes.all()}
        return {}

    def __str__(self) -> str:
        return f"{self.extension.name} - {self.name}"

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["extension", "name"],
                name="unique_media_type_name_per_extension",
            ),
        ]


class MediaSize(UUIDModel):
    class SizeChoices(models.TextChoices):
        INTRINSIC = "intrinsic", "Intrinsic"
        OPTIMIZED = "optimized", "Optimized"

    media_type = models.ForeignKey(
        MediaType,
        on_delete=models.CASCADE,
        verbose_name=_('Media Type'),
        help_text="Media type this size reffers to",
        related_name="sizes",
    )

    name = models.CharField(
        max_length=32,
        verbose_name=_('Name'),
        help_text=f"Name of this media size",
        choices=SizeChoices.choices,
        default=SizeChoices.INTRINSIC,
    )

    width = models.PositiveIntegerField(
        verbose_name=_('Width'),
        help_text="Width of this media size",
    )

    height = models.PositiveIntegerField(
        verbose_name=_('Height'),
        help_text="Height of this media size",
    )

    def get_tuple(self) -> Tuple[int, int]:
        return self.width, self.height

    def __str__(self) -> str:
        return (
            f"{self.media_type.extension.name} - {self.media_type.name}"
            + f" - {self.name}: ({self.get_tuple()})"
        )

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["media_type", "name"],
                name="unique_media_size_name_per_media_type",
            ),
            UniqueConstraint(
                fields=["media_type", "height", "width"],
                name="unique_media_size_dimensions_per_media_type",
            ),
        ]

原因分析

这是Django迁移反向执行时的状态模型与数据库数据不匹配导致的:
反向回退单个迁移时,操作执行顺序为先运行RunPython的reverse_code,再执行AlterField的反向操作。删除MediaType时,CASCADE会触发查询关联的MediaSize,但此时from_state.apps中的MediaSize模型是迁移前的版本(未关联新增的preview类型),而数据库中的MediaSize实例是由正向迁移创建的,二者属于不同的模型类对象,导致类型校验失败。

解决方案

方案1:拆分迁移文件

将AlterField和RunPython操作拆分为两个独立的迁移:

  1. 第一个迁移仅执行AlterField,添加preview选项;
  2. 第二个迁移执行RunPython,创建对应数据。
    反向回退时,会先回退第二个迁移(删除数据),再回退第一个迁移(移除preview选项),避免模型状态与数据的冲突。

方案2:修改reverter主动删除关联数据

在reverter中显式删除关联的MediaSize,再删除MediaType,避免CASCADE触发的类型不匹配查询:

def reverter(apps, schema_editor):
    MediaType = apps.get_model("applications", "MediaType")
    MediaSize = apps.get_model("applications", "MediaSize")
    # 先删除关联的MediaSize实例
    MediaSize.objects.filter(media_type__name="preview").delete()
    # 再删除目标MediaType实例
    MediaType.objects.filter(name="preview").delete()

内容的提问来源于stack exchange,提问作者Denise Albertazzi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 16:15:41