понедельник, 12 ноября 2012 г.

Документация South - Перевод. Часть 3: Дополнительные команды и миграция данных

Последовательная работа с миграцией

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

 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.

Комментариев нет:

Отправить комментарий