【问题标题】:How to organize migration for two related models and automatically set default field value for id of newly created object?如何组织两个相关模型的迁移并为新创建的对象的 id 自动设置默认字段值?
【发布时间】:2019-10-17 05:19:27
【问题描述】:

假设有一个生产数据库,里面有一些数据。我需要在下一个棘手的情况下迁移。

有一个模型(已经在数据库中),比如Model,它有其他模型的外键。

class ModelA: ...
class ModelX: ...

class Model:
  a = models.ForeignKey(ModelA, default = A)
  x = models.ForeignKey(ModelX, default = X)

我们需要再创建一个ModelYModel 应该引用的模型。并且在创建Model时,对象应该有一些与ModelY对象相关的默认值,这显然还没有,但我们应该在迁移过程中创建它。

class ModelY: ...
class Model:
  y = models.ForeignKey (ModelY, default = ??????)

所以迁移顺序应该是:

  • 创建ModelY
  • 在此表中创建一个默认对象,将其 id 放在某处
  • Model表中新建字段y,取默认值 上一段

当然,我想自动化所有这些。因此,为了避免手动应用一次迁移,然后创建一些对象,然后记下它的 id,然后将此 id 用作新字段的默认值,然后才使用此新字段应用另一次迁移。

而且我也想一步完成,所以在旧模型中同时定义ModelY和一个新字段y,生成迁移,以某种方式修复它,然后立即应用并制作它工作。

对于这种情况有什么最佳做法吗?特别是,在哪里存储这个新创建的对象的 id?同一个数据库中的一些专用表?

【问题讨论】:

  • ModelY 的表中应该有多少条记录?你确定你需要一个真正的模型吗?
  • 不止一个。是的,它应该是模型,即它应该能够在 django 的管理界面中创建。另外我想避免假设新创建的 id 是 1。
  • 也许这个模型应该有一个“自然”ID而不是代理ID?让我们建议ModelY 是颜色,而不是您可以拥有一个字段code = models.CharField(max_lenght=20, primary_key=True),并且您可以拥有一个使用code = 'red' 添加记录的夹具。关于 FK 字段,我宁愿保持它为空,并在信号或覆盖.save() 设置默认值,如果值为空。
  • 是的,它会以某种方式工作,但是当主键是 char 字段时,ModelY 表中的其他对象呢?会自动生成吗?我们能保证在这种情况下不会发生冲突吗?
  • 不,它不会自动生成,但保证不会有重复,因为主键还添加了唯一约束。换句话说,您必须手动设置主键,但如果您使用一些有意义的东西(例如上面案例中的颜色代码),这应该不是问题。

标签: python django django-migrations


【解决方案1】:

您无法在单个迁移文件中执行此操作,但是您可以创建多个迁移文件来实现此目的。我会尽力帮助你,虽然我不完全确定这是你想要的,它应该教你一两件事关于 Django 迁移。

我将在这里提到两种类型的迁移,一种是架构迁移,这些是您通常在更改模型后生成的迁移文件。另一种是数据迁移,需要使用makemigrations 命令的--empty 选项创建,例如python manage.py makemigrations my_app --empty,用于移动数据,设置需要更改为非空的空列上的数据等。

class ModelY(models.Model):
    # Fields ...
    is_default = models.BooleanField(default=False, help_text="Will be specified true by the data migration")

class Model(models.Model):
    # Fields ...
    y = models.ForeignKey(ModelY, null=True, default=None)

您会注意到y 接受null,我们稍后可以更改它,现在您可以运行python manage.py makemigrations 来生成架构迁移。

要生成您的第一个数据迁移,请运行命令python manage.py makemigrations <app_name> --empty。您将在迁移文件夹中看到一个空的迁移文件。您应该添加两种方法,一种用于创建您的默认 ModelY 实例并将其分配给您现有的 Model 实例,另一种将是存根方法,以便 Django 稍后允许您在需要时反转您的迁移。

from __future__ import unicode_literals

from django.db import migrations


def migrate_model_y(apps, schema_editor):
    """Create a default ModelY instance, and apply this to all our existing models"""
    ModelY = apps.get_model("my_app", "ModelY")
    default_model_y = ModelY.objects.create(something="something", is_default=True)

    Model = apps.get_model("my_app", "Model")
    models = Model.objects.all()
    for model in models:
        model.y = default_model_y
        model.save()


def reverse_migrate_model_y(apps, schema_editor):
    """This is necessary to reverse migrations later, if we need to"""
    return


class Migration(migrations.Migration):

    dependencies = [("my_app", "0100_auto_1092839172498")]

    operations = [
        migrations.RunPython(
            migrate_model_y, reverse_code=reverse_migrate_model_y
        )
    ]

