如何编写迁移以使用 ManyToManyField 更改模型的主键

Posted

技术标签:

【中文标题】如何编写迁移以使用 ManyToManyField 更改模型的主键【英文标题】:How to write migration to change primary key of model with ManyToManyField 【发布时间】:2016-02-20 03:39:07 【问题描述】:

我有一个 UserProfile 模型,它使用 OneToOneField 引用我的 User 模型。我还使用post_save 信号在创建用户时自动创建UserProfile。除了通过管理员(我使用内联)创建用户时,当我收到有关重复配置文件的错误时,这非常有用。 This answer recommends setting the primary key to be the OneToOneField referring to user.

那么之前:

class UserProfile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL)
    # ...
    subjects = models.ManyToManyField(Subject, null=True, blank=True)

之后

class UserProfile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
    # ...
    subjects = models.ManyToManyField(Subject, null=True, blank=True)

我正在尝试使用 Django 1.7 中的迁移来做到这一点,但由于配置文件有多个 ManyToManyField,因此生活变得复杂 - 所以它们都引用了 UserProfileid 字段模型。使用 makemigrations 创建迁移以使用户成为主键,并删除旧的 id 字段,但它忽略了 ManyToManyField。

我目前正陷入困境,在迁移中使用大量 RunSQL 语句来修改 ManyToManyField 的直通表。我刚刚遇到另一个错误,其中一个表中的约束名称与另一个表中的不同。

所以我的问题是:在 Django 迁移中是否有一种方法可以完成更改直通表的工作,使其引用新的主键,更新所有约束、键等?如果没有,处理这种情况的最佳方法是什么?

我将 Django 1.7 与 mysql 一起使用。

【问题讨论】:

我会提出解决方案不正确的论点。应修改您的信号以不尝试创建重复的配置文件。主键解决方案对我来说似乎很老套。 @Kye - 信号处理程序创建配置文件管理员内联尝试创建配置文件。因此,信号处理程序必须确定用户是由管理员创建的,以便知道管理员随后会尝试创建用户 - 它不能简单地测试该配置文件是否已存在于数据库中。如果配置文件在创建之前已经存在,另一种方法是让管理员内联测试,但这对我来说感觉很糟糕。 @HamishDowner 看起来像是一个错误:code.djangoproject.com/ticket/25012 【参考方案1】:

所以我最终使用 SQL 来修复它。我的解决方案的核心如下 - 基本上我

在新配置文件的 user_id 上创建索引 此索引必须存在才能将其作为外键引用 创建一个新的直通表 我从SHOW CREATE TABLE userprofile_userprofile_subjects(特定于MySQL)的输出开始 我稍微修改了键名和约束名 将所有数据复制到新的直通表中 通过表删除旧的 将新的直通表重命名为旧直通表的名称 终于完成了 django 迁移自动为我生成的操作

我希望这对其他人有所帮助。而且我仍然有兴趣了解更好的解决方案。

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        # ...
    ]

    operations = [
        migrations.RunSQL(
            'ALTER TABLE userprofile_userprofile '
            'ADD INDEX `userprofile_userprofile_1234abcd` (user_id)'
        ),
        migrations.RunSQL (
            'CREATE TABLE userprofile_temp_table ('
            '`id` int(11) NOT NULL AUTO_INCREMENT, '
            '`userprofile_id` int(11) NOT NULL, '
            '`subject_id` int(11) NOT NULL, '
            'PRIMARY KEY (`id`), '
            'UNIQUE KEY `userprofile_userprofile_subjects_userprofile_us_7ded3060_uniq` (`userprofile_id`,`subject_id`), '
            'KEY `userprofile_userprofile_subject_1be9924f` (`userprofile_id`), '
            'KEY `userprofile_userprofile_subject_e5a9504a` (`subject_id`), '
            'CONSTRAINT `subject_id_refs_id_69796996` FOREIGN KEY (`subject_id`) REFERENCES `otherapp_subject` (`id`), '
            'CONSTRAINT `userprofile_user_id_refs_user_id_1234abcd` FOREIGN KEY (`userprofile_id`) REFERENCES `userprofile_userprofile` (`user_id`) '
            ') ENGINE=InnoDB AUTO_INCREMENT=35500 DEFAULT CHARSET=utf8 '
        ),
        migrations.RunSQL (
            'INSERT INTO userprofile_temp_table '
            '(userprofile_id, subject_id) '
            '('
            '  SELECT userprofile_userprofile.user_id, userprofile_userprofile_subjects.subject_id'
            '    FROM userprofile_userprofile_subjects'
            '    INNER JOIN userprofile_userprofile'
            '    ON userprofile_userprofile_subjects.userprofile_id ='
            '        userprofile_userprofile.id'
            ')'
        ),
        migrations.RunSQL (
            'DROP TABLE `userprofile_userprofile_subjects`'
        ),
        migrations.RunSQL (
            'RENAME TABLE `userprofile_temp_table` TO `userprofile_userprofile_subjects`'
        ),
        migrations.RemoveField(
            model_name='userprofile',
            name='id',
        ),
        migrations.AlterField(
            model_name='userprofile',
            name='user',
            field=models.OneToOneField(
                primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL
            ),
            preserve_default=True,
        ),
    ]

【讨论】:

感谢@Hamish - 您的解决方案帮助我决定不使用自定义主键和具有 m2m 字段的模型:) 在 Django 处理该问题时遇到了一些错误(#25012、#24030、@ 987654323@)。详情here.

以上是关于如何编写迁移以使用 ManyToManyField 更改模型的主键的主要内容,如果未能解决你的问题,请参考以下文章

将 ManyToManyField 迁移为 null tr​​ue,空白 true,无法识别

如何编写迁移以重命名 Rails 中的 ActiveRecord 模型及其表?

您如何编写 Rails 迁移以删除 schema.rb 中的 :id => false 选项?

Python/Django - 在 for 标记中获取 ManyToManyField 的值

关于 django ManyToManyField

Django 1.8:删除/重命名数据迁移中的模型字段