Руководство Melbis Shop: оглавление
Весь обмен модуля с БД идёт через методы
MELBIS()->Sql*. Прямых обращений к драйверу СУБД из кода
модуля нет и быть не должно: методы парсера, помимо самого запроса,
ведут статистику для отладчика, проверяют префиксы таблиц и метят
изменённые таблицы для системы кеширования.
Три вещи повторяются в каждом вызове.
Первым аргументом всегда идёт __LINE__.
Это не формальность: по номеру строки отладчик показывает, какой именно
запрос модуля выполнился, сколько занял и с какими параметрами, а при
ошибке SQL — где её искать.
Имя таблицы пишется через {DBNICK}.
Реальный префикс базы подставит движок. Если написать префикс руками,
парсер остановится с ошибкой «Wrong prefix in table name» — и проверка
стоит намеренно: именно по этой записи движок узнаёт, от каких таблиц
зависит модуль, и когда сбрасывать его кеш. Пропусти он такой запрос,
зависимость осталась бы ненайденной, а кеш перестал бы обновляться.
Значения передаются параметрами, а не подстановкой в строку:
$command = "SELECT id, name
FROM {DBNICK}_store
WHERE topic_id = :TOPIC_ID
AND price <= :PRICE_MAX
";
$param = [
'topic_id' => $topic_id,
'price_max' => $price_max
];
$store = MELBIS()->SqlSelect(__LINE__, $command, $param);Плейсхолдер в тексте запроса пишется заглавными буквами с двоеточием
— :TOPIC_ID. Ключи массива параметров движок приводит к
верхнему регистру сам, так что в PHP их можно писать как удобно. Запрос
уходит подготовленным (prepared statement): значение попадает в базу
отдельно от текста запроса, поэтому кавычка или точка с запятой внутри
значения ничего не сломают.
Массив параметром передать нельзя — драйвер
связывает только скаляры. Список для IN (...) собирается в
PHP, с обязательным приведением типа:
$brand_ids = array_map('intval', $brand_set);
$brand_line = implode(',', $brand_ids);
$command = "SELECT id FROM {DBNICK}_store WHERE brand_id IN ($brand_line)";Приведение здесь — не украшение: это единственное, что стоит между списком из запроса пользователя и текстом SQL.
Методы отличаются только формой результата — текст запроса и параметры у всех устроены одинаково.
| Метод | Возвращает |
|---|---|
SqlSelect($mLine, $mCommand, $mParams) |
массив всех записей |
SqlSelectFlat($mLine, $mCommand, $mParams) |
одну запись плоским массивом (пустой массив, если ничего не нашлось) |
SqlSelectValue($mLine, $mCommand, $mDefault, $mParams) |
значение столбцов одной записи; форму и запасное значение задаёт
$mDefault (см. 2.1) |
SqlSelectLimit($mLine, $mCommand, $mOffset, $mLimit, $mParams) |
страницу записей и общее число строк |
SqlSelectPage($mLine, $mCommand, $mOrder, $mOffset, $mLimit, $mParams) |
то же самое, но отсекая строки на стороне сервера |
SqlSelectEnum($mLine, $mCommand, $mKey, $mValue, $mParams) |
все записи, у которых поле $mKey равно
$mValue |
SqlSelectEnumFlat($mLine, $mCommand, $mKey, $mValue, $mParams) |
одну такую запись |
SqlSelectEnumValue($mLine, $mCommand, $mDefault, $mKey, $mValue, $mParams) |
значение столбцов такой записи (как
SqlSelectValue) |
SqlSelectStatic / SqlSelectStaticFlat /
SqlSelectStaticValue |
то же, что
SqlSelect/SqlSelectFlat/SqlSelectValue,
но с кешированием результата в памяти (см. «Кеширование») |
SqlSelectValue — надстройка над
SqlSelectFlat для запросов, где нужна не запись, а
значение: счётчик, MAX(...), одно-два поля. Метод берёт
первую строку и отдаёт значения её столбцов, а не хеш с
именами полей.
Форму результата и поведение при пустой выборке задаёт
$mDefault — обязательный аргумент сразу после запроса.
Скалярный $mDefault → возвращается
значение первого столбца; если строки нет или значение столбца
NULL — сам $mDefault:
$param_count = [
'clann' => $clann
];
$command = "SELECT COUNT(*) FROM {DBNICK}_store WHERE clann = :clann";
$total = MELBIS()->SqlSelectValue(__LINE__, $command, 0, $param_count);
NULLиз базы — это «значения нет», он тоже уходит в$mDefault. Это и есть главный случай:SUM,MAX,MIN,AVGна пустой выборке возвращают одну строку со значениемNULL(а не ноль строк), поэтому дефолт0для них обязателен — иначе вернётсяNULL, а не число.
$mDefault-массив → его длина задаёт
число столбцов. Возвращается числовой массив (не хеш) — раскладывается
через list(). Лишние столбцы отбрасываются, недостающие и
NULL-столбцы берутся из $mDefault по позиции;
при пустой выборке возвращается весь $mDefault:
$param_id = [
'id' => $id
];
$command = "SELECT name, price FROM {DBNICK}_store WHERE id = :id";
list($name, $price) = MELBIS()->SqlSelectValue(__LINE__, $command, ['', 0], $param_id);SqlSelectEnumValue делает то же поверх
SqlSelectEnumFlat (см. 2.2), а
SqlSelectStaticValue — кеширующий вариант (см.
«Кеширование»).
SqlSelectEnum и SqlSelectEnumFlat решают
классическую задачу: на странице выводится сорок карточек товара, каждая
— отдельный модуль, и каждому нужна своя запись из store.
Прямолинейное решение даёт сорок запросов.
Эти методы делают запрос один раз, раскладывают весь
результат по значению поля $mKey и держат в памяти до конца
запроса. Второй и последующие вызовы с тем же текстом запроса и теми же
параметрами в базу уже не идут — они забирают готовую запись из
разложенного массива:
// Внутри модуля карточки: запрос написан на все товары сразу,
// а модуль забирает из результата только свой
$ids = MELBIS()->EnumGet('kStore', $id);
$command = "SELECT id, name, price
FROM {DBNICK}_store
WHERE id IN ( $ids )
";
$store = MELBIS()->SqlSelectEnumFlat(__LINE__, $command, 'id', $id);Список идентификаторов для такого запроса готовит модуль-родитель
через EnumSet, а EnumGet его отдаёт — эта пара
описана в разделе «Кеширование».
Ключ памяти считается по тексту запроса и параметрам, поэтому текст запроса должен быть одинаковым во всех вызовах. Собранное условие, которое меняется от вызова к вызову, эту оптимизацию отключит: каждый вариант станет отдельным запросом.
Оба метода возвращают массив с ключами total (всего
строк), offset, limit и rows
(записи страницы).
$goods = MELBIS()->SqlSelectLimit(__LINE__, $command, $offset, $limit, $param);
MELBIS()->TplAssign($tpl, [
'GOODS' => $goods['rows'],
'TOTAL' => $goods['total']
]);SqlSelectLimit запрос не трогает:
сервер отдаёт весь результат, метод берёт из него нужный срез уже на
стороне PHP. Работает на любой версии MySQL. На выдаче в сотню строк
разницы не видно; на нескольких тысячах широких записей это мегабайты,
прогнанные по сети и развёрнутые в PHP-массивы ради девяти карточек на
экране.
SqlSelectPage оборачивает запрос,
добавляет LIMIT и получает общее число строк оконной
функцией COUNT(*) OVER(). Сервер возвращает ровно страницу.
Требует MySQL 8.0+ или MariaDB 10.2+.
Сортировка передаётся отдельным параметром — массивом «поле → направление»:
$order = [
'status_pos' => 'asc',
'price' => 'desc',
's.id' => 'asc'
];
$goods = MELBIS()->SqlSelectPage(__LINE__, $command, $order, $offset, $limit, $param);Три требования к запросу, которые движок не проверяет:
ORDER BY внутри запроса быть не
должно. Оставленный внутри будет молча проигнорирован: порядок
строк производной таблицы стандартом не гарантируется, и сервер вправе
его отбросить.SELECT s.id, p.id ...) — производная таблица такого не
примет.Направление проверяется по списку (asc /
desc) — здесь ничего постороннего не пройдёт. А вот
имя поля проверяется только по набору символов, и набор
этот намеренно широкий: он пропускает скобки и двоеточие, чтобы работали
выражения вида RAND(:TOWN_HASH). Значит через имя поля
можно протащить вызов функции или подзапрос — кавычки и запятые
отсекаются, но (SELECT ...) состоит из разрешённых
символов.
Отсюда правило: имя поля сортировки задаёт разработчик, а не
посетитель. Пришедший из адресной строки
?order=price разбирайте через switch и
подставляйте в массив своё имя колонки — никогда не отдавайте
пользовательскую строку в ключ напрямую.
Нужен редко — когда результат обрабатывается потоком и разворачивать
его в массив целиком не хочется. SqlQuery возвращает
указатель на результат, дальше работают четыре метода:
$query = MELBIS()->SqlQuery(__LINE__, $command, $param);
$rows = MELBIS()->SqlNumRows($query);
MELBIS()->SqlDataSeek($query, 100);
for ( $i = 1; $i <= 20; $i++ )
{
$hash = MELBIS()->SqlFetchHash($query);
}SqlNumRows($query) — число строк в результате.SqlFetchHash($query) — очередная запись ассоциативным
массивом.SqlFetchRow($query) — то же, но с числовыми
индексами.SqlDataSeek($query, $pos) — перевести указатель на
позицию.Условие в запросе пишется по идентификатору, а разработчик знает
символьное имя: id в каждом магазине свой,
skey один и тот же. Карту skey → id по любой
справочной таблице отдаёт SysTableSkey('advert') — метод из
другой семьи, разобран в разделе «Системные двери и утилиты». Там же и
причина, по которой к валютам, поставщикам, языкам, группам покупателей
и параметрам товаров он не нужен: у них есть свои двери, и
id лежит в каждой строке.
Три метода закрывают почти все случаи. Все они возвращают число затронутых строк и сами помечают таблицу изменённой для системы кеширования.
// Вставка
$fields = [
'id' => MELBIS()->SqlGenId('store_param'),
'store_id' => $store_id,
'param_id' => $param_id,
'value_dec' => $value
];
MELBIS()->SqlInsert(__LINE__, '{DBNICK}_store_param', $fields);
// Изменение: последний аргумент — поле (или массив полей) условия
$fields = [
'id' => $store_id,
'price' => $price,
'status_key'=> 'kExist'
];
MELBIS()->SqlUpdate(__LINE__, '{DBNICK}_store', $fields, 'id');
// Удаление
MELBIS()->SqlDelete(__LINE__, '{DBNICK}_store_param', 'store_id', $store_id);У SqlUpdate поля условия перечисляются в том же массиве
$fields: движок раскладывает массив сам — что названо в
$mKeyField, уходит в WHERE, остальное в
SET. Условие может быть составным:
MELBIS()->SqlUpdate(__LINE__, $table, $fields, ['store_id', 'param_id']).
SqlDelete принимает либо пару «поле, значение», либо
массив условий целиком:
MELBIS()->SqlDelete(__LINE__, $table, ['store_id' => $id, 'param_id' => $param]).
Идентификаторы новых записей выдаёт
SqlGenId, а не AUTO_INCREMENT:
$store_id = MELBIS()->SqlGenId('store');Имя таблицы передаётся без префикса — так, как оно
записано в таблице {DBNICK}_generator. Метод атомарно
увеличивает счётчик и возвращает новое значение. Такая схема принята
потому, что идентификаторы должны быть согласованы между сервером и
нативным клиентом, работающим с той же базой. Когда записей много,
счётчик двигают не по одной — см. 3.1.
Вспомогательные методы:
SqlLastInsertId() — последний идентификатор, выданный
самой СУБД. С аргументом — SqlLastInsertId($id) — наоборот,
задаёт его, как LAST_INSERT_ID(expr) в MySQL: следующий
вызов без аргумента вернёт это значение.SqlAffectedRows() — сколько строк затронул последний
запрос.Импорт, пересчёт, перенос — работа, где записей не одна, а тысяча.
Цикл из SqlGenId и SqlInsert обходится на ней
в три запроса на каждую строку; блочные методы делают то же самое за
считанные запросы.
SqlGenIdBlock($mTableName, $mCount)
захватывает диапазон идентификаторов и возвращает их готовым
массивом:
$ids = MELBIS()->SqlGenIdBlock('store_param', count($values));Счётчик сдвигается одним запросом независимо от размера блока, так
что тысяча идентификаторов стоит столько же, сколько один. Пустой запрос
($mCount меньше единицы) возвращает пустой массив и в базу
не идёт — проверять размер набора перед вызовом не нужно. Имя таблицы,
как и у SqlGenId, пишется без префикса; если такой строки в
{DBNICK}_generator нет, разбор останавливается с
ошибкой.
SqlInsertBlock($mLine, $mTableName, $mRows)
вставляет массив записей одним запросом:
$rows = [];
foreach ( $values as $param_id => $value )
{
$rows[] = [
'id' => array_shift($ids),
'store_id' => $store_id,
'param_id' => $param_id,
'value_dec' => $value
];
}
$count = MELBIS()->SqlInsertBlock(__LINE__, '{DBNICK}_store_param', $rows);Что нужно знать про набор строк:
Длинный набор движок режет на части сам — в подготовленный запрос помещается ограниченное число значений, а в сетевой пакет ограниченный объём. Части уходят отдельными запросами, поэтому «всё или ничего» блок сам по себе не обещает: если запись должна быть неделимой, оберните вызов в транзакцию (см. 5) или возьмите таблицы блокировкой (см. 6).
Если запись идёт не через
SqlInsert/SqlUpdate/SqlDelete, а
собственным запросом через SqlQuery, движок не знает, какие
таблицы изменились — и кеш модулей, читающих эти таблицы, останется
устаревшим. Пометить таблицы нужно самостоятельно:
$command = "UPDATE {DBNICK}_store
SET rating = rating + 1
WHERE id = :ID
";
MELBIS()->SqlQuery(__LINE__, $command, ['id' => $id]);
MELBIS()->SqlTableChange(__LINE__, '{DBNICK}_store');SqlTableChange($mLine, $mTables, $mCheckAffect = true)
принимает имя таблицы или массив имён. По умолчанию метка ставится,
только если последний запрос действительно что-то изменил, — если строк
не затронуто, кеш сбрасывать незачем. Передайте третьим аргументом
false, чтобы пометить таблицы безусловно.
Возвращает метку времени или false, если изменений не
было.
Это, пожалуй, самый лёгкий для пропуска метод во всём API: забытый вызов ничего не ломает сразу — витрина просто продолжает показывать старые данные, пока кеш не истечёт по времени.
MELBIS()->SqlBegin(__LINE__);
try
{
MELBIS()->SqlInsert(__LINE__, '{DBNICK}_orders', $order);
MELBIS()->SqlInsert(__LINE__, '{DBNICK}_order_goods', $goods);
MELBIS()->SqlCommit(__LINE__);
}
catch ( \Throwable $e )
{
MELBIS()->SqlRollback(__LINE__);
}SqlBegin($mLine) — начать транзакцию. Вложенные
транзакции не поддерживаются: попытка начать вторую при активной первой
останавливает разбор с ошибкой.SqlCommit($mLine) — зафиксировать.SqlRollback($mLine) — откатить.SqlTransactionActive() — идёт ли транзакция прямо
сейчас. Полезно в библиотечной функции, которая не знает, вызвали её
внутри транзакции или нет.Учтите, что таблицы должны быть на движке, поддерживающем транзакции (InnoDB), — на MyISAM вызовы отработают без ошибок, но отката не произойдёт.
SqlTableLock и SqlTableUnlock — это
не LOCK TABLES уровня СУБД, а
кооперативная пометка «занято», которую видят программа и все модули.
Она нужна для согласования с нативным клиентом: пока менеджер выполняет
пакетную операцию над товарами, фоновая задача сервера не должна лезть в
те же таблицы.
if ( !MELBIS()->SqlTableLock(__LINE__, ['{DBNICK}_store', '{DBNICK}_store_param']) )
{
// Таблицы уже заняты другой операцией — выходим
return '';
}
// ... длительная пакетная работа ...
MELBIS()->SqlTableUnlock(__LINE__, ['{DBNICK}_store', '{DBNICK}_store_param']);SqlTableLock возвращает false, если взять
таблицы так и не удалось; в этом случае ничего не блокируется и работу
надо отложить. Блокировка договорная: она ничего не
запрещает физически, а работает лишь постольку, поскольку все участники
её проверяют. Забыть SqlTableUnlock нельзя — таблицы
останутся помеченными занятыми до ручной очистки.
Занятые таблицы метод ждёт сам. Полная сигнатура —
SqlTableLock($mLine, $mTables, $mUserId = 0, $mTries = false, $mPause = false):
число попыток и пауза между ними в миллисекундах; false
значит 10 попыток с паузой 500 мс. С умолчаниями false
приходит после десяти попыток за пять секунд — короткую чужую работу
метод пересиживает, не донося отказ до человека. Кому нужен прежний
мгновенный ответ — передаёт одну попытку:
$taken = MELBIS()->SqlTableLock(__LINE__, '{DBNICK}_store', $user_id, 1);У обоих методов есть третий аргумент — $mUserId,
сотрудник, для кого взята таблица. Он виден в списке
блокировок программы, и по нему же блокировка снимается.
$taken = MELBIS()->SqlTableLock(__LINE__, '{DBNICK}_store', $user_id);
// ... работа ...
$gone = MELBIS()->SqlTableUnlock(__LINE__, '{DBNICK}_store', $user_id);По умолчанию там ноль — «держит модуль витрины», как было всегда, так что написанный раньше код работать не перестал. Называть сотрудника стоит там, где работа идёт от его имени: застрявшая блокировка тогда не безымянная, и видно, чей запрос её оставил.
Занятость от владельца не зависит. Занятую таблицу
не возьмёт никто, включая того, кто её держит: повторный
SqlTableLock с тем же $mUserId тоже ответит
false. Владелец решает не «кому можно взять», а «кто может
снять».
SqlTableUnlock отвечает числом снятых
строк. Ноль означает, что снимать было нечего — чаще всего
потому, что брали от одного имени, а снимают от другого, и блокировка
осталась висеть:
if ( MELBIS()->SqlTableUnlock(__LINE__, '{DBNICK}_store', $user_id) === 0 )
{
// Взяли не тем именем: таблица так и стоит занятой
}SqlTableOptimize($mLine, $mTables = false) выполняет
OPTIMIZE TABLE и ANALYZE TABLE. Без второго
аргумента метод сам обходит все таблицы базы, у которых есть незанятое
место (Data_free > 0) и движок MyISAM или InnoDB:
$result = MELBIS()->SqlTableOptimize(__LINE__);Операция тяжёлая и блокирующая — её место в ночном cron-модуле, а не в коде витрины. Возвращает отчёт СУБД по каждой обработанной таблице.