Композиция: ForeignAttributeField

Как вы догадались, я продолжаю тему денормализации и моей реализации композитных полей.

С момента прошлого поста я успел значительно улучшить базовый CompositionField и решить несколько концептуальных проблем.

Итак что же новое появилось:

  • Сделал низкоуровневый CF полноценным классом, который теперь удобно сабклассить и добавлять новый функционал
  • Появилось место для интроспекции. Присоединение к хост модели стало более интеллектуальным из-за возможного отложенного присоединения.
  • Чуть-чуть изменился интерфейс - параметр commit теперь может быть задан для конкретного триггера. Это сугубо практическое изменение, которое помогло решить одну проблему с бесконечной рекурсией.
  • По совету Вани Сагалаева добавил генерацию freeze_FOO метода, который включает/выключает обработку сигналов. Полезно когда надо обработать какие-то данные скопом, а потом так же скопом пересчитать денормализованные поля.

Я грозился написать высокоуровневые обертки над CF чтобы облегчить использование и упростить конечный интерфейс. Сегодня я расскажу о первом сабклассе CF: ForeignAttributeField - поле которое отслеживает изменения некого внешнего поля в связанном объекте. Причем, поле может находится на любом уровне вложенности связи, что иногда очень удобно. Как я уже писал в прошлый раз - поля объектов, на которые непосредственно ссылается модель, так денормализовывать смыла мало. Проще использовать select_related. Но если заветный атрибут лежит глубже и для доступа к нему надо приджоинить, допустим, пять таблиц, то тут уже другой расклад и это может оказаться вполне хорошим юзкейсом. Кстати, это поле очень похоже на оригинальный DenormField, предложенный Эндрю Годвином. Я конечно не буду тут показывать пять уровней связей, а покажу лишь две, но формально их может быть сколь угодно много. Так же и композитных полей в модели может быть не ограниченное количество.

Пример с фильмом и режиссером из предыдущего поста перепишу по новому и добавлю ещё одно поле:

class Country(models.Model):
   name = models.CharField(max_length=250)

class Person(models.Model):
   name = models.CharField(max_length=250)
   country = models.ForeignKey(Country)

class Movie(models.Model):
   title = models.CharField(max_length=250)
   director = models.ForeignKey(Person)

   director_name = ForeignAttributeField("director.name")
   director_country = ForeignAttributeField("director.country.name")

Теперь посмотрим как это работает:

>>> country = Country.objects.create(name="USA")
>>> person = Person.objects.create(name="Steven Spielberg", country=country)
>>> movie = Movie.objects.create(title="ET", director=person)

>>> movie.director_name
'Steven Spielberg'

>>> movie.director_country
'USA'

>>> person.name = "Steven Allan Spielberg"
>>> person.save()

>>> movie = Movie.objects.get(pk=movie.id)
>>> movie.director_name
u'Steven Allan Spielberg'

>>> country.name = "United States"
>>> country.save()

>>> movie = Movie.objects.get(pk=movie.id)
>>> movie.director_country
u'United States'

Как видите всё довольно прозрачно и естественно. За это я и боролся. Надо только отметить, что в данном примере я явно каждый раз после изменения связных моделей тяну из базы объект movie, чтобы в нем были уже обновленные данные.

Кстати о борьбе. Скажу я вам, это потрясающий фан - придумывать и реализовывать подобные вещи. Сколько же нового понимаешь о структуре фреймворка. Словно как какой-то путешественик, пробираешься сквозь джунгли, прорубаешь себе дорогу и находишь выходы в непростых ситуациях. Очень интересно, занимательно и позновательно.

Поразило также насколько универсальным и обобщенным оказался CF сам по себе. Поскольку его функциональную часть не пришлось менять практически совсем за исключением выноса параметра commit в объявление триггера. Это связано с тем, что далеко не после всех вызовов обработчиков сигналов можно звать save объекта. Например его нельзя вызывать после pre_save/post_save сигналов того же объекта что и был обновлен, а то получается - бесконечная рекурсия.

Ещё - вся функциональность этого высокоуровнего поля заключается в правильном составлении тригеров и обработчиков событий (на базе интроспекции моделей), которые передаются в "отложенный" конструктор CF. Ничего более. Супер!

