Вчера я выражал неудовольствие собственным решением по автоматическому созданию профилей форума для пользователей. Напомню суть:
При заведении в системе новых пользователей, никто автомагически не будет для них создавать записи Profile’ов, и обращение user.cicero_profile будет вызывать exception. Поэтому, надо их где-то создавать. Я для этого использую middleware, которое вешается где-нибудь после авторизации и для каждого пользователя, залогиненного на сайт, смотрит, есть ли у него профиль, и создает если надо. Минусов я вижу два:
- надо помнить о том, что при установке Cicero надо обязательно включать это middleware
- любой залогиненный пользователь всегда дергает базу лишним запросом
Сегодня я переписал эту логику с помощью дескрипторов и спешу поделиться. Но должен сразу предупредить — это уже суровая магия :-)
При описании модели Profile в моем приложении, связанной с моделью User в системном приложении "auth", в самой модели User тут же появляется ссылка на профайл. Откуда она там берется? Это работа интересной питоновской штуки под названием "метакласс", которую Джанго использует на всю катушку.
Питон — язык динамический, и в нем описанный в программе класс создается непосредственно в рантайме как самостоятельный объект. Это отличается от, например, C++, где описание класса имеет смысл только на этапе компиляции и статично описывает структуру будущих объектов этого класса. В Питоне же во время создания класса на этот процесс можно еще и повлиять. Для этого создается "метакласс" — отдельный объект, которому скармливается подготовленный класс, и он может делать с ним все что угодно: добавлять, удалять и менять поля и методы. Вот, кому интересно, очень загрузочная статья по этому поводу.
В Джанго все модели БД проходят через метакласс ModelBase, который делает такие вещи:
Более подробно этот процесс можно изучить в статье Малколма Трединника.
Итак, ссылочные поля добавляют в модели, которые связывают, атрибуты для доступа в обе стороны: от родительских объектов к детям и наоборот. Эти атрибуты — питоновские дескрипторы — такие интересные объекты, которые позволяют определить операции чтения их значений и установки им новых значений (делфисты узнают в этом объектные property).
Например дескриптор, который создает поле OneToOneField в родительском классе, на который ссылается, выглядит так:
class SingleRelatedObjectDescriptor(object):
def __get__(self, instance, instance_type=None):
# - составить запрос в ORM для получения связанного объекта
# - закешировать его внутри себя для последующего использования
# - вернуть объект
def __set__(self, instance, value):
# поставить id родительского объекта, к которому привязан дескриптор, в
# нужное поле переданного зависимого объекта
Возвращаемся к 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. Опенсурс — мощь!
Комментарии: 18
7.03.07 18:16
Да, классно придумал!
7.03.07 19:41
Мдя, пора серьезно начинать учить питон. Иван, какие-нибудь книги не посоветуешь? Или мануалы.
8.03.07 02:20
А при удалении пользователя, этот профиль так же будет удаляться или нет?
..bw
8.03.07 11:33
Да, будет. Django подчищает все зависимые записи при удалении.
8.03.07 14:39
Я думал по-русски об этом написать, но что-то никак не придумывается нормального примера, на котором можно было бы объяснить...
9.03.07 12:09
Книжки... Мне всегда сложно про это отвечать :-). Сам я начал изучать с Dive Into Python. Она хорошо написана, но местами уже сильно устарела (особенно в описании объектов). И кроме того, к моменту, когда я начал питонить, я уже давно и много программировал. А просто учить язык и учить N-ный язык — это большая разница.
Поэтому мой совершенно честный совет будет такой: примкнуть к какому-нибудь большому питоновскому проекту, следить за разработкой и читать исходники. Это — лучшая школа.
16.03.07 04:15
Иван, заметка очень интересная (вообще, заглядывать во внутренности нетривиальных вещей интересно всегда). Однако, меня мучает один страшный вопрос.
А почему нельзя повесить создание user.cicero_profile на user.save()?
По-моему, это логичнее.
16.03.07 09:58
Потому что юзеры в проекте создаются (и save()'ятся) далеко не только в коде форума:
А раз он их не создает всех явно, то и save() ему негде вызывать.
28.03.07 10:44
А что делать с постами этого пользователя? Можно конечно разрешить null значения, но что тогда показывать об авторе поста.
28.03.07 17:27
А пользователи как раз поэтому и не должны удаляться, в общем-то. Зачем? Отключить их можно через флажок is_active, а удалять не нужно.
Единственно, у меня будет код, который будет сливать двух пользователей в одного (если ими владеет один человек). Но там статьи будут апдейтиться так, чтобы ими владел тот пользователь, который в результате слияния останется.
6.04.08 16:44
но как джанго поймёт что вешается именно наследник ??
так вот я немного непонимаю как сделать чтобы это заработало ? написать в User.Meta что то типа cicero_profile = ????? было бы элегантно...
ты вроде бы упустил этот момент...
6.04.08 21:14
Я не очень понимаю, что именно не понятно :-). В цитате ровно содержится ответ: я делаю наследника от OneToOneField под названием AutoOneToOneField, и именно и вешает динамически на User нужный дескриптор.
7.04.08 02:07
Проблему решил.
Я не догнал что надо переопределить поле в модели не простым OneToOneField а уже нашим, переопределённым. в другой ситуации наверное и очевидно было бы, но я так запутался в исходниках django.db. С виду вроде бы поля действуют в пределах модели, а джанго сам добавляет related поля в related модель, поэтому я пытался явно определить в модели relatedfield и так далее..
Если можно, для таких как я добавь там пару слов в статью, что нужно делать именно так.
PS из твоих статей много нового узнал. Роман о контролируемой раздаче и неблокирующих сокетах вообще шедевр... жаль что в интернете очень мало статей в которых описаны такие вот нетривиальные фичи которые нужны, в основном у гугла ответы ведут на твой блог
в целом удачи тебе, спасибо что ты есть )
22.01.09 00:17
2.07.09 16:00
Подскадите а в чем может быть прикол?
Использовал описанный метод - все прикрасно работало, пока не стал переносить на другой сервер
модель
from django.contrib.auth.models import User
from fields import AutoOneToOneField
все падает еще на проверке
2.07.09 16:41
единственное разумное что пока придумал это явно проверять наличие атрибута
извиняюсь что немного не в тему
24.08.09 17:17
18.02.10 21:52