Александр Кошелев
Александр Кошелев Python-разработчик

Композиция: 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 либо будут туда вписаны. Сами тесты доступны здесь. Они тоже пока ещё не устаканились, не покрывают максимум кода и возможно подвергнуться какой-то реорганизации. Но как источник примеров годятся уже сейчас.

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

comments powered by Disqus