不要直接将您的模型导入此迁移!模型需要通过apps.get_model("my_app", "my_model") 方法返回,以便获得在此迁移时间点的模型。如果将来您添加更多字段并运行此迁移,您的模型字段可能与数据库列不匹配(因为模型来自未来,有点......),您可能会收到一些关于数据库中缺少列的错误,并且这样的。还要小心在迁移中对模型/管理器使用自定义方法,因为您无法从此代理模型访问它们,通常我可能会将一些代码复制到迁移中,因此它始终运行相同。

现在我们可以返回并修改Model 模型,以确保y 不为空,并且它会在将来选择默认的ModelY 实例:

def get_default_model_y():
    default_model_y = ModelY.objects.filter(is_default=True).first()
    assert default_model_y is not None, "There is no default ModelY to populate with!!!"
    return default_model_y.pk  # We must return the primary key used by the relation, not the instance

class Model(models.Model):
    # Fields ...
    y = models.ForeignKey(ModelY, default=get_default_model_y)

现在您应该再次运行 python manage.py makemigrations 以创建另一个架构迁移。

您不应该混合架构迁移和数据迁移,因为迁移被包装在事务中的方式可能会导致数据库错误,这些错误会抱怨尝试在事务中创建/更改表和执行 INSERT 查询。

最后你可以运行python manage.py migrate,它应该会创建一个默认的ModelY对象,将它添加到你的模型的ForeignKey中,然后删除null,使它像一个默认的ForeignKey。

【讨论】:

  • A. J.,非常感谢,本教程非常有帮助。现在我正在使用您的示例,我想了解的是在哪个点为y 设置了默认值。据我了解,没有办法在 sql 模式本身中提供默认值,对吗?所以 y 在 Django 创建 ModelY 实例时设置。但是get_default_model_y 在迁移过程中被调用,为什么!?如果没有调用,迁移可以分两步进行,避免使用可空y 进行初始迁移。另外我要感谢is_default 解决方案,这是最好的,所以我们可以在管理员中更改默认对象。
  • 其实我想我们可以跳过第一步创建y,所以更简单的顺序是1)创建ModelY,不要碰Model 2)创建数据迁移,我们在这里只创建ModelY 对象,不再需要在Model 管理空值 3) 为Model 创建y 并具有可调用的默认值。再次不清楚为什么在迁移期间调用它,没有它,序列可以是 2 步。我还担心的是,Django 如何管理此类序列的压缩?它理解正确吗?
  • 啊,所以使用这种方法可能会发现可调用默认值不适用于迁移,因为您正在从迁移外部运行一些代码(尽管值得一试)。但是,我认为当它创建非空 y 列时,它需要一个在 SQL 中可序列化的默认值,而可调用的默认值可能会在这一步引起问题。
【解决方案2】:

最后我找到了以下解决方案。

首先我接受通过isDefault属性识别默认对象的想法,并编写了一些抽象模型来处理它,尽可能保持数据完整性(代码在帖子底部)。

我不太喜欢接受的解决方案是数据迁移与架构迁移混合在一起。很容易丢失它们,即在挤压过程中。有时我也会完全删除迁移,当我确定我的所有生产和备份数据库都与代码一致时,我可以生成单个初始迁移并伪造它。将数据迁移与模式迁移结合在一起会破坏此工作流程。

所以我决定将所有数据迁移保存在migrations 包之外的单个文件中。所以我在我的应用程序包中创建data.py,并将所有数据迁移放在单个函数migratedata中,记住这个函数可以在早期调用,当某些模型可能还不存在时,所以我们需要捕获@987654325 @ 应用程序注册表访问异常。比我对数据迁移中的每个RunPython 操作都使用此功能。

所以工作流程看起来像这样(我们假设 ModelModelX 已经到位):

1) 创建ModelY:

class ModelY(Defaultable):
    y_name = models.CharField(max_length=255, default='ModelY')

2) 生成迁移:

manage.py makemigration

3) 在data.py 中添加数据迁移(在我的情况下将模型名称添加到defaultable 列表中):

# data.py in myapp
def migratedata(apps, schema_editor):
    defaultables = ['ModelX', 'ModelY']

    for m in defaultables:
        try:
            M = apps.get_model('myapp', m)
            if not M.objects.filter(isDefault=True).exists():
                M.objects.create(isDefault=True)
        except LookupError as e:
            print '[{} : ignoring]'.format(e)

    # owner model, should be after defaults to support squashed migrations over empty database scenario
    Model = apps.get_model('myapp', 'Model')
    if not Model.objects.all().exists():
        Model.objects.create()

4) 通过添加操作RunPython编辑迁移:

from myapp.data import migratedata
class Migration(migrations.Migration):
    ...
    operations = [
        migrations.CreateModel(name='ModelY', ...),
        migrations.RunPython(migratedata, reverse_code=migratedata),
    ]

5) 将ForeignKey(ModelY) 添加到Model

class Model(models.Model):
    # SET_DEFAULT ensures that there will be no integrity issues, but make sure default object exists
    y = models.ForeignKey(ModelY, default=ModelY.default, on_delete=models.SET_DEFAULT)

6) 再次生成迁移:

