Я разрабатываю библиотеку для работы с Entity Attribute Value (репозиторий), сокращенно EAV (структура базы данных для хранения произвольных данных). В конце прошлой статьи я спросил у вас о чём мне ещё надо написать, вы попросили показать пример использования и сделать замеры быстродействия. Про замеры быстродействия статья была, эта будет о примере использования.
Назначение библиотеки
Прежде чем рассказать об использовании, надо обозначить цели этого использования. Первая цель это автоматизировать запись произвольных данных. Вторая цель - читать ранее записанные данные и делать произвольные выборки по этим данным.
Для этих двух целей применяется EAV, но он существенно замедляется при увеличении объёма данных, и главная цель библиотеки это сделать скорость работы с данными независимой от объёма данных.
Это достигается за счёт использования материализованных представлений и таблиц, и главная задача которую решает библиотека, это синхронизации данных между таблицами EAV и конкретными таблицами, выделенными под каждую категорию (Entity - сущность). Конечно сущность может быть выделена в материализованное представление, библиотека оставляет выбор за пользователем.
Задачи которые решает библиотека "Универсальный каталог":
Запись данных с произвольной структурой;
Выборка данных по произвольным атрибутам;
Примеры использования
Ниже будут приведены примеры того как задать произвольный набор атрибутов для произвольной сущности и как сделать выборку данных с произвольными условиями.
Кроме того будут приведены примеры:
как добавить новый атрибут в уже существующие материализованное представление или таблицу
как добавить новую позицию в уже существующие материализованное представление или таблицу
как обновить значения атрибутов существующей позиции в уже существующем материализованном представление или таблице
Предостережение
Для наглядности в коде не используются константы, для наглядности значения уникальных идентификаторов задаются на кириллице.
Весь приведённый код можно выполнить в консоли с помощью php -a
, надо только подключить автозагрузчик.
$path = [
__DIR__,
'vendor',
'autoload.php',
];
require_once(implode(DIRECTORY_SEPARATOR, $path));
При этом придётся указывать полные имена классов (возможно у меня не правильно настроен php.ini, поэтому я вынужден так делать), для примера:
$db = new \Environment\Database\PdoConnection($path);
Запись данных с произвольной структурой
Создадим произвольную структуру. И создадим одну позицию каталога - запишем данные в эту структуру.
Создать структуру
Структура это набор атрибутов (в терминах EAV - Attribute, в терминах каталога - характеристика), набор атрибутов это сущность - Entity / категория.
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Operator;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
/** @var PDO $conn объект для работы с СУБД */
$conn = (new PdoConnection($path))->get();
/* класс для работы с элементами каталога: сущность, атрибут, значение */
$operator = new Operator($conn);
/* создаём строковый атрибут */
$attribute1 = $operator->createKind(
'дискретный_символьный_атрибут',
'word',
'discrete',
);
/* создаём числовой атрибут */
$attribute2 = $operator->createKind(
'аналоговый_числовой_атрибут',
'number',
'continuous',
);
/* создаём сущность */
$essence = $operator->createBlueprint('сущность');
/* зададим атрибуты для сущности */
$operator->attachKind(
'сущность',
'дискретный_символьный_атрибут',
);
$operator->attachKind(
'сущность',
'аналоговый_числовой_атрибут',
);
Кроме типов данных 'word' и 'number', поддерживаются так же типы 'time' и 'interval' это соответственно 'TIMESTAMP WITH TIME ZONE' и 'INTERVAL' в Postgre.
Создадим одну позицию
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Operator;
$pathParts = [DIR, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
/** @var PDO $conn объект для работы с СУБД */
$conn = (new PdoConnection($path))->get();
/* класс для работы с элементами каталога: сущность, атрибут, значение */
$operator = new Operator($conn);
/* создадим позицию каталога, добавим "товар" в категорию */
$operator->createItem(
'сущность',
'позиция_из_категории_в_каталоге',
);
/* зададим значения для атрибутов позиции,
введём значения для характеристик "товара" */
$operator->changeContent(
'позиция_из_категории_в_каталоге',
'дискретный_символьный_атрибут',
'красный'
);
$operator->changeContent(
'позиция_из_категории_в_каталоге',
'аналоговый_числовой_атрибут',
'1234567890.1234'
);
На текущем этапе:
создали два атрибута
создали сущность
задали сущности два атрибута
создали позицию каталога
задали значения для атрибутов этой позиции
И это самая занудная часть работы с библиотекой, дальше будет проще, будет веселей.
Выборка данных по произвольным атрибутам
Что бы задать условия поиска, надо получить список возможных условий, по какому атрибуту в каких границах можно выполнить поиск.
После этого можно задать границы поиска и получить результаты.
Но наша цель получить результаты поиска за минимальное время, для этого надо выполнять поиск не по представлению созданному из таблиц EAV, а по материализованному представлению или по таблице.
Для этого необходимо поменять источник данных на "быстрый", а перед этим его необходимо создать.
Создать "быстрый" источник данных и задать для сущности работу через этот источник
Получить границы для условий поиска
Задать условия поиска и получить результат
Создать "быстрый" источник данных и задать для сущности работу через этот источник
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Schema;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
/* объект для работы с источником данных конкретной сущности */
$schema = new Schema($pdo, 'сущность');
/* создадим материализованное представление для сущности
и назначим его как источник данных для сущности */
$schema->handleWithRapidObtainment();
/* создадим таблицу для сущности
и назначим её как источник данных для сущности */
$schema->handleWithRapidRecording();
Получить границы для условий поиска
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Browser;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
/* объект для просмотра данных */
$browser = new Browser($pdo);
/* получим границы поиска, допустимые значения фильтров */
$filters = $browser->filters('сущность');
/*
$filters будет содержать:
array (
0 =>
AllThings\SearchEngine\DiscreteFilter::__set_state(array(
'values' =>
array (
0 => 'красный',
),
'attribute' => 'дискретный_символьный_атрибут',
)),
1 =>
AllThings\SearchEngine\ContinuousFilter::__set_state(array(
'min' => '1234567890.1234',
'max' => '1234567890.1234',
'attribute' => 'аналоговый_числовой_атрибут',
)),
)
*/
Задать условия поиска и получить результат
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Browser;
use AllThings\SearchEngine\ContinuousFilter;
use AllThings\SearchEngine\DiscreteFilter;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
$browser = new Browser($pdo);
$numbersFilter = new ContinuousFilter(
'аналоговый_числовой_атрибут',
'0',
'999999999.9999',
);
$wordsFilter = new DiscreteFilter(
'дискретный_символьный_атрибут',
['красный']
);
$filters = [$numbersFilter, $wordsFilter];
/* выполним поиск*/
$result = $browser->filterData('сущность', $filters);
/*
содержимое $result
array (
0 =>
array (
'thing_id' => 1,
'code' => 'позиция_из_категории_в_каталоге',
'дискретный_символьный_атрибут' => 'красный',
'аналоговый_числовой_атрибут' => '1234567890.1234',
),
)
*/
Добавить новый атрибут
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Operator;
use AllThings\ControlPanel\Schema;
$pathParts = [DIR, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
$operator = new Operator($pdo);
/ создаём новый атрибут */
$attribute3 = $operator->createKind(
'дискретный_числовой_атрибут',
'number',
'discrete',
);
/* зададим новый атрибут для сущности */
$operator->attachKind(
'сущность',
'дискретный_числовой_атрибут',
);
/* объект для управления данными сущности */
$schema = new Schema($pdo, 'сущность');
/* Если мы используем в качестве быстрого источника данных
материализованное представление,
то нам необходимо его пересоздать
*/
/* пересоздадим материализованное представление */
$schema->setup($attribute3);
/* в материализованное представление добавиться новая колонка */
/* DTO атрибута - $attribute3, при работе через материализованное представление,
можно не передавать */
/* Если мы используем в качестве быстрого источника данных
таблицу,
то нам необходимо добавить колонку для нового атрибута
*/
/* Добавим колонку для нового атрибута */
$schema->setup($attribute3);
/* в таблице добавиться новая колонка */
/* DTO атрибута - $attribute3, при работе через таблицу,
лучше передать, тогда будет добавлена только одна колонка,
иначе будет пересоздана вся таблица */
/* Что бы в дальнейшем при добавление новых позиций в таблицу,
не было проблем из-за разного количества колонок в представлении и в таблице,
необходимо обновить представление.
Для этого надо поменять способ доступа к данным у объекта сущности
и после этого обновить представление. */
/* изменяем способ доступа к данным сущности */
$schema->changeStorage('view');
/* пересоздаём представление */
$schema->setup();
/* Создадим значения для нового атрибута для всех ранее созданных моделей */
/* будет создана запись для Value - Значения - в EAV таблицах */
$operator->expandItem(
'позиция_из_категории_в_каталоге',
'дискретный_числовой_атрибут',
'-1'
);
/* Если этого не сделать, то присвоить (или обновить) Значение для Атрибута
не получиться, потому что фактически значение не было создано
и в БД нет записи для обновления */
/* после этого надо вернуть способ доступа */
/* материализованное представление */
$schema->changeStorage('materialized view');
/* таблица */
$schema->changeStorage('table');
/* при этом пересоздавать или "освежать" источник данных не надо */
Добавить новую позицию
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Operator;
use AllThings\ControlPanel\Schema;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
/* объект для работы с сущностями, атрибутами, значениями */
$operator = new Operator($pdo);
/* создаём новую позицию */
$thing = $operator->createItem(
'сущность',
'новая-позиция',
);
/* объект для управления данными сущности */
$schema = new Schema($pdo, 'сущность');
/* не зависимо от того
материализованное представление или
таблица,
выбраны в качестве способа доступа к данным сущности,
достаточно выполнить "освежение" источника данных */
$schema->refresh();
/* если способ доступа материализованное представление, то оно будет пересчитано
если способ доступа - таблица , то новая запись будет добавлена из представления,
поэтому важно, что бы в представлении и в таблице были одни и те же колонки */
Обновить значения атрибутов
use Environment\Database\PdoConnection;
use AllThings\ControlPanel\Operator;
use AllThings\ControlPanel\Schema;
use AllThings\DataAccess\Crossover\Crossover;
$pathParts = [__DIR__, 'configuration', 'pdo.env',];
$path = implode(DIRECTORY_SEPARATOR, $pathParts);
$pdo = (new PdoConnection($path))->get();
/* оператор для работы с сущностями, атрибутами, значениями */
$operator = new Operator($pdo);
/* Если способ доступа к данным сущности - материализованное представление,
то нам необходимо обновить данные в таблицах EAV.
Если способ доступа к данным - таблица,
то обновлять таблицы EAV не надо, они будут обновлены вместе с данными таблицы
*/
/* обновляем значение атрибута у конкретной позиции в таблицах EAV */
$operator->changeContent(
'новая-позиция',
'дискретный_числовой_атрибут',
'0',
);
/* объект для управления данными сущности */
$schema = new Schema($pdo, 'сущность');
/* Если способ доступа к данным сущности - материализованное представление,
то его надо освежить */
$schema->refresh();
/* Если способ доступа к данным сущности - таблица,
то нам для того что бы освежить данные в таблице,
достаточно отдать массив новых значений,
для каждой позиции требуется отдельный вызов Schema::refresh(array $values = [])
*/
$value =
(new Crossover())
->setLeftValue('новая-позиция')
->setRightValue('дискретный_числовой_атрибут')
->setContent('0');
$schema->refresh([$value]);
Заключение
По большей части все операции выполняются на высоком уровне абстракции.
Большинство действий выполняется в две-четыре операции. Из-за количества комментариев в коде кажется, что кода много, но если откинуть болерплейт с юзингами, с созданием объекта PDO, с созданием других объектов, то останется от двух до пяти строк действительно работающего кода.
С помощью этой статьи вы легко освоитесь с использованием библиотеки "Универсальный каталог".
Пользуйтесь на здоровье :)