Давным-давно я написал более менее работающий пример 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, не иначе.