Django:如何从 ManyToMany 迁移到 ForeignKey?

Posted

技术标签:

【中文标题】Django:如何从 ManyToMany 迁移到 ForeignKey?【英文标题】:Django: How to migrate from ManyToMany to ForeignKey? 【发布时间】:2019-07-09 08:00:40 【问题描述】:

我正在使用 Django 和 Django Rest Framework 构建一个 REST API 和服务器。我们正在使用 postgres 数据库。

我需要简化一个设计糟糕的关系。我们有一个模型 (House) 与另一个模型 (City) 有 ManyToMany 关系。实际上,当这是ForeignKey 关系时就足够了。

我用谷歌搜索并找不到任何博客文章或文档如何正确地朝这个方向迁移。我只能找到另一种方式(FK 到 M2M)。

我 98% 确定服务器上的所有数据都将与 FK 关系一致(这意味着我很确定所有房屋只有一个城市)。我们需要改变关系有几个原因,并且无法保留 M2M。

恐怕只是更改模型并运行makemigrationsmigrate。我想知道,您如何正确地从 M2M 迁移到 FK?有什么需要注意的地方吗?如果令人惊讶的是有多个城市的房屋,我该如何处理数据?如果重要的话,数据集仍然非常小(少于 10k 个条目)。

非常感谢。

【问题讨论】:

【参考方案1】:

EDIT首先创建数据库备份

首先创建一个新的临时FK关系

_city = models.ForeignKey(...)

并进行迁移

python manage.py makemigration
python manage.py migrate

然后您需要通过创建一个空的迁移文件在相关应用中创建new data migration:

python manage.py makemigration --empty myhouseapp

并在该文件中手动将新关系从 M2M 分配给 FK。它看起来像:

from django.db import migrations


def save_city_fk(apps, schema):
    City = apps.get_model('myapp', 'City')
    House = apps.get_model('myapp', 'House')
    for house in House.objects.all():
        house._city = house.cities.all().first()  # Or whatever criterea you want
        house._city.save()


def save_city_m2m(apps, schema):
    # This should do the reverse
    City = apps.get_model('myapp', 'City')
    House = apps.get_model('myapp', 'House')
    for house in House.objects.all():
        if house._city:
            house.cities.add(house._city)


class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.RunPython(save_city_fk, save_city_m2m)
    ]

删除 M2M 字段,然后创建另一个迁移。

python manage.py makemigrations

将 FK 从 _city 重命名为 city 并创建最终迁移

python manage.py makemigrations

然后迁移:

python manage.py migrate

【讨论】:

哇,谢谢,这听起来很完美。我现在就试试看! 没问题,只是小心这些迁移。很容易陷入混乱。 只是不要忘记,如果在运行数据迁移后不断添加新房屋,您也需要迁移它们,因此您可能会感觉到例如。将新外键字段的填充也添加到新实例的 save() 方法中... 我已经完成了,我会接受你的回答。但是我会添加一个详细描述我所做的答案?非常感谢您! ?? 感谢您的出色回答。顺便说一句,只需将 python manage.py makemigration --empty myhouseapp 更改为 python manage.py makemigrations --empty myhouseapp 谢谢【参考方案2】:

基于Timmy's answer 这是我所做的:

    我添加了一个类似 city = models.ForeignKey(City, related_name='has_houses', blank=True, null=True) 的字段,以避免 related_name 用于反向关系并将 FK 留空。然后我跑了makemigrationsmigrate

    接下来,我运行了python manage.py makemigrations --empty houses,因为我的应用名为houses

    我添加了迁移代码(见下文)。然后我跑了migrate

    我删除了 M2M 字段和 related_namenullblank 约束,最后一次运行了 makemigrationsmigrate

迁移代码:

# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2019-02-15 09:09
from __future__ import unicode_literals

from django.db import migrations


def save_city_fk(apps, schema):
    City = apps.get_model('cities', 'City')
    House = apps.get_model('houses', 'House')
    for house in House.objects.all():
        house.city = house.cities.all().first()
        house.save()


class Migration(migrations.Migration):

    dependencies = [
        ('houses', '0003_auto_20190215_0949'),
    ]

    operations = [
        migrations.RunPython(save_city_fk),
    ]

【讨论】:

对此要注意的一点是,如果您反向运行迁移,您将丢失数据。注意@Timmy 有两个迁移函数;一个用于正向迁移,一个用于反向迁移。这样,如果您取消应用迁移(即迁移到较早的状态),数据将恢复为原来的状态。【参考方案3】:

首先,显然您需要确保(通过进行适当的查询)您确实每栋房屋只有一个城市。如果有房屋拥有多个城市,则需要通过从关系中删除城市、拆分房屋等方式来解决冲突。

之后,你可以分步进行:

House 中创建一个新的 FK,使用旧 m2m 关系中的一个城市 ID 迁移并填充它 重命名 m2m 字段,迁移,然后根据需要为新 FK 指定旧 m2m 字段的名称并再次迁移 检查您的查询是否有效并根据需要进行调整 当对一切正常感到满意时,删除旧的 m2m 字段 - 只有在迁移此步骤后,您才会失去回滚 db 的能力

【讨论】:

【参考方案4】:

对于 M2M 关系,最好建立一个新的模型来表示关系。下面是一个`员工拥有多个职称和多个员工获得的职称的例子

class Designation(models.Model):
    desig_id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=60, unique=True)
    job_discription = models.CharField(max_length=2000)

    def __str__(self):
        return self.title

class Employee(AbstractUser):
    middle_name = models.CharField(max_length=20, blank=True,  null=True)
    basic_salary = models.FloatField(default=1)
    designation = models.ManyToManyField(Designation, default=None, blank=True,
                                     through='EmployeeDesignation')
    certification = models.ManyToManyField(Certification, default=None, null=True, blank=True,
                                        through='EmployeeCertification')
    emp_img = models.FileField(default=None,upload_to='employees')
    leaves_allowed = models.IntegerField(default=25)
    leave_balance = models.IntegerField(default=25)
    leave_count = models.IntegerField(default=0)
    objects = EmployeeManager()

    def __str__(self):
        return self.first_name+' '+self.last_name

class EmployeeDesignation(models.Model):
    desig = models.ForeignKey(Designation, on_delete=models.CASCADE)
    emp = models.ForeignKey(Employee, on_delete=models.CASCADE)

    class Meta:
        unique_together = (('emp', 'desig'),)

【讨论】:

以上是关于Django:如何从 ManyToMany 迁移到 ForeignKey?的主要内容,如果未能解决你的问题,请参考以下文章

如何为包含 on-delete 级联的 ManyToMany 关系创建 Django 迁移?

如何在 Django 中一次将多个对象添加到 ManyToMany 关系?

如何使用 Django Rest Framework 向 ManyToMany 字段添加数据?

Django 从 post_save 信号访问 ManyToMany 字段

django DetailView 没有从 ManyToMany 字段中捕获数据

如何将模型从一个 django 应用程序迁移到一个新应用程序中?