Всем привет.
У меня было несколько публикаций по теме Entity Attribute Value, сокращенно EAV (паттерн программирования для хранения произвольных данных). За прошедшее время я допилил библиотеку для работы с EAV, и хотел бы поделиться с вами своими наработками.
В библиотеке реализован базовый сценарий:
Создать сущность
Создать атрибут
Задать для сущности определённые атрибуты
Задать значение для атрибутов (создать предмет)
Сформировать хранилище для предметов в формате представления, материализованного представления, таблицы
Вычислить значения фильтров для поиска по предметам
Выполнить поиск по предметам
Добавить новый предмет
Добавить новый атрибут для сущности
Изменить значение атрибута (изменить характеристику предмета)
Изменения данных в пунктах 8-10, автоматически отражаются в хранилище данных не зависимо от формата выбранного в пункте 5.
Если кто то чувствует в себе интерес к этой теме, то предлагаю присоединиться к разработке.
Если вам хочется получить опыт в программировании чуть больше чем очередной HellowWord, то для вас тоже можно придумать интересный челендж.
Для тех кто не в курсе, EAV это когда нам не надо в ручную проектировать и создавать новую таблицу в БД приложения для хранения информации о новой бизнес сущности, потому что у нас уже есть три таблицы и нам этого хватает для всего.
В моей реализации шесть таблиц. Если хочется хранить данные не только как строки, то тогда это будет семь таблиц и плюс по одной таблице на каждый тип данных, но не в этом суть статьи, речь не столько о реализации, сколько о proof of concept , который можно потрогать руками.
Предыдущие публикации на эту тему:
Как установить
Для работы библиотеки потребуется СУБД PostgreSQL9+ и PHP7.4+ (в теории код можно упростить до PHP5, но тогда не будет type hitting, что не камильфо)
Скачать код из репозитория storage-for-all-things
Развернуть СУБД с помощью sql скрипта из файла
configuration/install-tables-and-roles.sql.example
Настроить соединение с СУБД - отредактировать файл
configuration/db_test.php.example
Переименовать
configuration/db_test.php.example
вconfiguration/db_test.php
Выполнить тест в файле
tests/Integration/AutomatedProcessTest.php
Если все тесты прошли успешно, значит вы всё настроили без ошибок.
Если какие то тесты упали, то значит мне надо править баги :) Буду благодарен за ваши баг репорты.
Как использовать
Как использовать библиотеку вам будет понятно из кода и комментариев теста AutomatedProcessTest
.
Наружу из библиотеки торчит три ручки, три класса:
Operator
AllThings\ControlPanel\Operator
Browser
AllThings\ControlPanel\Browser
Schema
AllThings\ControlPanel\Schema
Класс Operator работает непосредственно с данными.
Класс Browser позволяет просматривать данные.
Класс Schema создаёт объект СУБД для хранения данных и для сохранения согласованности данных (обновления по необходимости).
О какой согласованности речь ? Собственно данные хранятся в трёх таблицах, но для удобства доступа к данным их можно хранить в трёх разных формах: представление, материализованное представление или таблица. Отсюда возникает задача согласования данных в исходных трёх таблицах и в той форме которая была выбран для хранения.
Если, с представлением, синхронизация происходит автоматически на уровне СУБД, то с материализованным представлением и таблицей, необходимы дополнительные действия. За их выполнение отвечает класс Schema.
Класс Operator
Этот класс позволяет:
1 Создать сущность (чертёж для создания предметов)
public function createBlueprint(
string $code,
string $storageKind = Storable::DIRECT_READING,
string $title = '',
string $description = ''
): IEssence
2 Создать атрибут (характеристика предмета)
public function createKind(
string $code,
string $dataType,
string $rangeType,
string $title = '',
string $description = ''
): IAttribute
3 Для сущности добавить атрибут
public function attachKind(string $essence, string $kind): Operator
4 Создать предмет
public function createItem(
string $essence,
string $code,
string $title = '',
string $description = ''
): Nameable
5 Задать значение для атрибута (задать значение для характеристики предмета)
public function changeContent(
string $thing,
string $attribute,
string $content
)
6 Добавить предмету новый атрибут (задать значение для новой характеристики предмета)
public function expandItem(
string $thing,
string $attribute,
string $value
): Operator
Используя эти шесть функций, мы можем вести базу данных нашего Универсального каталога, базу данных предметов.
Но когда мы затевали наш Универсальный каталог, нашей задаче было не только создание базы данных с характеристиками предметов, прежде всего мы хотим наши данные отдавать на просмотр, в этом нам поможет класс Browser.
Класс Browser
Вычислить параметры поиска
Выполнить поиск по заданным параметрам
1 Получить фильтры для выполнения поиска
public function filters(string $code): array
Фильтры вернуться в формате массива, который придётся разбирать руками, ни каких DTO для облегчения понимания не было сделано, извините, такие издержки у proof of concept.
2 Выполнить поиск по заданным параметрам
public function filterData(string $code, $filters = []): array
Данные вернуться как массив, что делать с этим массивом пусть решает вызывающий код.
Фильтры передаются как массив экземпляров классов AllThings\SearchEngine\ContinuousFilter
и AllThings\SearchEngine\DiscreteFilter
, это ещё два класса которые торчат из библиотеки, можно было конечно обойтись массивом со своей системой индексов, но в этом случае я не поленился и сделал DTO.
Как задать параметры фильтрации можно посмотреть в тесте tests/Integration/AutomatedProcessTest.php
.
Я рассказал о том как задать данные и как их выгрузить, теперь мне осталось рассказать о том как данные быстро выдать и как данные быстро изменить. Всё это задачи класса Schema.
Класс Schema
Основное назначение этого класса, это:
Создать на основе сущности (на основе атрибутов) объект СУБД для хранения значений
Обновить значение в объекте СУБД при изменении значения атрибута сущности (конкретного предмета)
1 Создать хранилище для значений атрибутов
public function setup(): Schema
Что будет сделано ?
Будет проверено какая форма был выбрана для хранения данных, это может быть:
Storable::DIRECT_READING
- viewStorable::RAPID_OBTAINMENT
- materialized viewStorable::RAPID_RECORDING
- table
Далее будет создан соответствующий объект СУБД, и он будет заполнен данными в соответствии с тремя базовыми таблицами.
2 Обновить значение при изменении этого значения в базовой таблице
public function refresh(?ICrossover $value = null): Schema
Тут мы видим ещё один класс, который просочился наружу из нашей библиотеки.
ICrossover
это интерфейс который хранит значение атрибута для конкретного предмета.
Что произойдёт при вызове этого метода ? будет определена форма хранения данных и эти данные будет соответствующим способом обновлены.
Если мы хотим что бы было обновлено только это значение, то в ICrossover
следует передать новое значение атрибута, если значение не передать, то хранилище значений будет обновлено полностью.
Последний нюанс о котором надо обязательно упомянуть, это то как мы сообщаем системе о том в какой форме мы хотим хранить наши данные для быстрого доступа.
3 Задать форму хранения данных
public function changeStorage(string $storageKind): Schema
Как было сказано выше, формой может быть одна из следующих:
Storable::DIRECT_READING
- viewStorable::RAPID_OBTAINMENT
- materialized viewStorable::RAPID_RECORDING
- table
О выборе формы
В статье Идеальный каталог, оптимизация выборки данных я приводил показатели быстродействия по всем формам.
К этому можно добавить, то что форма представления и материализованного представления возможна только для хранения данных внутри одной БД. В то время как форма таблицы делает возможным хранение данных в отдельной СУБД.
Сейчас конечно библиотека не даёт такой возможности, но это только proof of concept, который можно развить как угодно.
Замеры производительности
Не будет замеров. Конечно без них статья сильно теряет в ценности. Но что бы написать тесты с профилированием потребуется один полноценный рабочий день и прямо сейчас я не обладаю такой роскошью.
Откладывать публикацию очень не хочется и так вся эта история растянулась на три года :)
Планы
Как вы могли заметить в статье речь идёт только о добавлении данных: добавить сущность, добавить атрибут, добавить значение, ни разу не было речи о том что бы что то удалить.
Удаление на самом деле не реализовано, сделать не долго, но для proof of concept не требуется. Для использования в работе в библиотеке много не хватает, поэтому я для себя не вижу смысла доводить её до ума. Если кому то захочется, то пожалуйста, давайте сделаем. Мне для самого себя это не нужно.
Мои контакты у меня в профиле и в репозитории.
В будущем возможно соберусь и подробней распишу использование библиотеки с конкретным примером, или напишу тесты и померяю время выполнения операций.
Зависит конечно от реакции и выбора сообщества.
Спасибо за внимание.
PrinceKorwin
А вот то, что не делали тестов на производительность, да ещё и в течении 3 лет это вы зря. Тем более, что речь о библиотеке которая работает с БД.
И дело же не в отсутствующем времени, верно? Мне кажется вы просто лукавите и не договариваете. Производительность EAV схем всегда была ключевой проблемой.
Современный PostgreSQL имеет JSONB. Который мне кажется будет предпочтительнее к использованию на текущий момент, чем EAV.
pbatanov
А есть сравнение производительности поиска по json (через GIN) и поиска в EAV (где, полагаю, достаточно какого-нибудь хеша)? Ну т.е. например задача, найти все сущности у которых не заполнен (отсутствует значение) атрибут X.
PrinceKorwin
На JSONB (не json) в PostgreSQL можно назначать индексы.
pbatanov
Я знаю, поэтому я и указал в скобках GIN. мой вопрос не про возможность, а про производительность.
PrinceKorwin
В вашем вопросе будет сравниваться производительность индекса с индексом без относительно кто был источником данных (EAV или JSONB — не важно).
Jsonb поддерживает не только GIN индексы (btree и hash также поддерживаются).
pbatanov
Именно. В этом и суть вопроса. Но я на него все еще не получил ответ. Есть ли у вас бенчмарки, которые подтверждают ваши тезисы?
Из этих утверждений я делаю вывод, что вы считаете реализацию указанной в статье логике через JSONB производительней, чем через EAV. Я прошу вас чем-то подкрепить это утверждение и в качестве примера дал вполне конкретный юзкейс.
PrinceKorwin
Какие? Если вы на EAV используете btree и его же на JSONB на одних и тех же данных (и если собранная статистика использования схожа), то производительность будет идентична. Вы в этом сомневаетесь?
На своей практике я сталкивался с разными системами реализующие EAV на разных БД (Oracle, DB2, PostgreSQL), а также с PostgreSQL с JSONB.
Что именно выбрать — вопрос критериев. О которых я уже упоминал.
На мой взгляд наиболее эффективным был подход с обычной реляционной структурой для данных у которых структура статична или редко изменяема и вынесением динамической части в JSONB (если речь идет про PostgreSQL).
Но опять таки все зависит от исходных задач и профиля нагрузки на БД — серебренной пули не бывает.
В вашем примере заведомо в проигрышной позиции JSONB т.к. для него вы выбрали GIN индекс, тогда как для EAV hash. Если я правильно понял ваш юзкейс.
pbatanov
Я не очень понял ваш тезис про "проигрышную позицию". Я вам процитировал документацию постгри, в которой сказано, что другие индексы для JSONB полезны только для прямого сравнения документов, а не поиска по ним. Если это неверная информация, то мне было бы интересно почитать и про это тоже. В целом меня интересуют оба подхода с самыми эффектиными индексами. Не важно, что это именно будет.
Так все таки JSONB или не бывает? Мой вопрос возник именно потому, что изначальное утверждение было достаточно безапелляционным, поэтому мне стали интересны какие-то подтверждения этого тезиса. Потому что на горизонте есть подобного рода задачи.
PrinceKorwin
Этот мой тезис был про EAV без относительно JSONB. Если бы у EAV не было проблем с производительностью, то все бы на нем и сидели — никто не любит накатывать DDL в продакшене :)
В своём сообщении я добавил ведь перечисление когда EAV может быть интересен/полезен.
brtee и hash индексы вполне можно делать на конкретные поля в JSONB. Не обязательно сравнивать документы целиком. Пример:
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Если JSON:
{«tags»: {«nk88»: {«ik37»: «iv161»}}, «publisher»: «XlekfkLOtL», «criticrating»: 3}
То индекс publisherhash идет только на поле «publisher». И позволяет искать только по нему.
pbatanov
Это понятно, что так можно, но это кажется несколько расходится с той идеей, что пользователь(оператор) может сам атрибут создать (иначе зачем нам EAV, а не предописанная в коде схема). Создавать индексы по действиям пользователя - я не уверен что хорошая затея, особенно на нагруженной базе.
Ну, это можно решать другим способом, например мастер данные хранить как EAV, а для производительных операций использовать отдельный слой, который будет работать с документами. Такое решение
У нас на самом деле с EAV другая проблема. EAV в отличие от документного формата гораздо сложней версионировать. Вот тут проблемы (в том числе индексации) вылезают в полный рост, а минусы JSONB наоборот - уходят, т.к. больше не надо редактировать отдельное поле в документе, перезаписывая весь документ. В такие моменты удобство работы с документом вместо EAV становится прям супер очевидным/
Мы сами в новых задачах склоняемся к тому, чтобы делать документное хранилище как раз из-за того, что с ним гораздо комфортней работать при многих сценариях. А везде, где некомфортно - будут сделаны отдельные слои, которые решат эту проблему более эффективным способом, если необходимо
PrinceKorwin
Да. Всё верно. Сколько не видел EAV всегда в итоге приходили к какому-то гибридному виду.
pbatanov
del хабр чет подвисает, задублировалось
FanatPHP
Мне кажется что по крайней мере на части задач быстродействие не будет принципиальным моментом.
Скажем, пресловутый поиск по параметрам все равно отдается на откуп внешнему поисковому движку, а от основной БД требуется только редактирование отдельных значений, ну и чтение всех подряд.
pbatanov
Ну, это не всегда так. У меня есть опыт работы с системой, у которой быстрый поиск для клиентов - да, отдельный поисковик из производных значений (он не индексируют напрямую базу а отдельный поток данных из нее). а для внутренних нужд - на базе. Плюс есть задачи массовой обработки сущностей - это иногда удобно делать транзакционно, что тоже лучше работает, когда поиск идет по БД
SbWereWolf Автор
Вы заблуждаетесь на счёт EAV, посмотрите замеры в статье «Идеальный каталог, оптимизация выборки данных», ссылки по тексту есть.
Когда мы из EAV делаем таблицу или материализованное представление, то поиск по ним ни как не может быть медленней JSONB.
И главная задача этой библиотеки это создание инфраструктуры для работы с EAV через индивидуальную таблицу для данных которые часто обновляются или для работы через материализованное представление для данных которые обновляются крайне редко.
JSONB затащили в постгрес что бы утереть нос MongoDB и прочим. Почему стали популярны документарные БД? потому что не надо заморачиваться на работу со схемой базы данных, во первых это же надо кроме языка программирования научиться разбираться с DDL, что то понимать в СУБД. Второе надо написать библиотечку для работы с EAV и по пути собрать море граблей.
Правильно что ни кто не это не заморачивается, для стартапов это не нужный риск.
А если ты пишешь код для себя, исключительно из академического интереса, то почему бы и нет.
Мне было интересно проверить концепцию, я её проверил и сделал это ещё три года назад в 2018 году когда написал статью «Идеальный каталог, оптимизация выборки данных».
Разработка библиотеки это ещё одно доказательство в пользу жизнеспособности идеи.
И я думаю что эта идея была реализован и не раз, просто в инхаус системах, поэтому широкой публике не известна.
PrinceKorwin
Только вот у материализированного представления есть свои нюансы.
Любая идея может быть жизнеспособной. Вопрос только в критериях её применимости. Вы же понимаете, что серебренной пули не бывает.
Поэтому (лично мне) в этой статье для большей объективности не хватает:
1. замеров производительности
2. критериев когда EAV уместен, а когда — нет