В этой заметке я напишу очередную шпаргалку (я сам ими пользуюсь :)
На этот раз — по миграции данных с помощью Django South.
Django South — это инструмент, который позволяет отслеживать изменения модели данных и соответствующим образом изменять SQL-таблицы. Однако, что делать если мы добавляем поле, которое необходимо сразу заполнить значениями?
Предположим, у нас есть модель такого вида:
from django.db import models class User(models.Model): username = models.CharField(max_length=255) password = models.CharField(max_length=60) name = models.TextField()
И мы недовольны тем, что пароли в ней хранятся в открытом виде.
Для решения этого вопроса мы хотим заменить поле password двумя хэш-полями:
password_salt = models.CharField(max_length=8) password_hash = models.CharField(max_length=40)
И здесь мы будем иметь две проблемы:
- Поля пароля не могут быть пустыми — определены как NOT NULL;
- Перед удалением поля password нам необходимо заполнить поля password_salt и password_hash на его основе.
Django South предлагает такие преобразования делать в три этапа:
1. Добавить новые поля, разрешив им быть пустыми
password_salt = models.CharField(max_length=8, null=True) password_hash = models.CharField(max_length=40, null=True)
Обратите внимание на использование null=True.
Создадим миграцию для добавления полей:
$ ./manage.py schemamigration myapp --auto
2. Подготовить миграцию данных
$ ./manage.py datamigration myapp hash_passwords Created 0003_hash_passwords.py.
Открыв файл myapp/migrations/0003_hash_passwords.py, мы увидим в нем пустые функции forwards() и backwards(). Названия говорят сами за себя, поэтому давайте просто посмотрим, что туда можно записать:
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. Оно содержит модели, гарантировано соответствующее моменту создания данной миграции.
По умолчанию orm содержит не всю схему данных, а только мигрирующую модель и модели, связанные с ней отношениями Foreign Key и Many To Many.
Для добавления в orm других моделей используйте опцию «— -freeze appname» команды datamigration.
Увы, после задуманной миграции откат с восстановлением plain-text пароля будет невозможен…
def backwards(self, orm): raise RuntimeError("Cannot reverse this migration.")
3. Привести модель данных к конечному виду
Удаляем старое поле password и указываем, что новые поля не могут быть пустыми (убираем из описания null=True).
Создаем финальную миграцию
$ ./manage.py schemamigration myapp --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 myapp.User Created 0004_auto__del_field_user_password.py. You can now apply this migration with: ./manage.py migrate myapp
Скрипт задаст вопрос про значение по умолчанию для удаляемого поля (на случай отката миграции) — укажем ему «».
Теперь мы можем применить все три миграции одновременно
$ ./manage.py migrate myapp
Пример взят из стандартной документации South.