Вчера я выражал неудовольствие собственным решением по автоматическому созданию профилей форума для пользователей. Напомню суть:
При заведении в системе новых пользователей, никто автомагически не будет для них создавать записи Profile’ов, и обращение user.cicero_profile будет вызывать exception. Поэтому, надо их где-то создавать. Я для этого использую middleware, которое вешается где-нибудь после авторизации и для каждого пользователя, залогиненного на сайт, смотрит, есть ли у него профиль, и создает если надо. Минусов я вижу два:
- надо помнить о том, что при установке Cicero надо обязательно включать это middleware
- любой залогиненный пользователь всегда дергает базу лишним запросом
Сегодня я переписал эту логику с помощью дескрипторов и спешу поделиться. Но должен сразу предупредить — это уже суровая магия :-)
Откуда берутся ссылки
При описании модели Profile в моем приложении, связанной с моделью User в системном приложении "auth", в самой модели User тут же появляется ссылка на профайл. Откуда она там берется? Это работа интересной питоновской штуки под названием "метакласс", которую Джанго использует на всю катушку.
Питон — язык динамический, и в нем описанный в программе класс создается непосредственно в рантайме как самостоятельный объект. Это отличается от, например, C++, где описание класса имеет смысл только на этапе компиляции и статично описывает структуру будущих объектов этого класса. В Питоне же во время создания класса на этот процесс можно еще и повлиять. Для этого создается "метакласс" — отдельный объект, которому скармливается подготовленный класс, и он может делать с ним все что угодно: добавлять, удалять и менять поля и методы. Вот, кому интересно, очень загрузочная статья по этому поводу.
В Джанго все модели БД проходят через метакласс ModelBase, который делает такие вещи:
- считывает параметры полей и уносит их в отдельное место
- проходится по всем полям и просит их добавить что-нибудь в модель, в частности:
- ссылочные поля (OneToOneField, ForeignKey, ManyToManyField) ищут свои связываемые модели и добавляют по обеим сторонам связи нужные атрибуты доступа
- файловые поля добавляют в модели специфичные методы для сохранения и доступа к файлам
- по системе рассылается сигнал "класс такой-то модели создан"
Более подробно этот процесс можно изучить в статье Малколма Трединника.
Что представляет собой ссылка
Итак, ссылочные поля добавляют в модели, которые связывают, атрибуты для доступа в обе стороны: от родительских объектов к детям и наоборот. Эти атрибуты — питоновские дескрипторы — такие интересные объекты, которые позволяют определить операции чтения их значений и установки им новых значений (делфисты узнают в этом объектные property).
Например дескриптор, который создает поле OneToOneField в родительском классе, на который ссылается, выглядит так:
class SingleRelatedObjectDescriptor(object):
def __get__(self, instance, instance_type=None):
# - составить запрос в ORM для получения связанного объекта
# - закешировать его внутри себя для последующего использования
# - вернуть объект
def __set__(self, instance, value):
# поставить id родительского объекта, к которому привязан дескриптор, в
# нужное поле переданного зависимого объекта
AutoOneToOneField
Возвращаемся к Cicero. Есть джанговский класс User и есть связанный с ним через OneToOneField класс Profile. В процессе создания Profile создается атрибут User.cicero_profile
— объект-дескриптор. Соответственно для каждого объекта класса User при попытке взять user.cicero_profile
будет вызываться метод __get__ этого дескриптора. Который просто выполняет запрос в духе select cicero_profile.id, ... where user_id = ...
. И вызывает исключение, если профайла в базе нет.
Чтобы это изменить, я просто написал собственный дескриптор — наследник стандартного, у которого метод __get__ работает так:
try:
# получить объект
except ОбъектНеНайден:
# создать объект
return объект
Ну и чтобы заставить OneToOneField вешать на User'а не стандартный дескриптор, а мой, пришлось и от самого поля сделать наследника, который переопределяет метод contribute_to_related_class, назначающий дескриптор.
Код вышел довольно просто, потому что вся идея уже была написана в джанговских классах, от которых я наследовался:
from django.db.models import OneToOneField
from django.db.models.fields.related import SingleRelatedObjectDescriptor
class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor): # this line just can't be too long, right?
def __get__(self, instance, instance_type=None):
try:
return super(AutoSingleRelatedObjectDescriptor, self).__get__(instance, instance_type)
except self.related.model.DoesNotExist:
obj = self.related.model(**{self.related.field.name: instance})
obj.save()
return obj
class AutoOneToOneField(OneToOneField):
'''
OneToOneField, которое создает зависимый объект при первом обращении
из родительского, если он еще не создан.
'''
def contribute_to_related_class(self, cls, related):
setattr(cls, related.get_accessor_name(), AutoSingleRelatedObjectDescriptor(related))
if not cls._meta.one_to_one_field:
cls._meta.one_to_one_field = self
В итоге:
- пользователю форума не нужно ничего никуда прописывать при установке
- объект профиля создается только тогда, когда к нему реально обращаются
- объект профиля будет при необходимости создаваться и для тех пользователей, которые уже были в системе на момент установки форума
P.S. Питон — мощь!
P.P.S. Опенсурс — мощь!
Комментарии: 20
Да, классно придумал!
Мдя, пора серьезно начинать учить питон. Иван, какие-нибудь книги не посоветуешь? Или мануалы.
А при удалении пользователя, этот профиль так же будет удаляться или нет?
..bw
Да, будет. Django подчищает все зависимые записи при удалении.
Я думал по-русски об этом написать, но что-то никак не придумывается нормального примера, на котором можно было бы объяснить...
Книжки... Мне всегда сложно про это отвечать :-). Сам я начал изучать с Dive Into Python. Она хорошо написана, но местами уже сильно устарела (особенно в описании объектов). И кроме того, к моменту, когда я начал питонить, я уже давно и много программировал. А просто учить язык и учить N-ный язык — это большая разница.
Поэтому мой совершенно честный совет будет такой: примкнуть к какому-нибудь большому питоновскому проекту, следить за разработкой и читать исходники. Это — лучшая школа.
Иван, заметка очень интересная (вообще, заглядывать во внутренности нетривиальных вещей интересно всегда). Однако, меня мучает один страшный вопрос.
А почему нельзя повесить создание user.cicero_profile на user.save()?
По-моему, это логичнее.
Потому что юзеры в проекте создаются (и save()'ятся) далеко не только в коде форума:
А раз он их не создает всех явно, то и save() ему негде вызывать.
А что делать с постами этого пользователя? Можно конечно разрешить null значения, но что тогда показывать об авторе поста.
А пользователи как раз поэтому и не должны удаляться, в общем-то. Зачем? Отключить их можно через флажок is_active, а удалять не нужно.
Единственно, у меня будет код, который будет сливать двух пользователей в одного (если ими владеет один человек). Но там статьи будут апдейтиться так, чтобы ими владел тот пользователь, который в результате слияния останется.
но как джанго поймёт что вешается именно наследник ??
так вот я немного непонимаю как сделать чтобы это заработало ? написать в User.Meta что то типа cicero_profile = ????? было бы элегантно...
ты вроде бы упустил этот момент...
Я не очень понимаю, что именно не понятно :-). В цитате ровно содержится ответ: я делаю наследника от OneToOneField под названием AutoOneToOneField, и именно и вешает динамически на User нужный дескриптор.
Проблему решил.
Я не догнал что надо переопределить поле в модели не простым OneToOneField а уже нашим, переопределённым. в другой ситуации наверное и очевидно было бы, но я так запутался в исходниках django.db. С виду вроде бы поля действуют в пределах модели, а джанго сам добавляет related поля в related модель, поэтому я пытался явно определить в модели relatedfield и так далее..
Если можно, для таких как я добавь там пару слов в статью, что нужно делать именно так.
PS из твоих статей много нового узнал. Роман о контролируемой раздаче и неблокирующих сокетах вообще шедевр... жаль что в интернете очень мало статей в которых описаны такие вот нетривиальные фичи которые нужны, в основном у гугла ответы ведут на твой блог
в целом удачи тебе, спасибо что ты есть )
Подскадите а в чем может быть прикол?
Использовал описанный метод - все прикрасно работало, пока не стал переносить на другой сервер
модель
from django.contrib.auth.models import User
from fields import AutoOneToOneField
все падает еще на проверке
единственное разумное что пока придумал это явно проверять наличие атрибута
извиняюсь что немного не в тему
Dimas, там баг в библиотеке, см. https://bitbucket.org/kmike/django-annoying/changeset/7d7237a708ab