manage.py makemigration

7) 迁移:

manage.py migrate

8) 完成!

整个链可以应用于空数据库,它将创建最终模式并用初始数据填充它。

当我们确定我们的数据库与代码同步时,我们可以轻松删除一长串迁移,生成单个初始迁移,将RunPython(migratedata, ...) 添加到其中,然后使用--fake-initial 迁移(删除django_migrations 表之前)。

嗯,如此简单的任务的解决方案如此棘手!

终于有Defaultable模型源码了:

class Defaultable(models.Model):
    class Meta:
        abstract = True

    isDefault = models.BooleanField(default=False)

    @classmethod
    def default(cls):
        # type: (Type[Defaultable]) -> Defaultable
        """
        Search for default object in given model.
        Returning None is useful when applying sqashed migrations on empty database,
        the ForeignKey with this default can still be non-nullable, as return value
        is not used during migration if there is no model instance (Django is not pushing
        returned default to the SQL level).

        Take a note on only(), this is kind of dirty hack  to avoide problems during 
        model evolution, as default() can be called in migrations within some 
        historical project state, so ideally we should use model from this historical
        apps registry, but we have no access to it globally. 

        :return: Default object id, or None if no or many.
        """

        try:
            return cls.objects.only('id', 'isDefault').get(isDefault=True).id
        except cls.DoesNotExist:
            return None

    # take care of data integrity
    def save(self, *args, **kwargs):
        super(Defaultable, self).save(*args, **kwargs)
        if self.isDefault:  # Ensure only one default, so make all others non default
            self.__class__.objects.filter(~Q(id=self.id), isDefault=True).update(isDefault=False)
        else:  # Ensure at least one default exists
            if not self.__class__.objects.filter(isDefault=True).exists():
                self.__class__.objects.filter(id=self.id).update(isDefault=True)

    def __init__(self, *args, **kwargs):
        super(Defaultable, self).__init__(*args, **kwargs)

        # noinspection PyShadowingNames,PyUnusedLocal
        def pre_delete_defaultable(instance, **kwargs):
            if instance.isDefault:
                raise IntegrityError, "Can not delete default object {}".format(instance.__class__.__name__)

        pre_delete.connect(pre_delete_defaultable, self.__class__, weak=False, dispatch_uid=self._meta.db_table)

【讨论】:

    【解决方案3】:

    我留下我之前的答案只是为了显示搜索想法。最后我建立了全自动解决方案,因此不再需要手动编辑 django 生成的迁移,但代价是猴子补丁,经常。

    这个想法是为默认的 ForeignKey 提供可调用的,如果它不存在,它会创建引用模型的默认实例。但问题是,这个可调用对象不仅可以在最终的 Django 项目阶段调用,还可以在迁移期间使用旧项目阶段调用,因此可以在模型仍然存在的早期阶段为已删除的模型调用它。

    RunPython 操作中的标准解决方案是在迁移状态下使用应用程序注册表,但此功能对我们的可调用对象不可用,因为此注册表作为 RunPython 的参数提供,并且在全局范围内不可用。但是为了支持迁移应用和回滚的所有场景,我们需要检测我们是否在迁移,并访问适当的应用程序注册表。

    唯一的解决方案是猴子补丁 AddField 和 RemoveField 操作,以保持迁移应用程序注册表在全局变量中,如果我们正在迁移。

    migration_apps = None
    
    
    def set_migration_apps(apps):
        global migration_apps
        migration_apps = apps
    
    
    def get_or_create_default(model_name, app_name):
        M = (migration_apps or django.apps.apps).get_model(app_name, model_name)
    
        try:
            return M.objects.get(isDefault=True).id
    
        except M.DoesNotExist as e:
            o = M.objects.create(isDefault=True)
            print '{}.{} default object not found, creating default object : OK'.format(model_name, app_name)
            return o
    
    
    def monkey_patch_fields_operations():
        def patch(klass):
    
            old_database_forwards = klass.database_forwards
            def database_forwards(self, app_label, schema_editor, from_state, to_state):
                set_migration_apps(to_state.apps)
                old_database_forwards(self, app_label, schema_editor, from_state, to_state)
            klass.database_forwards = database_forwards
    
            old_database_backwards = klass.database_backwards
            def database_backwards(self, app_label, schema_editor, from_state, to_state):
                set_migration_apps(to_state.apps)
                old_database_backwards(self, app_label, schema_editor, from_state, to_state)
            klass.database_backwards = database_backwards
    
        patch(django.db.migrations.AddField)
        patch(django.db.migrations.RemoveField)
    

    其余的,包括带有数据完整性检查的 Defaultable 模型在 GitHub repository

    【讨论】:

      猜你喜欢
      • 2013-06-03
      • 2016-09-11
      • 1970-01-01
      • 2021-08-14
      • 2012-09-20
      • 2012-01-10
      • 2011-01-10
      • 2018-11-23
      • 1970-01-01
      相关资源
      最近更新 更多