Всем привет.
У меня было несколько публикаций по теме 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 не требуется. Для использования в работе в библиотеке много не хватает, поэтому я для себя не вижу смысла доводить её до ума. Если кому то захочется, то пожалуйста, давайте сделаем. Мне для самого себя это не нужно.
Мои контакты у меня в профиле и в репозитории.
В будущем возможно соберусь и подробней распишу использование библиотеки с конкретным примером, или напишу тесты и померяю время выполнения операций.
Зависит конечно от реакции и выбора сообщества.
Спасибо за внимание.