Опыт Django (1) Эффективное использование миграций

Django


использовать и учитьсяDjangoПри кадрировании я обнаружил, что многие люди, в том числе и я,DjangoКогда проект управляется версиями, обычноmigrationsфайл добавлен в.gitignoreсередина.

Автору всегда было интересно, верен ли такой подход, поэтому я пошел проверять официальную документацию и нашел следующий абзац.

оригинал:

You should think of migrations as a version control system for your database schema. makemigrations is responsible for packaging up your model changes into individual migration files - analogous to commits - and migrate is responsible for applying those to your database.

Файлы миграции для каждого приложения находятся в каталоге «миграции» внутри этого приложения и предназначены для фиксации и распространения как части его кодовой базы.Вы должны создать их один раз на своем компьютере для разработки, а затем запустить тот же миграции на компьютерах ваших коллег, ваших промежуточных компьютерах и, в конечном итоге, на ваших производственных компьютерах.

Китайский перевод:

Вы можете представить миграцию как систему контроля версий для вашей базы данных. Команда makemigrations отвечает за сохранение изменений вашей модели в файле миграции — так же, как коммиты — в то время как migrate отвечает за фиксацию изменений в базе данных.

Файл миграции для каждого приложения сохраняется в папке «миграции» каждого соответствующего приложения и подготавливает способ его выполнения в виде распределенной базы кода. Вы должны воссоздавать эти файлы каждый раз, когда запускаете одну и ту же миграцию на своем компьютере для разработки или на компьютере вашего коллеги, а затем и на рабочем компьютере.

Согласно официальной документации,migrationsПрактика фиксации в репозитории неверна.

и если вы хотите использоватьdjangoПоставляется упакованнымTestCaseвыполнять модульные тесты,migrationsтакже должны быть сохранены.

В следующей статье автор также представитdjangoсерединаTestCaseопыт использования.

Далее в проектеmigrationsНекоторый опыт и некоторые проблемы, с которыми столкнулись.

Инициализация данных с миграциями

теперь у нас естьBookМодель, которую я хочу использовать вmigrateПосле этого инициализируйте некоторые данные.

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)

Например: сгенерируйте три имени какHamlet,Tempest,The Little Princeкнига.

в исполненииpython manage.py makemigrationsпослеmigrationsпапка будет создана0001_initial.pyдокумент.

файл содержитBookНекоторый код для инициализации этой модели.

Знакомство с использованиемmigrationsПри инициализации данных сначала введитеmigrationsДве часто используемые операции:

RunSQL,RunPython

Как следует из названия, выполнитьSQL语句иPython函数.

Ниже я используюmigrationsсерединаRunPythonдля инициализации данных.

  1. в соответствующем приложенииmigrationsфайл новый0002_init_book_data.py migrations/.
    ​ ├── 0001_initial.py.
    └── 0002_init_book_data.py.

  2. затем увеличитьMigrationнаследование классовdjango.db.migrations.Migration, И вoperationsДобавьте код, который необходимо выполнить.

make_good_use_of_migrations/migrations/0002_init_book_data.py view raw
from django.db import migrations

"""
make_good_use_of_migrations 是App的名字
"""


def init_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    init_data = ['Hamlet', 'Tempest', 'The Little Prince']
    for name in init_data:
        book = Book(name=name)
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0001_initial'),
    ]

    # 这里要注意dependencies为上一次migrations的文件名称

    operations = [
        migrations.RunPython(init_book_data)
    ]
  1. бегатьpython manage.py migrate, вы можете видеть, что данные были сгенерированы в базе данных.

Восстановление данных с помощью миграции

Мы часто сталкиваемся с такой ситуацией, например мне нужно датьBookМодель добавляет поле внешнего ключа, и это поле не может быть пустым, поэтому старые данные нужно обработать и исправить, мы можем это сделать.

  1. Поля, которые необходимо добавить в первую очередьnullсвойство установлено наTrue, затем выполнитеmakemigrations
make_good_use_of_migrations/models.py view raw
class Author(models.Model):
    name = models.CharField(max_length=32)


class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
  1. в соответствующем приложенииmigrationsфайл новый0004_fix_book_data.py migrations/.
    ├── 0001_initial.py.
    ├── 0002_init_book_data.py.
    ├── 0003_auto_20181204_0533.py.
    └── 0004_fix_book_data.py.
make_good_use_of_migrations/migrations/0004_fix_book_data.py view raw
from django.db import migrations


def fix_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    Author = apps.get_model('make_good_use_of_migrations', 'Author')
    for book in Book.objects.all():
        author, _ = Author.objects.get_or_create(name='%s author' % book.name)
        book.author = author
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0003_auto_20181204_0533'),
    ]

    operations = [
        migrations.RunPython(fix_book_data)
    ]
  1. Наконец,Bookв моделиauthorСвойства поляnullустановить какFalseи выполнитьmakemigrations. После выполнения появится,

    You are trying to change the nullable field 'author' on book to non-nullable without a default; we can't do that (the database needs something to populate existing rows).
    Please select a fix:
     1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
     2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
     3) Quit, and let me add a default in models.py
    Select an option:
    

    Выберите здесь2элемент, что означает игнорировать данные о том, что поле уже пусто, используйтеRunPythonилиRunSQLСделай сам.

    Выполнить после завершения выбораpython manage.py migrate, вы обнаружите, что данные в базе данных будут обрабатываться так, как мы ожидали.

