Django:如何从 ManyToMany 迁移到 ForeignKey?

Django: How to migrate from ManyToMany to ForeignKey?

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

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

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

我有 98% 的把握服务器上的所有数据都将与 FK 关系保持一致(意味着我很确定所有房屋都只有一个城市)。由于多种原因,我们需要更改关系,无法保留 M2M。

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

非常感谢。

编辑 首先创建数据库备份

首先建立一个新的临时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

对于 M2M 关系,最好构建一个新的模型来表示关系。以下是一个`employee having multiple designation 和 designation 由多名员工获得的例子

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'),)

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

之后,您可以按步骤进行:

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

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

  1. 我添加了一个像这样的字段 city = models.ForeignKey(City, related_name='has_houses', blank=True, null=True) 以避免反向关系的 related_name 并让 FK 空白。然后我运行makemigrationsmigrate.

  2. 接下来,我运行python manage.py makemigrations --empty houses因为我的app取名为houses.

  3. 我添加了迁移代码(见下文)。然后我运行migrate.

  4. 我删除了 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),
    ]

我看到答案是说您必须创建多个迁移文件才能执行此操作,但这不是很好的做法,一段时间后您可能会将迁移压缩在一起。 只需创建一个空迁移或使用以下命令创建一个自动迁移:

python manage.py makemigrations

并为现有实例提供默认值。

之后,您可以更改文件并修改更改并应用数据迁移而不会丢失任何数据。 在添加新字段后创建函数和 运行 它们,之后,您可以删除它们并正确重命名新字段。

migrations.AddField(...),

migrations.RunPython(save_city_fk),

migrations.RemoveField(...),

migrations.AlterField(...),

他们订购很重要。