简体   繁体   English

在Django中自动生成自定义迁移

[英]Automatically generate custom migrations in Django

Django provides a really nice feature called makemigrations where it will create migration files based on the changes in models. Django提供了一个非常不错的功能,称为makemigrations ,它将根据模型的更改创建迁移文件。 We are developing a module where we will want to generate custom migrations. 我们正在开发一个模块,我们将在该模块中生成自定义迁移。

I haven't found much info about creating custom migrations in the Django docs. 我在Django文档中找不到有关创建自定义迁移的很多信息。 There is documentation on the various Operation classes that can yield migrations but there's nothing about creating custom Operation classes that can yield custom migrations. 有关于可以产生迁移的各种Operation类的文档,但是关于创建可以产生自定义迁移的自定义Operation类没有任何内容。

The autodetector module for generating new migrations also doesn't seem to leave much room for adding custom Operation classes: https://github.com/django/django/blob/master/django/db/migrations/autodetector.py#L160 用于生成新迁移的autodetector模块似乎也没有为添加自定义Operation类留出太多空间: https : //github.com/django/django/blob/master/django/db/migrations/autodetector.py#L160

It seems this is completely static. 看来这是完全静态的。 Are there other ways to generate custom migrations, perhaps by using existing classes with a custom management command? 还有其他方法可以生成自定义迁移,也许是通过将现有类与自定义管理命令一起使用来生成的?

You can create a custom class to hook into the makemigrations class and add your custom migrations stuff then execute using the "runscript" command. 您可以创建一个自定义类以连接到makemigrations类中,并添加您的自定义迁移内容,然后使用“ runscript”命令执行。 Below is a sample module where the file is named custom_migrations.py and located in a "scripts" folder off one of your apps: 以下是一个示例模块,其中文件名为custom_migrations.py,位于其中一个应用程序的“脚本”文件夹中:

from django.core.management.commands.makemigrations import Command
"""
To invoke this script use:
   manage.py runscript custom_migrations --script-args [app_label [app_label ...]] name=my_special_migration verbosity=1
"""    

class MyMigrationMaker(Command):
    '''
    Override the write method to add more stuff before finishing
    '''


    def write_migration_files(self, changes):
        print("Do some stuff to \"changes\" object here...")
        super().write_migration_files(changes)



def run(*args):
    nargs = []
    kwargs = {}
    # Preload some options with defaults and then can be overridden in the args parsing
    kwargs['empty'] = True
    kwargs['verbosity'] = 1
    kwargs['interactive'] = True
    kwargs['dry_run'] = False
    kwargs['merge'] = False
    kwargs['name'] = 'custom_migration'
    kwargs['check_changes'] = False
    for arg in args:
        kwarg = arg.split('=', 1)
        if len(kwarg) > 1:
            val = kwarg[1]
            if val == "True":
                arg_val = True
            elif val == "False":
                arg_val = False
            elif val.isdigits():
                arg_val = int(val)
            else:
                arg_val = val
            the_kwargs[kwarg[0]] = arg_val
        else:
            nargs.append(arg)
    MyMigrationMaker().handle(*nargs, **kwargs)

An alternative if you are not willing to Tango with Django internals is to use this script below to generate a migration file by invoking a random method that will produce the Python code you want to run in the migration and insert it into a valid migration file that will then be part of the standard Django migrations. 如果您不愿意使用Django内部结构的Tango,可以使用以下脚本通过调用随机方法来生成迁移文件,该方法将生成您要在迁移中运行的Python代码并将其插入到有效的迁移文件中,然后将成为标准Django迁移的一部分。 It you had an app named "xxx" and you had a method in a file xxx/scripts/test.py looking like this: 如果您有一个名为“ xxx”的应用,并且在文件xxx / scripts / test.py中有一个方法,如下所示:

def run(*args, **kwargs):
return "print(\"BINGO!!!!!!!!! {} :: {}\".format(args[0], kwargs['name']))"

... and you invoked the script shown at the bottom of this post stored in xxx/scripts/custom_migrations.py with the following command ...,您使用以下命令调用了存储在xxx / scripts / custom_migrations.py中的此帖子底部显示的脚本

manage.py runscript custom_migrations --script-args xxx name=say_bingo callable=xxx.scripts.test.run

