База данных · PPA

Конструктор запросов

Запросы к БД собираются цепочкой методов на репозитории: select, from, where, join, orderBy, limit. Условия описываются предикатами Qb:: — они параметризованы, поэтому запрос защищён от инъекций, а части можно свободно комбинировать.

Условия Qb::Стиль fluent-цепочкаПлейсхолдеры :iqbN (авто)

Что такое конструктор запросов и зачем

Конструктор запросов (query builder) — программный способ собирать SQL из методов вместо написания строки целиком.

Проблема. Сырые SQL-строки с интерполяцией значений — прямой путь к SQL-инъекции и дублированию: одну и ту же выборку с чуть разными фильтрами приходится переписывать заново, а собрать условие динамически (по наличию параметров) в строке неудобно.

Решение. Собирайте запрос из методов, а значения передавайте предикатами Qb:: — они уходят в параметры (:iqb0, :iqb1…), а не в текст SQL. Части запроса переиспользуются и комбинируются. Об этом и раздел.

Начало запроса

instance() создаёт репозиторий для запроса; необязательный алиас сразу вызывает as():

php
use Flytachi\Winter\K2\Ppa\Repository\Qb;

$users = UserRepository::instance('u')       // FROM users u
  ->where(Qb::eq('u.status', 'active'))
  ->orderBy('u.id DESC')
  ->limit(20)
  ->findAll();

select / from / as

Метод Назначение
select(string) Заменить список колонок сырым выражением (гидрация → stdClass)
from(string|repo) Переопределить источник (таблица или подзапрос); один раз
as(string) Алиас таблицы (префикс колонок и FROM)
php
$repo->select('status, COUNT(*) AS cnt, AVG(score) AS avg');  // агрегаты
$repo->from('public.archive_users');                          // своя таблица
$repo->as('u');                                               // алиас u

Условия — where + Qb

where() задаёт условие, andWhere / orWhere / xorWhere дописывают. Значение уходит в параметр, не в текст:

php
UserRepository::instance()
  ->where(Qb::eq('role', 'editor'))
  ->andWhere(Qb::eq('status', 'active'))
  ->andWhere(Qb::gt('score', 50))
  ->findAll();
// WHERE role = :iqb0 AND status = :iqb1 AND score > :iqb2

Предикаты Qb

Группа Предикаты
Сравнение eq · neq · gt · gte · lt · lte
NULL isNull · isNotNull
Множество in(col, []) · notIn(col, [])
Шаблон like(col, val, insensitive) · notLike(...)
Диапазон between · notBetween (+ betweenBy)
Логика and(...) · or(...) · xor(...) · clip(...)
Сырое raw('...', [binds]) · custom(...)
Прочее empty() · case(...)

Группировка условий

andWhere/orWhere дописывают на верхнем уровне без скобок. Для вложенной логики группируйте через Qb::and / Qb::or внутри одного where():

php
// (status = 'active') OR (role = 'admin' AND score > 90)
UserRepository::instance()
  ->where(Qb::or(
      Qb::eq('status', 'active'),
      Qb::and(
          Qb::eq('role', 'admin'),
          Qb::gt('score', 90),
      ),
  ))
  ->findAll();

Динамические фильтры

Qb::empty() как нейтральная точка + addAnd() — удобно собирать условие по наличию параметров:

php
$qb = Qb::empty();
if ($status !== null) $qb->addAnd(Qb::eq('status', $status));
if ($role   !== null) $qb->addAnd(Qb::eq('role', $role));

UserRepository::instance()->where($qb)->findAll();

null — безопасный no-op

where(null) не добавляет условие. Это позволяет передавать необязательный фильтр напрямую, без обёрток.

JOIN

Пять типов; источник — строка (таблица) или репозиторий (подзапрос); условие $on — строка или Qb:

Метод SQL ON
join(src, on) JOIN обязательно
joinInner(src, on) INNER JOIN обязательно
joinLeft(src, on) LEFT JOIN обязательно
joinRight(src, on) RIGHT JOIN обязательно
joinCross(src) CROSS JOIN нет
php
OrderRepository::instance('o')
  ->joinLeft('users u',      'o.user_id = u.id')
  ->joinLeft('products p',   'o.product_id = p.id')
  ->where(Qb::eq('o.status', 'shipped'))
  ->select('o.id, u.name, p.title')
  ->findAll();

Источником джойна может быть другой репозиторий — если у него есть доп. части (where/limit), он оборачивается в производную таблицу, а параметры сливаются:

php
$recent = OrderRepository::instance('o')
  ->where(Qb::gt('o.created_at', '2024-01-01'))
  ->select('user_id, SUM(total) AS total_spent');

UserRepository::instance('u')
  ->joinLeft($recent, 'u.id = o.user_id')
  ->findAll();
// LEFT JOIN (SELECT ... FROM orders o WHERE o.created_at > :iqb0) o ON(...)

Группировка, сортировка, лимит

Метод Назначение
groupBy('col') GROUP BY
having(Qb) HAVING (условие как where)
orderBy('col DIR') ORDER BY
limit(n) LIMIT
php
ContractRepository::instance('d')
  ->joinInner('schedule sc', 'sc.document_id = d.id')
  ->where(Qb::eq('d.status', 'active'))
  ->groupBy('d.id')
  ->orderBy('MIN(sc.date) ASC')
  ->limit(50)
  ->findAll();

CTE, UNION и биндинги

PPA поддерживает with() / withRecursive() (CTE), union() / unionAll() и ручные биндинги binding(). Полный reference низкоуровневого построения — в доках CDO.

Выполнение и отладка

Собранный запрос выполняется fetch-методом (findAll, find, count, exists — см. CRUD и выборка). Посмотреть готовый SQL до выполнения:

php
$repo = UserRepository::instance('u')->where(Qb::eq('u.status', 'active'));
echo $repo->buildSql();   // собрать и вернуть SQL со всеми частями
// SELECT u.id, ... FROM users u WHERE u.status = :iqb0

Дальше

  • CRUD и выборкаfind/findAll/count и запись
  • Пагинация — постраничная выдача результатов
  • CDO — низкоуровневый билдер, CTE, union, все операторы Qb