8000 GitHub - ansakoy/djmigr: Toying with django migrations
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

ansakoy/djmigr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 

Repository files navigation

Проект для экспериментов с джанго-миграциями без привлечения внимания санитаров

A project for experimenting with django migrations with minimal turbulence

Установка

cd djmigr

python3.9 -m venv djmigrvenv

djmigrvenv/bin/pip install --upgrade pip
djmigrvenv/bin/pip -r requirements.txt
create user djmigr with password 'djmigr';
CREATE DATABASE djmigr LC_COLLATE 'ru_RU.UTF-8' lc_ctype 'ru_RU.UTF-8' OWNER djmigr template template0;

События

# Удаляем папки с миграциями
% rm -rf library/migrations/
-- Убиваем информацию о миграциях из БД
delete from django_migrations;
% python manage.py makemigrations
No changes detected

# Но:
% python manage.py makemigrations library
Migrations for 'library':
  library/migrations/0001_initial.py
    - Create model Author
    - Create model Book

Итого у нас есть база, в которой уже есть схема и данные, но после удаления старых миграций и создания новых у нас новые попытки смигрировать заканчиваются плохо:

% python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, library, sessions
Running migrations:
  Applying contenttypes.0001_initial...Traceback (most recent call last):
  File "/Users/ansakoy/Documents/Cheatsheets/djmigr/djmigrvenv/lib/python3.9/site-packages/django/db/backends/utils.py", line 87, in _execute
    return self.cursor.execute(sql)
psycopg2.errors.DuplicateTable: relation "django_content_type" already exists
...

Применяем фейк

% python manage.py migrate --fake
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, library, sessions
Running migrations:
  Applying contenttypes.0001_initial... FAKED
  Applying auth.0001_initial... FAKED
  Applying admin.0001_initial... FAKED
  Applying admin.0002_logentry_remove_auto_add... FAKED
  Applying admin.0003_logentry_add_action_flag_choices... FAKED
  Applying contenttypes.0002_remove_content_type_name... FAKED
  Applying auth.0002_alter_permission_name_max_length... FAKED
  Applying auth.0003_alter_user_email_max_length... FAKED
  Applying auth.0004_alter_user_username_opts... FAKED
  Applying auth.0005_alter_user_last_login_null... FAKED
  Applying auth.0006_require_contenttypes_0002... FAKED
  Applying auth.0007_alter_validators_add_error_messages... FAKED
  Applying auth.0008_alter_user_username_max_length... FAKED
  Applying auth.0009_alter_user_last_name_max_length... FAKED
  Applying auth.0010_alter_group_name_max_length... FAKED
  Applying auth.0011_update_proxy_permissions... FAKED
  Applying auth.0012_alter_user_first_name_max_length... FAKED
  Applying library.0001_initial... FAKED
  Applying sessions.0001_initial... FAKED

При следующем запуске migrate уже без фейка всё хорошо:

% python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, library, sessions
Running migrations:
  No migrations to apply.

Действительно, никаких изменений не было. Добавим изменения:

% python manage.py makemigrations
Migrations for 'library':
  library/migrations/0002_book_publish_year.py
    - Add field publish_year to book

% python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, library, sessions
Running migrations:
  Applying library.0002_book_publish_year... OK
(djmigrvenv) ansakoy ~/Documents/Cheatsheets/djmigr/djmigr % 

Сработало, и все данные на месте.

Теперь допустим, что мы не просто удалили миграции, а внесли еще изменения в модели и теперь хотим это всё смигрировать в базу, не удаляя данные.

Удаляем миграции:

rm -rf library/migrations 

Создаем новые атрибуты у моделей library, а заодно и вообще новый апп blog со своими моделями.

Пробуем создать миграции:

% python manage.py makemigrations
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Entry
    - Create model Comment

Миграции создались, но только для нового аппа. И они даже успешно применились для нового аппа:

% python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

Но в старом аппе миграции закономерным образом не появились. Создадим прицельно:

% python manage.py makemigrations library
Migrations for 'library':
  library/migrations/0001_initial.py
    - Create model Author
    - Create model Book

Смигрироваться, конечно, не смигрируется, потому что в базе уже отмечена миграция 0001 как состоявшаяся:

% python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, library, sessions
Running migrations:
  No migrations to apply.

Чтобы она не мешалась, удалим из базы все миграции (delete from django_migrations;).

И для полноты картины положим какие-нибудь данные в таблицы нового аппа.

Делаем фейковые миграции (python manage.py makemigrations --fake). Успешно.

Всё прекрасно, только миграции 0001 из аппа library (и соответственно изменения в них) теперь невидимы для джанго. Посмотрим, что получится, если вбросить какое-нибудь еще изменение и попробовать его смигрировать.

% python manage.py makemigrations
Migrations for 'library':
  library/migrations/0002_book_useless_field.py
    - Add field useless_field to book
% python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, library, sessions
Running migrations:
  Applying library.0002_book_useless_field... OK

Смигрировалось без ошибок. Но только миграцию 0001, где были дополнительные поля, по-прежнему не видим. В итоге в базу добавилось только новое поле из 0002, а поля, добавленные в первой сгенерированной миграции так и остаются за кадром, ибо такова суть фальшивых миграций.

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

Для чистоты эксперимента опять убьем миграции в аппах и в базе тоже.

Попробуем --fake-initial

