#пост
Довольно часто мне приходилось делать raw sql запросы в БД для получения данных. Причин тому было несколько:
- Так быстрее
- Не нужно писать длинные билдеры
- Запрос часто менялся, проще всего переносить as-is из требований в код
- Подсветка синтаксиса в IDE
- Удобная отладка: скопировал в DB Viewer и запускаешь
- И многое другое
Зачастую в проекте уже была какая-нибудь ORM или Active Record, где всегда результат запроса был объектом с необходимыми свойствами и типами.
Однако работа с raw sql обычно заканчивалась перегоном массивом из одного метода в другой.
Это неудобно:
- Нет типизации параметров функции –
array $data- Нет типизации свойств –
$data['user_id'] – int/string?- Нет понятия наличия свойства в массиве вообще –
$data['unknown_prop'] – ошибка, которую найдешь только при выполнении кодаКак-то давно общаясь в чате про Active Record в Yii3 ко мне пришло озарение, что AR – это всё еще сложно – нужно наследоваться от базового класса или подключать trait, где-то доставать фабрики и самое главное, всё равно raw sql не смапить в модельку.
К тому моменту hydrator из Yii3 был почти готов и я решил сделать Proof Of Concept обычного data-mapper, с использовать db и hydrator.
Вот что из этого вышло:
Remap
Библиотека содержит лишь 1 класс и делегирует работу по исполнению запроса в yiisoft/db. Создание объекта и его наполнение библиотека делегирует в yiisoft/hydrator.
Таким образом, логика выглядит линейной и понятной:
1. Выполняет raw sql запрос
2. Проходим построчно по результату запроса
3. Создаем DTO и наполняем его нужными полями
Всё это скрыто в методе
Remap::map() Пример работы с библиотекой:
final class Todo
{
public int $id;
public string $title;
public int $status;
public int $created_at;
}
$sql = 'SELECT id, title, status, created_at FROM todo WHERE id = :id LIMIT 1';
$params = ['id' => 1];
$remap->map(Todo::class, $sql, $params);
Remap::map() Метод принимает первым аргументов класс, который нужно будет создать и наполнить.
Второй и третий аргументы – сам запрос и параметры для подстановки. Можно передать во второй параметр
string $sql , а в третий array $params , а можно сразу во второй передать [$sql, $params] Метод возвращает
\Generator объектов. Это позволит обрабатывать большие объемы результатов запроса и не падать с нехваткой памяти.Как вариант использования библиотеки я сразу подметил, что удобно было бы использовать класс, как AR и Query Builder, поэтому с легкостью вышеупомянутый пример можно превратить в следующее:
$remap->map(Todo::class, Todo::selectOne($id));
$remap->map(Todo::class, Todo::selectAll());
Использовать можно по-разному, поэтому для еще большего упрощения записи можно написать следующее:
$remap->map(...Todo::selectAll());
Статические методы в
Todo в текущих примерах будут возвращать массив из нужных значений:-
[$sql, $params] в первом примере-
[self::class, $sql, $params] во втором примереА использование yiisoft/hydrator позволило использовать Attributes для произведения дополнительных трансформаций. Например:
final class TodoModelHydrator
{
#[Data('id')] // В свойство $identifier запишется то, что лежит в id свойстве
public string $identifier; // дополнительно cast from int -> to string
#[Data('title')] // title -> text
public string $text;
public string $status; // int -> string
#[Data('created_at')] // created_at -> date_created
public string $date_created; // int -> string
}
Таким образом, результат запроса можно преобразовать под нужные конвенции объектов и не зависеть от устройства внешнего хранилища.
Причесал библиотеку, написал тесты, подкрутил Psalm на 1 уровень, тегнул 1.0.
Надеюсь эта библиотека поможет и вам. Буду рад любым предложениям по улучшению.
——
Полезные ссылки:
Remap
Использование атрибутов yiisoft/hydrator
@handle_topic