Файл с исходниками доступен как и раньше тут. Если вы его посмотрите, то заметите заготовки для ещё двух типов полей: ChildsAggregationField, AttributesAggregationField - но они пока на уровне идеи. Как дойдут руки, то обязательно их реализую. Некие намеки на их использования можно посмотреть в тестах.

Ах да, все примеры использования которые я приводил и буду - либо уже являются частью тест набора для CF либо будут туда вписаны. Сами тесты доступны здесь. Они тоже пока ещё не устаканились, не покрывают максимум кода и возможно подвергнуться какой-то реорганизации. Но как источник примеров годятся уже сейчас.

А вам как? Нравится?

Комментарии 8

  1. Nonexistent написал:

    Красота =)

    Оставлен 07 Октябрь 2008 в 10:39
  2. barbuza написал:

    Алекс, я твой фанат)

    Оставлен 07 Октябрь 2008 в 12:21
  3. Kirill Morozov написал:

    Я пока не совсем понял. Какое практическое применение? Чтобы избежать кучу джоинов?

    Оставлен 07 Октябрь 2008 в 12:31
  4. Александр Кошелев написал:

    Я пока не совсем понял. Какое практическое применение? Чтобы избежать кучу джоинов?

    Точно. Эдакий кеш атрибутов. Но, конечно, эффективен он, когда уровней связи много.

    Оставлен 09 Октябрь 2008 в 22:24
  5. ramusus написал:

    Сделал для своих нужд шорткат для кол-ва связанных объектов ForeignCountField:

    class ForeignCount(CompositionField):
        def __init__(self, model, link_back_name, link_to_foreign_name, filter={}, native=None, signal=None):
            self.model = model
            self.do = lambda object, foreign, signal: getattr(object, link_to_foreign_name).filter(**filter).count()
            self.instance_getter = lambda foreign: getattr(foreign, link_back_name)
            self.native = native or models.PositiveIntegerField(default=0)
            self.signal = signal or (models.signals.post_save, models.signals.post_delete)
    
            self.internal_init(
                native = self.native,
                trigger = dict(
                    on = self.signal,
                    sender_model = self.model,
                    do = self.do,
                    field_holder_getter = self.instance_getter
                )
            )
    
    ForeignCountField = ForeignCount
    
    • model - модель, на который вешать сигнал
    • linkbackname - поле-ссылка с внешнего на текущий
    • linktoforeign_name - поле-ссылка на множество внешних с текущего
    • filter (opt) - фильтры для выборки внешних объектов, если нужны
    • native (opt) - поле для кол-ва связей, не знаю может ли понадобиться когда-нибудь
    • signal (opt) - сигналы

    Что думаете насчет кода?

    Оставлен 26 Апрель 2009 в 02:28
  6. Александр Кошелев написал:

    Что думаете насчет кода?

    Идея хорошая. Вообще, чем больше таких шорткатов тем лучше. Самые распространенные случаи вполне можно охватить.

    Надо подумать насчет выделения композиций в отдельное приложение. Нет желания помочь?:-)

    Оставлен 26 Апрель 2009 в 15:00
  7. ramusus написал:

    bq. Нет желания помочь?:-)

    Спасибо, что подрерактировал мое описание. Вынести его конечно было бы правильным - мне вот потребовалось использовать только composition.py и я сделал симлинк /utils/composition.py. Выделение его в отдельное приложение разве сложный процесс? Или ты предпологаешь некий рефакторинг?

    Оставлен 26 Апрель 2009 в 15:49
  8. Александр Кошелев написал:

    Суть в том что просто вытащить модуль наружу не интересно. Надо оформить стандарный питонячий пакет, с setup.py и прочими радостями. Разместить его в PyPI, чтобы была возможность его устанавливать через easy_install/pip.

    Плюс какой-то минимум документации надо предоставить.

    Но главное надо побольше шорткатов написать. Возможно и отрефакторить в каких-то местах уже имеющийся код.

    За время с момента когда я это разрабатывал по сути никаких настолько гибких альтернатив не появилось.

    Поэтому мне кажется что такой пакет может оказать полезным. А его maitenance не такая простая задача.

    Оставлен 26 Апрель 2009 в 17:10