Django:如何为两个相关模型组织迁移并自动为新创建的对象的ID设置默认字段值?

时间:2019-05-31 14:36:35

标签: python django django-migrations

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

有一个模型(已经存在于db中),例如public void parseJSONData(String response) { try { JSONObject obj = new JSONObject(response); JSONArray dataArray = obj.getJSONArray("data"); for (int i = 0; i < dataArray.length(); i++) { JSONObject dataobj = dataArray.getJSONObject(i); haberModel1.setTitle(dataobj.getString("header")); haberModel1.setUrl(dataobj.getString("picture")); haberModel1.setId(dataobj.getString("id")); haberModelArrayList.add(haberModel1); setupRecycler(); } } catch (JSONException e) { Log.d(""+TAG,e.printStackTrace() ); } } ,它具有其他模型的外键。

private void setupRecycler() {
        lManager = new LinearLayoutManager(getApplicationContext(), LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(lManager);
        rvAdapter = new RvAdapter(this, haberModelArrayList);
        recyclerView.setAdapter(rvAdapter);
        isLoading = false;
    }

我们需要再创建一个Model应该引用的模型class ModelA: ... class ModelX: ... class Model:   a = models.ForeignKey(ModelA, default = A)   x = models.ForeignKey(ModelX, default = X) 。并且在创建ModelY时,对象应该具有与某个Model对象相关的默认值,该默认值显然尚不可用,但是我们应该在迁移期间创建它。

Model

因此迁移顺序应为:

  • 创建ModelY
  • 在此表中创建一个默认对象,将其ID放在某处
  • class ModelY: ... class Model:   y = models.ForeignKey (ModelY, default = ??????) 表中创建一个新字段ModelY,并使用默认值 来自上一段

当然,我想将所有这些自动化。因此,为了避免手动进行一次迁移,然后创建一些对象,然后写下其ID,然后将此ID用作新字段的默认值,然后再对该新字段进行另一次迁移,就可以了。

我也想一步一步做完所有事情,因此在旧模型中同时定义y和新字段Model,生成迁移,以某种方式解决,然后应用于一次,使其工作。

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

3 个答案:

答案 0 :(得分:3)

您将无法在单个迁移文件中执行此操作,但是您将能够创建多个迁移文件来实现此目的。尽管我不确定您要的是什么,但我会尽全力帮助您,它应该教给您关于Django迁移的一两件事。

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

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。

答案 1 :(得分:0)

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

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

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

因此,我决定将所有数据迁移都保留在migrations软件包之外的单个文件中。因此,我在应用程序包中创建了data.py,并将所有数据迁移都放在单个函数migratedata中,请记住,可以在早期阶段调用此函数,而此时某些模型可能仍然不存在,因此我们需要捕获LookupError个应用注册表访问的异常。比起数据迁移中的每个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进行迁移(删除{ {1}}表之前)。

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

最后有django_migrations个模型源代码:

Defaultable

答案 2 :(得分:0)

我留下了先前的答案只是为了表达对思想的搜索。最终,我建立了全自动解决方案,因此不再需要手动编辑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