% python manage.py migrate --fake-initial
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, library, sessions
Running migrations:
  Applying contenttypes.0001_initial... FAKED
  Applying auth.0001_initial... FAKED
  Applying admin.0001_initial... FAKED
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name...Traceback (most recent call last):
  File "/Users/ansakoy/Documents/Cheatsheets/djmigr/djmigrvenv/lib/python3.9/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.UndefinedColumn: column "name" of relation "django_content_type" does not exist

Упс. Ладно, попробуем частями. Откатимся к исходной позиции: в наших аппах нет миграций, в базе сведения о миграциях удалены. Сначала сфальсифицируем существующие миграции встроенных джанго-приложений:

% python manage.py migrate --fake
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... FAKED
  Applying auth.0001_initial... FAKED
  Applying admin.0001_initial... FAKED
  Applying admin.0002_logentry_remove_auto_add... FAKED
  Applying admin.0003_logentry_add_action_flag_choices... FAKED
  Applying contenttypes.0002_remove_content_type_name... FAKED
  Applying auth.0002_alter_permission_name_max_length... FAKED
  Applying auth.0003_alter_user_email_max_length... FAKED
  Applying auth.0004_alter_user_username_opts... FAKED
  Applying auth.0005_alter_user_last_login_null... FAKED
  Applying auth.0006_require_contenttypes_0002... FAKED
  Applying auth.0007_alter_validators_add_error_messages... FAKED
  Applying auth.0008_alter_user_username_max_length... FAKED
  Applying auth.0009_alter_user_last_name_max_length... FAKED
  Applying auth.0010_alter_group_name_max_length... FAKED
  Applying auth.0011_update_proxy_permissions... FAKED
  Applying auth.0012_alter_user_first_name_max_length... FAKED
  Applying sessions.0001_initial... FAKED

Допустим. Теперь сгенерируем миграции наших приложений и попробуем таки сделать --fake-initial. Сделалось, но собственно это привело к фальсификации этих самых единственных файлов с миграциями, которые образовались по итогам перегенерации.

Мораль:

  • migrate --fake нужно для того, чтобы "проматывать" миграции к нужному состоянию, подгодняя их под те изменения, которые были внесены в схему базы руками. То есть создали в базе новую таблиц или добавили поле, записали это в моделях, прогнали фейк. Теперь в базе в django_migrations отмечено, что эта миграция уже применена, и не будет скандалов, что, мол такая таблица/поле уже есть в базе.
  • migrate --fake-initial нужно для того, чтобы решать проблему сосуществования первой миграции, полностью соответствующей уже существующей схеме базы, и всех последующих, которые должны изменить эту схему. Если запустить миграции с этой опцией, опять же, для начальной миграции будет сгенерирована запись в базе, что дескать она уже применена, а последующие уже применятся по-настоящему.

Ни то, ни другое не решает проблему с "дельтой", когда в одной миграции (по совпадению начальной) у нас есть и старое, и новое.

Такое впечатление, что придется таки лезть в миграции руками и разводить старое и новое. Строго говоря, нового у нас всего два поля в library.Book. Вот наша начальная миграция на момент создания:

# Generated by Django 4.0.5 on 2022-06-01 17:43

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Author',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('full_name', models.CharField(max_length=500, verbose_name="Author's name")),
                ('birth_date', models.DateField(blank=True, default=None, null=True, verbose_name='Date of birth')),
            ],
        ),
        migrations.CreateModel(
            name='Book',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=1000, verbose_name='Title')),
                ('publish_year', models.IntegerField(blank=True, default=None, null=True, verbose_name='Publication year')),
                ('abstract', models.TextField(blank=True, default=None, max_length=1000, null=True, verbose_name='Abstract')),
                ('useless_field', models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Useless Field')),
                ('author', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='books', to='library.author')),
            ],
        ),
    ]

Нам нужно сделать так, чтобы в схему базы добавились поля abstract. И еще у нас есть поле useless_field. С ним получилось странно, потому что по итогам предыдущих экспериментов оно почему-то добавилось как read only, а нам надо, чтобы оно было нормальным. Поэтому дропнем его от греха подальше в базе и создадим заново.

alter table library_book drop column useless_field;

Теперь создадим пустую миграцию в library:

python manage.py makemigrations --name abstract_useless_field library --empty

Вот что получилось:

# Generated by Django 4.0.5 on 2022-06-01 18:24

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('library', '0001_initial'),
    ]

    operations = [
    ]

Добавляем туда нужные поля:

# Generated by Django 4.0.5 on 2022-06-01 18:24

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('library', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='book',
            name='abstract',
            field=models.TextField(blank=True, default=None, max_length=1000, null=True, verbose_name='Abstract'),
        ),
        migrations.AddField(
            model_name='book',
            name='useless_field',
            field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Useless Field'),
        ),
    ]

А также удаляем эти поля их начальной миграции во избежание последующего бардака:

# Generated by Django 4.0.5 on 2022-06-01 17:43
...
        migrations.CreateModel(
            name='Book',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=1000, verbose_name='Title')),
                ('publish_year', models.IntegerField(blank=True, default=None, null=True, verbose_name='Publication year')),
                ('author', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='books', to='library.author')),
            ],
        ),
    ]
...

И запускаем миграции:

% python manage.py migrate 
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, library, sessions
Running migrations:
  Applying library.0002_abstract_useless_field... OK

И у нас в базе появились эти поля. Правда почему-то всё-таки опять read only.

Отбой, это не read only, а глюки ГУИ. Всё в порядке, метод работает.

About

Toying with django migrations

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

0