Последовательная работа с миграцией
Иногда Вы можете обнаружить, что изменения в модели требуют некоторых улучшений. Предположим, Вы определили модель:
class Group(models.Model): name = models.TextField(verbose_name="Name") facebook_page__id = models.CharField(max_length=255)
и вы создали и применили миграцию:
./manage.py schemamigration southtut --auto ./manage.py migrate southtut
После чего Вы обнаружили, что: name на самом деле должно быть CharField, а не TextField, а facebook_page__id содержит двойное подчёркивание, а не одинарное, как Вы хотели. Тогда можно исправить эти проблемы и выполнить:
./manage.py schemamigration southtut --auto --update + Added model southtut.Group Migration to be updated, 0026_auto__add_group, is already applied, rolling it back now... previous_migration: 0025_auto__foo (applied: 2012-05-25 21:20:47) Running migrations for southtut: - Migrating backwards to just after 0025_auto__foo. < partner:0026_auto__add_group Updated 0026_auto__add_group.py. You can now apply this migration with: ./manage.py migrate southtut
Что произошло? South удалил последнюю миграцию, которая создала модель, но в которой были ошибки, и заменил её новой миграцией, которая уже не содержит этих ошибок.
Стоит так же обратить внимание на то, что та миграция, которая была уже применена, была автоматически откачена назад. Вы можете теперь применить последнюю версию миграции чтобы получить правильный вариант модели:
./manage.py migrate southtut
Можно повторять этот процесс так часто, как требуется, внося необходимые изменения и в итоге получить лишь одну миграцию, где будут учтены все ваши пожелания.
Просмотр текущих миграций
Часто бывает полезно посмотреть, какие миграции были применены на данный момент и какие доступны для использования. Для этой причины есть команда ./manage.py migrate --list. Вот результат выполнения этой команды для нашего проекта:
$ ./manage.py migrate --list southtut (*) 0001_initial (*) 0002_auto__add_field_knight_dances_whenever_able (*) 0003_auto__add_field_knight_shrubberies (*) 0004_auto__add_unique_knight_name
Наличие звёздочки (*) говорит о том, что миграция была применена, а отсутствие - что ещё нет. Соответственно, чтобы увидеть только те миграции, которые были применены, воспользуйтесь командой ./manage.py migrate --list | grep -v "*"
Если у Вас есть несколько приложений, работающих при помощи миграций, то можно задать и имя приложения для просмотра миграций только для него.
Перенос данных
До сих пор мы говорили только о "миграции схемы", то есть об изменении колонок и индексов. Но есть и другой тип миграции - "миграция данных".
Миграция данных используется для изменения данных, сохранённых в вашей БД для приведения их в соответствие с новой схемой или предоставления новых возможностей. Например, если Вы сохраняете пароль в виде обычного текста (если это и правда так, то немедленно исправьте это. НЕМЕДЛЕННО!!!) и теперь хотите хранить его в виде солёного хеша, то Вам могут потребоваться эти три шага (причём каждый шаг - это одна миграция):
- Создаём две новые колонки: password_salt и password_hash (миграция схемы)
- Используя содержимое колонки password вычисляем соль и хеш для каждого пользователя (миграция данных)
- Удаляем старую колонку password (миграция схемы)
Как провести первую и последнюю миграцию Вы уже и сами знаете: изменяете models.py и запускаете ./manage.py schemamigration --auto myapp. Главное не удаляйте сразу колонку password, так как нам понадобятся данные из неё для заполнения двух новых колонок (всегда, всегда делайте бакуп вашей БД перед тем, как сделать любое изменение, которое может попортить данные. Потому что однажды именно так и будет).
Давайте возьмём реальный пример. Создадим новое приложение под именем southtut2. Добавим его в INSTALLED_APS и создадим его модель:
from django.db import models class User(models.Model): username = models.CharField(max_length=255) password = models.CharField(max_length=60) name = models.TextField()
Сделаем начальную миграцию, применим её и добавим запись:
$ ./manage.py schemamigration --initial southtut2 Creating migrations directory at '/home/andrew/Programs/litret/southtut2/migrations'... Creating __init__.py in '/home/andrew/Programs/litret/southtut2/migrations'... + Added model southtut2.User Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate southtut2 $ ./manage.py migrate southtut2 Running migrations for southtut2: - Migrating forwards to 0001_initial. > southtut2:0001_initial - Loading initial data for southtut2. $ ./manage.py shell In [1]: from southtut2.models import User In [2]: User.objects.create(username="andrew", password="ihopetheycantseethis", name="Andrew Godwin") Out[2]: <User: User object> In [3]: User.objects.get(id=1).password Out[3]: u'ihopetheycantseethis'
from django.db import models import sha class User(models.Model): username = models.CharField(max_length=255) password = models.CharField(max_length=60) password_salt = models.CharField(max_length=8, null=True) password_hash = models.CharField(max_length=40, null=True) name = models.TextField() def check_password(self, password): return sha.sha(self.password_salt + password).hexdigest() == self.password_hash
Сделаем миграцию схемы, которая создаст две новых колонки (обратите внимание, что для обоих стоит null=True. После того, как мы добавим в них данные, мы изменим их на null=False):
$ ./manage.py schemamigration southtut2 --auto + Added field password_salt on southtut2.User + Added field password_hash on southtut2.User Created 0002_auto__add_field_user_password_salt__add_field_user_password_hash.py. You can now apply this migration with: ./manage.py migrate southtut2
Теперь время перейти к более интересной части. Сперва нам надо создать скелет для нашей миграции данных (в отличие от миграции схемы, South не может сделать это для Вас):
$ ./manage.py datamigration southtut2 hash_passwords Created 0003_hash_passwords.py.
Если Вы откроете созданный файл, то увидете, что South создал оболочку для миграции. Тут есть определения модели, функции forwards() и backwards(), но ещё нет никакого кода. Давайте же напишем код функции forwards(), который обработает имеющиеся пароли:
def forwards(self, orm): import random, sha, string for user in orm.User.objects.all(): user.password_salt = "".join([random.choice(string.letters) for i in range(8)]) user.password_hash = sha.sha(user.password_salt + user.password).hexdigest() user.save()
Обратите внимание, что мы используем orm.User для доступа к модели User - это позволяет нам получить доступ к той версии модели User, из которой была сделана эта миграция, так что если мы захотим выполнить эту миграцию позже, мы не получим другую, новую, модель User.
Если Вы хотите получить доступ к модели из другого приложения, то используйте синтаксис типа orm['contenttypes.ContentType']. Модели будут доступны только если Вы можете получить к ним доступ при помощи связей ForeignKey или ManyToMany. Если Вы хотите заморозить другие модели - передайте в командной строке --freeze appname.
Мы должны возбудить исключение при вызове метода backwards(), так как получение пароля из хеша для нас невозможно:
def backwards(self, orm): raise RuntimeError("Cannot reverse this migration.")
Выглядит замечательно. (Для того, чтобы выполнить миграцию данных достаточно опять же запустить команду ./manage.py migrate myapp) И, теперь, удалим поле password из нашей модели и запустим schemamigration последний раз:
$ ./manage.py schemamigration southtut2 --auto ? The field 'User.password' does not have a default specified, yet is NOT NULL. ? Since you are adding or removing this field, you MUST specify a default ? value to use for existing rows. Would you like to: ? 1. Quit now, and add a default to the field in models.py ? 2. Specify a one-off value to use for existing columns now ? Please select a choice: 2 ? Please enter Python code for your one-off default value. ? The datetime module is available, so you can do e.g. datetime.date.today() >>> "" - Deleted field password on southtut2.User Created 0004_auto__del_field_user_password.py. You can now apply this migration with: ./manage.py migrate southtut2
Обратите внимание, что South спрашивает про значение по умолчанию для password, так как если Вы захотите отменить эту миграцию и восстановить поле password, Вам будет нужно либо задать для него значение по умолчанию или же установить для него null=True. Я задал в качестве значения по умолчанию пустую строку. Теперь, применим миграцию:
$ ./manage.py migrate southtut2 Running migrations for southtut2: - Migrating forwards to 0004_auto__del_field_user_password. > southtut2:0002_auto__add_field_user_password_salt__add_field_user_password_hash > southtut2:0003_hash_passwords > southtut2:0004_auto__del_field_user_password - Loading initial data for southtut2.
Замечательно. Мы добавили новые колонки, перенесли значения паролей и удалили старую колонку. Теперь давайте посмотрим, что получилось:
$ ./manage.py shell In [1]: from southtut2.models import User In [2]: User.objects.get(id=1).check_password("ihopetheycantseethis") Out[2]: True In [3]: User.objects.get(id=1).check_password("fakepass") Out[3]: False
Похоже, всё так, как и должно быть!
В миграции данных можно сделать гораздо больше. Для Вас доступны все модели. Единственное, в чём стоит быть осторожным - Вы не сможете получить доступ к любому пользовательскому методу или менеджеру ваших моделей и они не могут быть заморожены (никак). Вы должны скопировать код в саму миграцию. Спокойно добавляете методы в класс Migration. South будет игнорировать всё, кроме forwards и backwards.
Комментариев нет:
Отправить комментарий