Click here to load reader
View
1.380
Download
2
Embed Size (px)
Citation preview
Реляционный слой модели:паттерны и антипаттерны
Interlabs
11 ноября 2013
1 / 48
Основная проблема
• cовместимость между ОО и БД — за счет сложного ORM• много кода и кoнфигурирования на стороне приложения• content repository — отдельная длинная история
Сложно, ресурсоемко, неудобно.2 / 48
Что мы хотим?
• реляционная БД как хранилище по умолчанию• максимально простой слой ORM• расширяемость на уровне базы и модели приложения• возможность организации (подобия) контент-репозитория• работа непосредственно с БД, если необходимо• использование преимуществ реляционных СУБД• собственный код в слое модели, только если необходимо• возможность определять в коде поведение сущностеймодели, если необходимо
3 / 48
Разумный компромисс— универсальность компонентов, + более простая реализация
Слой модели можно упростить, если не решатьвсе проблемы на стороне ORM.
4 / 48
Схема имен и метаданные
Именование объектов базыАнтипаттерн «эстетическая схема именования», разные схемы
имен в базе и модели, необходимо постоянноепреобразование, имена таблиц из имен классов.
6 / 48
Именование объектов базы• минимизируем различия между базой и моделью• расширяемость через иерархическую схему имен• класс сущности — деталь реализации, не отражен в базе
7 / 48
Схема именВ модульной CMS необходима расширяемость на уровне базы,нужны пространства имен.
Типы Таблицы
blog.blog blog_blogblog.post blog_postblog.post.content blog_post_contentauth.user auth_usercommerce.group commerge_group
Классы модели — реализация и не отражаются в базе. Незабываем об ограничениях по длине имени.
8 / 48
Описание структуры
Антипаттерн аннотации, рефлексия на стороне модели/ORM.
• cargo-культ Java без нормальной поддержки на уровнеязыка
• ресурсоемкий парсинг аннотаций, необходимостькеширования и автогенерации кода
• рефлексия: привязка схемы данных к реализации, низкаяпроизводительность
• дополнительные телодвижения для использованияметаданных на клиенте
9 / 48
Описание структуры
Решение хранение информации о структуре базы вотдельном json-файле
• простой, легко читаемый и редактируемый формат• из нескольких файлов для различных модулей легкопостроить композицию (array_replace_recursive())
• может использоваться как на сервере (PHP) так и наклиенте (JavaScript)
10 / 48
Идентификаторысущностей
Автоинкрементный idПрост в использовании, эффективный кластерный индекс, но:
• непереносим между разными копиями базы→ проблемыс миграцией контента
• свой для каждой таблицы→ сложнее реализоватьуниверсальные отношения между разными типами данных
• генерируется после записи в таблицу — часто неудобнодля реализации редактирования
• «утекание» значений id при ON DUPLICATE KEY• плохо масштабируется
В ряде случаев можно считать антипаттерном
12 / 48
Уникальный глобальный id
Уникальный идентификатор, не зависящий от экземпляра базыи таблицы. Каждое новое значение уникально в глобальномсмысле.
• позволяет единообразно идентифицировать весь контент• упрощает миграцию контента между базами• позволяет перейти от разрозненных видов данных кединому контентному репозиторию
• упрощает масштабирование
13 / 48
Проблемы глобального id
Использование глобального id не так просто, ведь он:
• должен быть глобальным и уникальным• должен быть упорядоченным, иначе проблемы скластерными индексами
• должен занимать как можно меньше места• должен относительно легко генерироваться• должен корректно обрабатываться и в базе, и на клиенте
В идеале — встроенный тип данных в базе, на практике — увы.
14 / 48
Выбор глобального id
• строковый GUID — много места, неупорядочен• модифицированный строковый GUID — можноупорядочить, но все равно много места
• бинарный модифицированный GUID — возможныйвариант, но неудобно работать с базой в ручном режиме(нечитаемые идентификаторы)
• целочисленный 64-разрядный идентификатор — наиболеепримлемый вариант
id BIGINT(20) NOT NULL
15 / 48
64-разрядный id
• тип данных BIGINT, большой, но приемлем• начиная с 5.1 — встроенная функция UUID_SHORT()• в более ранних версиях можно написать аналог• не зависит от разрядности платформы на стороне сервера• достаточно уникален (см. документацию1) и упорядочен• можно генерировать триггером (но скорее всего не нужно)
SELECT UUID_SHORT();
1http://dev.mysql.com/doc/refman/5.1/en/miscellaneous-functions.html16 / 48
Работа с 64-разрядным id
• отсутствует поддержка 64-разрядного int в PHP на 32bit• на клиенте всегда работаем со строковым представлением• PHP преобразует строки в числа для индексов массивов,но поведение отличается на 32/64bit, помним об этом
• в идеале — генерация на клиенте, но приходитсяотдельной операцией на сервер из проблем с рарядностью
• тем не менее у нас всегда есть id-записи до ее помещенияв базу данных
• можем использовать id еще до выполнения записи в базу• идентификатор в sphinx совпадает с id записи
17 / 48
Структура базы
Типовые проблемы ORMС точки зрения структуры таблиц и результатов запросовobject-relational mismatch чаще всего проявляется в видеследующих проблем:
• частичная загрузка сущности для снижения потребленияресурсов
• загрузка дополнительных атрибутов по необходимости, втом числе агрегированных значений
• прозвольная компоновка атрибутов разных сущностей прииспользовании JOIN
• отображение записей на иерархию классов сущностей
19 / 48
Частичная подгрузкаАнтипаттерн: частичная загрузка объекта для снижения
потреления ресурсов
• состояние объекта в ООП не может быть частичным• всегда загружать объект целиком — накладно• явно указывать список атрибутов для загрузки — неудобно• во многих ORM использование частичной загрузки нерекомендуется
Отказываемся от частичной подгрузки
20 / 48
Дополнительные атрибутыАнтипаттерн: динамические COUNT() с GROUP BY, результаты
JOIN с сущностями другого типа.
• непонятно, где размещать результат с т.з. системы типовсущностей (разумные варианты2 обычно не типизированы)
• JOIN сложно описывать в рамках объектого API• использование JOIN и агрегатных вычислений каждый раз,когда нужно вывести результат — очень неэффективно
Отказываемся от загрузки дополнительныхатрибутов и использования JOIN
2https://code.google.com/p/dapper-dot-net/21 / 48
Иерархия классовНужно хранить иерархию классов сущностей с отличающимисяатрибутами. Три классических способа:
• одна sparse table для всех классов• отдельная таблица для каждого класса• общая таблица для общих атрибутов и дополнительныетаблицы для отличающихся
Антипаттерн: сложная логика ORM, использование JOIN приработе с несколькими таблицами, неоптимальныйsparse table
Меньше наследования, больше композиции22 / 48
Структура основанная на композиции• один тип сущности — одна таблица• данные таблицы всегда загружаются целиком (SELECT *)• необходимость частичной загрузки — повод использоватькомпозицию
• иерархии классов для сущностей — редко и скорее дляповедения
• JOIN — либо вообще не используются, либо используютсяво VIEW
• с точки зрения ORM JOIN не существует• агрегатные вычисления — либо VIEW либо денормализация
Денормализация наше все
23 / 48
Композиция: пример
24 / 48
Объектная модельМинимальная объектная модель по умолчанию, без написаниякода:
$blog->title // атрибут$blog->author // ссылочная сущность (1:1)$blog->author->profile->bio // атрибут композиции$blog->details->about // атрибут композиции
$blog->posts() // связанные сущности (1:N)
$post->comments->replies() // связанные сущности$post->comments->numOfReplies // денормализованый атрибут$post->blog // связанная сущность$post->content->body // атрибут композиции
25 / 48
ДенормализацияВычислять агрегаты каждый раз, когда их надо показать —ресурсоемко и неправильно.
Антипаттерн: JOIN и агрегатные функции по любому поводу.
• производим вычисления только при изменении данных• сохраняем вычисленные значения в специальных полях• единый id может облегчить задачу• обновление значений — через триггеры илипериодические процессы
Выигрываем в простоте ORM (не нужно поддерживатьвычисляемые поля, дополнительные атрибуты, GROUP BY и т.д.)и в производительности.
26 / 48
Entity Attribute Value (E.A.V.)Антипаттерн произвольная схема поверх фиксированной
схемы SQL, самый печально известныйантипаттерн
Возможность менять структуру данных, не меняя структурыбазы, но какой ценой:
• невозможность работать с базой без клиентского кода• отсутствие constraints• невозможность использования индексов• неэффективное хранение различных типов данных• неоднородность подхода — часть данных в традиционнойсхеме, часть — в E.A.V.
Примеры: инфоблоки Битрикс, каталоги товаров Magento27 / 48
Альтернатива E.A.V.
• если допускается изменение структуры базы: композицияотдельных таблиц под разные наборы атрибутов
• если нет — лучше отказаться от реляционной базы• глобальный идентификатор + декомпозиция упрощаютзадачу
• изменение структуры данных меняет структуру базы, ноэто меньшее зло
• использование композиции таблиц не означаетиспользование JOIN!
Пример сходной идеологии — типы данных в Drupal (но с JOIN).
28 / 48
Сериализованные значенияАнтипаттерн хранение сложного сериализованного значения в
TEXT-поле
Соблазн использования большой но редко окупается:
• для поиска необходимо извлечение отдельных значений всамодельный индекс
• сложно работать непосредственно с базой, особенно дляспецифичных форматов (PHP serialize)
• лучше использовать стандартные форматы (JSON, YAML) итолько для данных, по которым гарантированно непроводится поиск
• интересный вариант — индексация JSON в Sphinx
29 / 48
Отношения междусущностями
1→ 1
$post = $model->from(’blog.post’)->find($id);print $post->blog->title;
• объектная модель — динамические свойства объекта• foreign key на уровне базы
31 / 48
1→ N
$blog = $model->from(’blog.blog’)->find($id);foreach ($blog->posts() as $post) {
print $post->title;}
• объектная модель — динамический вызов• foreign key на уровне базы
32 / 48
Проблемы 1→ NАнтипаттерн: реализация в виде коллекции сущностей,
заполняемая ORM при загрузке объекта илипрокси-объектом
В блоге 1000 записей, мы действительно хотим подгружать все,пусть даже лениво?
• мы никогда не работаем с полной коллекцией связанныхсущностей
• связанные сущности возвращаются вызовом метода• выборку можно ограничить, передавая параметры• выборка ограничена по умолчанию (как в sphinx)
33 / 48
1→ N: еще примеры
$blog = $model->from(’blog.blog’)->find($id);
foreach ($blog->posts()->sort([’pubDate’ => -1])->limit(20) as $post
) {foreach ($post->comments() as $comment) {
print $comment->author->email;}
}
Выборка $post->comments() ограничена в определенииотношения в конфигурации модели.
34 / 48
1 = 1: декомпозиция
foreach ($blog->posts() as $post) {print $post->content->body;
}
• объектная модель — динамическое свойство• хорошая альтернатива частичной загрузке 35 / 48
N→ M
• c точки зрения API те же проблемы, что и 1→ N, толькохуже.
• классический вариант с таблицей с двумя ссылочнымиполями — проблема с кластерным индексом.
• часто отношение содержит дополнительные атрибуты,проще всего рассматривать как два отношения 1→ N.
• можно ввести дополнительные read-only типы дляупрощения клиентского кода, реализованные через VIEW.
36 / 48
N→ M: пример
37 / 48
Иерархии
Антипаттерн ajacency list, дополнительные плагины к ORM
Иерархии очень важны, особенно для хранения контента.Должна быть полноценная поддержка на уровне ORM.
• используем автоматически поддерживаемые closuretables3
• на уровне API parent — динамическое свойство,children() — метод возвращаемой коллекции
3http://technobytz.com/closure_table_store_hierarchical_data.html38 / 48
Иерархии: примерВыборка всех дочерних сущностей производится однимединственным запросом.
function tree($children, $origin) {foreach ($children as $group) {
printf("%s %sn", $group->id, $group->name);tree($children->children($group), $group);
}}
$group = $mode->from(’commerce.group’)->find($id);tree($group->children(), $group);
39 / 48
Уровень модели
Анемичная модельАнтипаттерн: модель состоит из набора классов без
поведения, фактически — структур данных
Но в ряде случаев этого достаточно, особенно для веб-сайта!
• отдельные классы нужны только для сущностей,обладающих поведением.
• можно использовать ORM для вывода контента вообще несоздавая классов сущностей.
foreach ($model->from(’blog.blog’)->limit(5) as $blog) {foreach ($blog->posts()->limit(10) as $post) {print $post->content->body;
}}
41 / 48
Язык запросовАнтипаттерн реализация собственного языка запросов через
DSL или аналог SQL в ORM.
• нельзя объять необъятное, достаточно реализоватьнебольшое подмножество для основных операций
• фильтрация по значениям атрибутов, сортировка,ограничение объема выборки
• используем массив для описания запроса• легко и быстро создать и модифицировать• можно реализовать композицию запросов• можно преобразовывать в JSON и использовать на разныхуровнях приложения
• можно прописывать в конфигурации42 / 48
Определение запроса$posts = $model->from(’blog.post’)->query(
[ "filter" => ["pubDate" => [ ">" => $from ],"state" => [ "=" => STATE::PUBLISHED ]
],"sort" => [
"pubDate" => -1,],"limit" => 10
]);
Почему это возможно: нет JOIN, атрибуты всегда относятся кодной таблице (типу данных).
43 / 48
Проблема 1:N запросовАнтипаттерн: генерация отдельного запроса для каждой
выбранной сущности для полученияассоциированной сущности. JOIN дляминимизации количества запросов.
• JOIN — медленно и плохо масштабируемо, большоеколичество избыточных данных
• выборка по значениям идентификаторов — гораздолучшая идея
NotORM гениальная идея4, плохая реализация5
4https://www.facebook.com/jakubvrana/posts/4153596751514305http://www.notorm.com
44 / 48
Выборка по набору id// SELECT * FROM blog_blog LIMIT 10foreach ($model->from(’blog.blog’) as $blog) {
// SELECT * FROM auth_user WHERE id IN (id1,id2, ...)// id1, id2, id3 — значения поля authorprint $blog->author->email;
// SELECT * FROM blog_post WHERE blog IN (id1, id2, id3) LIMIT 20// id1, id2, id3 — идентификаторы блогов в первой выборке
// SELECT * FROM blog_post_content WHERE id IN (id1, id2, i3);// id1, id2, id3 — идентификаторы постов из предыдущего запросаforeach ($blog->posts()->limit(20) as $post) {print $post->content->body;
}}
4 вида сущностей — 4 запроса независимо от количества сущностей.45 / 48
Выборка по набору id
• используется PRIMARY или FOREIGN KEY• не передается лишних данных, как часто получается вслучае JOIN
• проще оптимизировать, чем JOIN• нет JOIN — можно хранить различные типы в разных базах• или на разных серверах• или использовать индексы Sphinx (тот же паттерн доступа)• или использовать кеш
46 / 48
Выводы• база данных — полноценный участник слоя модели• нужно использовать сильные стороны реляционной СУБД,а не скрывать их
• ORM и слой модели гораздо проще, если пойти имнавстречу со стороны базы
• триггеры и view могут упрощают клиентский код• денормализация — это очень важно• глобальный id — очень полезно• выборка по набору id — тоже• классы модели — единицы поведения сущностей, а неструктуры данных
47 / 48
Что читатьСсылки из слайдов, а также:
• SQL Antipatterns Strike Back6 — типовые антипаттерны реляционных БД• Models for Hierarchical Data7 — работа с деревьями в SQL• Trees and Hierarchies in SQL for Smarties8 — книга по иерархическимструктурам в SQL
• Nuxeo VCS Architecture9 — архитектура контентного репозитория набазе реляционной СУБД
• Data Normalization, Denormalization and the Forces of Darkness10 —статья про денормализацию
6http://www.slideshare.net/billkarwin/sql-antipatterns-strike-back7http://www.slideshare.net/billkarwin/models-for-hierarchical-data8http://amzn.to/HMRuja9http://doc.nuxeo.com/display/public/NXDOC/VCS+Architecture10http://fastanimals.com/melissa/WhitePapers/
NormalizationDenormalizationWhitePaper.pdf48 / 48