Сегодня занимался модельками своего блога и решил приделать к Entry (это запись в блоге) счётчик комментариев. Представление данных в приложении у меня реализовано с помощью SQLAlchemy, напомню, что это реализация Data Mapper, в отличие от модных сейчас (в основном благодаря Django и Rails) Active Record реализаций слоя источника данных.
Для подсчёта комментариев у меня нет никакого аккумулятора в таблице entries_table, таким образом, мне надо каждый раз считать count на комментариях. Но как это сделать правильно?
Первое что приходит в голову — сделать аттрибут Entry.comments_count загружаемым по требования (lazy-loading attribute) и повесить на него запрос::
Всё просто, обращаемся к аттрибуту — происходит запрос, не обращаемся — не происходит. Но что, если обращение происходит слишком часто, например как теперь это делается у меня на главной странице в блоге (указано количество комментариев к каждой записи). На это потребуется n+1 запрос, где n — это количество записей на страницу. Не очень хорошо…
А теперь самое интересное: SQLAlchemy умеет мапить классы не только на отдельные таблицы, но и на JOIN'ы и даже на SELECT'ы. То есть всё что нам нужно это составить SQL запрос::
Вот как он выглядит используя описание метаданных (Metadata mapping) SQLAlchemy::
Всё довольно просто. Теперь мапим классик Entry на этот SELECT::
Всё, готово! Теперь при получении записей Entry, мы будем иметь у каждого экземпляра аттрибут comments_count. И всё это за один запрос.
Django.orm и Rails со своим ActiveRecord нервно курят в сторонке ;).
Для подсчёта комментариев у меня нет никакого аккумулятора в таблице entries_table, таким образом, мне надо каждый раз считать count на комментариях. Но как это сделать правильно?
Загрузка по требованию
Первое что приходит в голову — сделать аттрибут Entry.comments_count загружаемым по требования (lazy-loading attribute) и повесить на него запрос::
SELECT COUNT(*) FROM comments_table WHERE comments_table.entry_id == <ENTRY_ID>
Всё просто, обращаемся к аттрибуту — происходит запрос, не обращаемся — не происходит. Но что, если обращение происходит слишком часто, например как теперь это делается у меня на главной странице в блоге (указано количество комментариев к каждой записи). На это потребуется n+1 запрос, где n — это количество записей на страницу. Не очень хорошо…
Объединяем таблицы
А теперь самое интересное: SQLAlchemy умеет мапить классы не только на отдельные таблицы, но и на JOIN'ы и даже на SELECT'ы. То есть всё что нам нужно это составить SQL запрос::
SELECT <поля из entries_table>, COUNT(comments_table.id)
FROM entries_table
LEFT OUTER JOIN comments_table
ON entries_table.id == comments_table.entry_id
GROUP BY entries_table.id
Вот как он выглядит используя описание метаданных (Metadata mapping) SQLAlchemy::
entries_with_comments = select(
[
entries_table,
func.count(comments_table.c.id).label("comments_count")
],
from_obj=[entries_table.outerjoin(comments_table)],
group_by=[c for c in entries_table.c]
).alias("entries_with_comments")
Всё довольно просто. Теперь мапим классик Entry на этот SELECT::
mapper(Entry, schema.entries_with_comments,
primary_key=[schema.entries_with_comments.c.id],
)
Всё, готово! Теперь при получении записей Entry, мы будем иметь у каждого экземпляра аттрибут comments_count. И всё это за один запрос.
Django.orm и Rails со своим ActiveRecord нервно курят в сторонке ;).