Устранение конфликтов, вызванных миграциями во время разработки с участием нескольких человек.

Чтобы смоделировать многоотраслевую разработку с участием нескольких человек, создайте новыйmaster-2, и версия создаетсяAuthorперед уроком и вBookдобавил в модельremarkполе.

model.pyСодержимое файла следующее:

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    remark = models.CharField(max_length=32, null=True)

migrationsКаталог файлов выглядит следующим образом:

migrations/.
├── 0001_initial.py.
├── 0002_init_book_data.py.
└──0003_book_remark.py.

когда мы ставимmaster-2Код объединен вmaster, ты найдешьmigrationsПовторяющиеся номера появляются в0003и они совместно зависят от0002_init_book_data.

migrations/.
├── 0001_initial.py.
├── 0002_init_book_data.py.
├── 0003_auto_20181204_0533.py
├── 0003_book_remark.py
├── 0004_fix_book_data.py
└──0005_auto_20181204_0610.py.

Затем нужно использовать команду:

python manage.py makemigrations --merge

затем вmigrationsкаталог для создания0006_merge_20181204_0622.pyдокумент

make_good_use_of_migrations/migrations/0006_merge_20181204_0622.py view raw
from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('make_good_use_of_migrations', '0005_auto_20181204_0610'),
        ('make_good_use_of_migrations', '0003_book_remark'),
    ]

    operations = [
    ]

В это время выполняетсяpython manage.py migrateВот и все.

Вопросы, требующие внимания при использовании миграции. RunPython

Невозможно вызвать функцию класса модели в функции

предположим вBookВ модели определены две функцииprint_nameи функции классаprint_class_name

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
    remark = models.CharField(max_length=32, null=True)

    def print_name(self):
        print(self.name)

    @classmethod
    def print_class_name(cls):
        print(cls.__name__)

существуетmigrationsЕго нельзя назвать средним, да и автор не изучал его внимательно.BookПри инициализации класса инициализируются только поля.

make_good_use_of_migrations/migrations/0004_fix_book_data.py view raw
from django.db import migrations


def fix_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    Author = apps.get_model('make_good_use_of_migrations', 'Author')
    for book in Book.objects.all():
        author, _ = Author.objects.get_or_create(name='%s author' % book.name)
        book.author = author
        """
        book.print_name()
        book.print_class_name()
        这样调用会报错
        """
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0003_auto_20181204_0533'),
    ]

    operations = [
        migrations.RunPython(fix_book_data)
    ]

Метод сохранения, переопределенный классом модели в функции, недействителен, включая унаследованный метод сохранения.

существуетmigrationsВсе переписано вsaveметоды не будут работать, например:

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
    remark = models.CharField(max_length=32, null=True)

    def print_name(self):
        print(self.name)

    @classmethod
    def print_class_name(cls):
        print(cls.__name__)

    def save(self, *args, **kwargs):
        if not self.remark:
            self.remark = 'This is a book.'

Окончательная инициализация сгенерированных данныхremarkЗначение поля по-прежнему пусто.

Все сигналы, зарегистрированные моделью в функции, недействительны

Хотя даваяBookмодель зарегистрированаsignal,Но когдаmigrationsвсе равно не будет работать

make_good_use_of_migrations/models.py view raw
@receiver(pre_save, sender=Book)
def generate_book_remark(sender, instance, *args, **kwargs):
    print(instance)
    if not instance.remark:
        instance.remark = 'This is a book.'

Не помещайте обработку данных в файл миграции для изменений модели

При восстановлении данных или создании данных инициализации не помещайте функции обработчика в автоматически сгенерированные изменения или сгенерированные поля или модели.migrationsфайл, например:

make_good_use_of_migrations/migrations/0005_auto_20181204_0610.py view raw
class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0004_fix_book_data'),
    ]

    operations = [
        migrations.AlterField(
            model_name='book',
            name='author',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
                                    to='make_good_use_of_migrations.Author'),
        ),
        """
        migrations.RunPython(xxx) 不要把数据处理放到模型变更中
        """
    ]

Основная причина не ставить их вместе заключается в том, что когдаRunPythonЛогика обработки функции не может выполняться вниз после возникновения исключения.

django_migrationsЭта транзакция не будет зарегистрирована,Но изменения в структуре таблицы выполнены!

Это тожеDjango migrationsПлохая часть, правильная должна быть откатом базы данных при возникновении исключения.

Как только это произойдет, вы сможете только вручнуюmigrationsимя как0005_auto_20181204_0610, записать в таблицу базы данныхdjango_migrations, тоRunPythonВходная логика вынесена отдельно.

Суммировать

Вышеуказанный автор использовал в проектеDjangoобрамленныйmigrationsопыт, следующая статья познакомитDjangoобрамленныйTestCase.

Исходный код этой статьи будет размещен вgithubначальство,GitHub.com/ELFGuangzhouP/Великое ограбление…

本人博客原文地址:elfgzp.cn/2018/12/04/…