Всем привет.

У меня было несколько публикаций по теме Entity Attribute Value, сокращенно EAV (паттерн программирования для хранения произвольных данных). За прошедшее время я допилил библиотеку для работы с EAV, и хотел бы поделиться с вами своими наработками.

В библиотеке реализован базовый сценарий:

  1. Создать сущность

  2. Создать атрибут

  3. Задать для сущности определённые атрибуты

  4. Задать значение для атрибутов (создать предмет)

  5. Сформировать хранилище для предметов в формате представления, материализованного представления, таблицы

  6. Вычислить значения фильтров для поиска по предметам

  7. Выполнить поиск по предметам

  8. Добавить новый предмет

  9. Добавить новый атрибут для сущности

  10. Изменить значение атрибута (изменить характеристику предмета)

Изменения данных в пунктах 8-10, автоматически отражаются в хранилище данных не зависимо от формата выбранного в пункте 5.

Если кто то чувствует в себе интерес к этой теме, то предлагаю присоединиться к разработке.

Если вам хочется получить опыт в программировании чуть больше чем очередной HellowWord, то для вас тоже можно придумать интересный челендж.

Для тех кто не в курсе, EAV это когда нам не надо в ручную проектировать и создавать новую таблицу в БД приложения для хранения информации о новой бизнес сущности, потому что у нас уже есть три таблицы и нам этого хватает для всего.

В моей реализации шесть таблиц. Если хочется хранить данные не только как строки, то тогда это будет семь таблиц и плюс по одной таблице на каждый тип данных, но не в этом суть статьи, речь не столько о реализации, сколько о proof of concept , который можно потрогать руками.

Предыдущие публикации на эту тему:

  1. Идеальный каталог, набросок архитектуры

  2. Идеальный каталог, вариант реализации

  3. Идеальный каталог, оптимизация выборки данных

Как установить

Для работы библиотеки потребуется СУБД PostgreSQL9+ и PHP7.4+ (в теории код можно упростить до PHP5, но тогда не будет type hitting, что не камильфо)

  1. Скачать код из репозитория storage-for-all-things

  2. Развернуть СУБД с помощью sql скрипта из файла configuration/install-tables-and-roles.sql.example

  3. Настроить соединение с СУБД - отредактировать файл configuration/db_test.php.example

  4. Переименовать configuration/db_test.php.example в configuration/db_test.php

  5. Выполнить тест в файле tests/Integration/AutomatedProcessTest.php

Если все тесты прошли успешно, значит вы всё настроили без ошибок.

Если какие то тесты упали, то значит мне надо править баги :) Буду благодарен за ваши баг репорты.

Как использовать

Как использовать библиотеку вам будет понятно из кода и комментариев теста AutomatedProcessTest.

Наружу из библиотеки торчит три ручки, три класса:

  1. Operator AllThings\ControlPanel\Operator

  2. Browser AllThings\ControlPanel\Browser

  3. 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. Вычислить параметры поиска

  2. Выполнить поиск по заданным параметрам

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. Создать на основе сущности (на основе атрибутов) объект СУБД для хранения значений

  2. Обновить значение в объекте СУБД при изменении значения атрибута сущности (конкретного предмета)

1 Создать хранилище для значений атрибутов

public function setup(): Schema

Что будет сделано ?

Будет проверено какая форма был выбрана для хранения данных, это может быть:

  • Storable::DIRECT_READING - view

  • Storable::RAPID_OBTAINMENT - materialized view

  • Storable::RAPID_RECORDING - table

Далее будет создан соответствующий объект СУБД, и он будет заполнен данными в соответствии с тремя базовыми таблицами.

2 Обновить значение при изменении этого значения в базовой таблице

public function refresh(?ICrossover $value = null): Schema

Тут мы видим ещё один класс, который просочился наружу из нашей библиотеки.

ICrossover это интерфейс который хранит значение атрибута для конкретного предмета.

Что произойдёт при вызове этого метода ? будет определена форма хранения данных и эти данные будет соответствующим способом обновлены.

Если мы хотим что бы было обновлено только это значение, то в ICrossover следует передать новое значение атрибута, если значение не передать, то хранилище значений будет обновлено полностью.

Последний нюанс о котором надо обязательно упомянуть, это то как мы сообщаем системе о том в какой форме мы хотим хранить наши данные для быстрого доступа.

3 Задать форму хранения данных

public function changeStorage(string $storageKind): Schema

Как было сказано выше, формой может быть одна из следующих:

  • Storable::DIRECT_READING - view

  • Storable::RAPID_OBTAINMENT - materialized view

  • Storable::RAPID_RECORDING - table

О выборе формы

В статье Идеальный каталог, оптимизация выборки данных я приводил показатели быстродействия по всем формам.

К этому можно добавить, то что форма представления и материализованного представления возможна только для хранения данных внутри одной БД. В то время как форма таблицы делает возможным хранение данных в отдельной СУБД.

Сейчас конечно библиотека не даёт такой возможности, но это только proof of concept, который можно развить как угодно.

Замеры производительности

Не будет замеров. Конечно без них статья сильно теряет в ценности. Но что бы написать тесты с профилированием потребуется один полноценный рабочий день и прямо сейчас я не обладаю такой роскошью.

Откладывать публикацию очень не хочется и так вся эта история растянулась на три года :)

Планы

Как вы могли заметить в статье речь идёт только о добавлении данных: добавить сущность, добавить атрибут, добавить значение, ни разу не было речи о том что бы что то удалить.

Удаление на самом деле не реализовано, сделать не долго, но для proof of concept не требуется. Для использования в работе в библиотеке много не хватает, поэтому я для себя не вижу смысла доводить её до ума. Если кому то захочется, то пожалуйста, давайте сделаем. Мне для самого себя это не нужно.

Мои контакты у меня в профиле и в репозитории.

В будущем возможно соберусь и подробней распишу использование библиотеки с конкретным примером, или напишу тесты и померяю время выполнения операций.

Зависит конечно от реакции и выбора сообщества.

Спасибо за внимание.