Давным-давно я написал более менее работающий пример PickleField'а - поля для хранения любого питоновского объекта, поддерживающего сериализацию при помощи pickle или cPickle.
И все бы неплохо, только на днях мне понадобилось хранить в этих PickleField'ах Django'вские модели, например экземпляры пользователей (auth.User) или проектов (самописная модель). Да, возможно, это не совсем верно, но задача стояла именно такая.
Не надеясь получить какой-то подвох я стал просто сохранять эти instance, и вроде бы все работало, пока я не написал смехотворный тест:
project = choice(Project.objects.all())
user = choice(project.users.all())
PickleModel.objects.create(object_field=project)
obj = PickleModel.objects.get(object_field=project)
obj.object_field = user
obj.save()
obj = PickleModel.objects.get(object_field=user)
obj.delete()
гдe PickleModel простая тестовая модель:
class PickleModel(models.Model):
object_field = PickleField()
И каково было мое удивление, когда тест завершился ошибкой PickleModel.DoesNotExist на 9 строке вместо чистого прохождения. Интересно, сказал я и полез в код, смотреть почему PickleField при чистом сохранении (PickleModel.objects.create) правильно сохраняет в базу весь инстанс проекта, а при обновлении тестового объекта (obj.object_field = ...) сохраняет что-то не то и не так.
Сначала думал, что ошибка где-то в PickleField, на секунду даже задумался о использовании стороннего решения. Но потом понял, что сам PickleField не причем, и что при обновлении тестового объекта не вызывается PickleField.get_db_prep_save. Интересно, решил я и пошел смотреть на то, как именно проходит процесс обновления объекта в django.db.models.base.Models.save_base, а оттуда в django.db.models.base.sql.UpdateQuery.add_update_fields, где собственно и происходит процесс генерирования sql-запроса для обновления. Там я и нашел причину такого поведения Django.
Причина оказалась весьма логична c одной стороны. Django для всех своих инстансов вызывает prepare_database_save, чтобы хранить не инстанс, а его первичный ключ в базе данных. С другой стороны осталось не до конца понятно, почему такого же поведения нет при создании объектов. Впрочем, как именно и где происходит вызов prepare_database_save для инстансов моделей при создании нового объекта я не проверял, а начал думать как обойти эту особенность Django.
Для начала решил по манки-патчить по-тупому, добавив в PickleField.to_python:
if hasattr(value, 'prepare_database_save'):
delattr(value, 'prepare_database_save')
на что Питон сказал мне: Are you crazy, man? И добавил AttributeError: prepare_database_save. Тогда я решил по манки-патчить более умно, в два захода :) Для начала подменил поведение prepare_database_save в том же PickleField.to_python:
if hasattr(value, 'prepare_database_save'):
value.prepare_database_save = \
lambda unused: PickleField().get_db_prep_save(value)
а затем, в PickleField.get_db_prep_value возвращал объекту первичное состояния, чтобы его возможно было сериализовать:
if hasattr(value, 'prepare_database_save') and \
isinstance(getattr(value, 'prepare_database_save'),
types.LambdaType):
delattr(value, 'prepare_database_save')
После всего этого, запустил исходный тест, порадовался чистому проходу и решил написать вот этот малость бессмысленный псот :)
Ах да, сам код PickleField доступен как всегда в kikola.db.fields, ну или отдельным гистом.
И еще одна забавная возможность PickleField'a. Благодаря модифицированию стандартного поведения get_default вы можете указывать любой питоновский объект, как дефолтное значение для этого поля. Например:
class PickleModel(models.Model):
dict_field = PickleField(default={'a': 1, 'b': 2, 'c': 3})
list_field = PickleField(default=[1, 2, 3])
tuple_field = PickleField(default=(1, 2, 3))
Весьма полезная для меня возможность, подсмотренная в issues к django-extensions.
Апдейт
Ха, недолго музыка играла для моего предыдущего решения. И я тому весьма рад ибо оно было уж слишком уродливым. Вообщем, суть проблемы прошлого решения в следующем неработающем тесте:
project = choice(Project.objects.all()) obj = PickleModel.objects.create(object_field=project) obj.save() obj = PickleModel.objects.get(object_field=project)
а именно в том, что если поле object_field осталось нетронутым при изменении модели, по при вызове save все-таки вызовется метод prepare_database_save и конвертирует модель в строку - первичный ключ.
Но не беда, манки-патчим django.db.models.Model.prepare_database_save и получаем работающий тест и код:
# Make able to store Django model objects in ``PickleField``
def picklefield(func):
def wrapper(obj, field):
if isinstance(field, PickleField):
return field.get_db_prep_save(obj)
return func(obj, field)
return wrapper
models.Model.prepare_database_save = \
picklefield(models.Model.prepare_database_save)
Хотя, по правде говоря, и это решение мне не очень нравится. Dirty hack, не иначе.