Then you would end up with a migration file in xxx/migrations with the appropriate number sequence (something like 0004_say_bingo.py) looking like this: 然后,您将得到一个xxx / migrations中的迁移文件,并带有适当的数字序列(类似于0004_say_bingo.py),如下所示:

 Generated by Django 2.1.2 on 2018-12-14 08:54

from django.db import migrations
def run(*args, **kwargs):
    print("BINGO!!!!!!!!! {} :: {}".format(args[0], kwargs['name']))

class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.RunPython(run,)
    ]

The script is as follows: 脚本如下:

from django.core.management.base import no_translations
from django.core.management.commands.makemigrations import Command
from django.db.migrations import writer
from django import get_version
from django.utils.timezone import now
import os
import sys

"""
To invoke this script use:
    manage.py runscript custom_migrations --script-args [app_label [app_label ...]] callable=my_migration_code_gen name=my_special_migration

  -- the "name" argument will be set as part of the migration file generated
  -- the "callable" argument will be the function that is invoked to generate the python code you want to execute in the migration
    the callable will be passed all the args and kwargs passed in to this script from the command line as part of --script-args
    Only app names are allowed in args so use kwargs for custom arguments
"""


class LazyCallable(object):
    def __init__(self, name):
    self.n, self.f = name, None

    def __call__(self, *a, **k):
        if self.f is None:
            modn, funcn = self.n.rsplit('.', 1)
            if modn not in sys.modules:
                __import__(modn)
            self.f = getattr(sys.modules[modn], funcn)
        return self.f(*a, **k)


class MyMigrationMaker(Command):
    '''
    Override the write method to provide access to script arguments
    '''

    @no_translations
    def handle(self, *app_labels, **options):
        self.in_args = app_labels
        self.in_kwargs = options
        super().handle(*app_labels, **options)

    '''
    Override the write method to add more stuff before finishing
    '''

    def write_migration_files(self, changes):
        code = LazyCallable(self.in_kwargs['callable'])(self.in_args, self.in_kwargs)
        items = {
            "replaces_str": "",
            "initial_str": "",
        }
        items.update(
            version=get_version(),
            timestamp=now().strftime("%Y-%m-%d %H:%M"),
        )
        items["imports"] = "from django.db import migrations\n\ndef run(*args, **kwargs):\n    "
        items["imports"] += code.replace("\n", "\n    ") + "\n\n"
        items["operations"] = "        migrations.RunPython(run,)\n"
        directory_created = {}
        for app_label, app_migrations in changes.items():
            for migration in app_migrations:
                # Describe the migration
                my_writer = writer.MigrationWriter(migration)
                dependencies = []
                for dependency in my_writer.migration.dependencies:
                    dependencies.append("        %s," % my_writer.serialize(dependency)[0])
                    items["dependencies"] = "\n".join(dependencies) + "\n" if dependencies else ""

                # Write the migrations file to the disk.
                migrations_directory = os.path.dirname(my_writer.path)
                if not directory_created.get(app_label):
                    if not os.path.isdir(migrations_directory):
                        os.mkdir(migrations_directory)
                    init_path = os.path.join(migrations_directory, "__init__.py")
                    if not os.path.isfile(init_path):
                        open(init_path, "w").close()
                    # We just do this once per app
                    directory_created[app_label] = True
                migration_string =  writer.MIGRATION_TEMPLATE % items
                with open(my_writer.path, "w", encoding='utf-8') as fh:
                    fh.write(migration_string)
                if self.verbosity >= 1:
                    self.stdout.write("Migration file: %s\n" % my_writer.filename)



def run(*args):
    glob_args = []
    glob_kwargs = {}

    # Preload some options with defaults and then can be overridden in the args parsing
    glob_kwargs['empty'] = True
    glob_kwargs['verbosity'] = 1
    glob_kwargs['interactive'] = True
    glob_kwargs['dry_run'] = False
    glob_kwargs['merge'] = False
    glob_kwargs['name'] = 'custom_migration'
    glob_kwargs['check_changes'] = False
    for arg in args:
        kwarg = arg.split('=', 1)
        if len(kwarg) > 1:
            glob_kwargs[kwarg[0]] = kwarg[1]
        else:
            glob_args.append(arg)
    MyMigrationMaker().handle(*glob_args, **glob_kwargs)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM