Робота з БД в CleverStyle Framework

На рахунок БД на перший погляд може здатися, що функціональність з коробки досить убога. Частково це правда, але компенсується тим, арсенал дуже добре продуманий, вирішує поставлені завдання і орієнтований на продуктивність.
А якщо вам потрібні більш функціональні інструменти — їх завжди можна встановити за смаком, це набагато простіше ніж випиляти складного вайлуватого монстра.
Ця стаття покаже основні інтерфейси, а трейты
cs\CRUD
та
cs\CRUD_helpers
залишаться на інший раз.

Без ORM та Query builder-а

ORM не вписується в ідеологію фреймворку (пакет
doctrine/orm
, приміром, без будь-яких залежностей, навіть без урахування
doctrine/*
пакетів в півтора рази більше всього фреймворку).
Query builder так само дуже далекий, приміром, я ніколи не зрозумію навіщо люди пишуть таке (Laravel 5.2):
DB::table('users')->where('name', 'John')->first()

Замість такого:
SELECT *
FROM `users`
WHERE `name` = 'John'
LIMIT 1

Або ось ще (Yii2):
new \yii\db\Query())
->select(['id', 'email'])
->from('user')
->where(['last_name' => 'Smith'])
->limit(10)

Знову ж таки замість:
SELECT `id`, `email`
FROM `user`
WHERE `last_name` = 'Smith'
LIMIT 10

Читабельність (ІМХО) гірше, немає підсвічування синтаксису, перевірка синтаксису, статичного аналізу та доповнення команд і полів (при сконфігурованої IDE), а при ускладненні запитів все одно простіше буде написати чистий SQL ніж розбиратися в тонкощах роботи Query builder-а.
Отже, з підходом стало ясно — будемо писати SQL.

SQL буває різний

Фреймворк на момент написання статті (версія 5.32.x) підтримує 3 движка баз даних: MySQL, SQLite, PostgreSQL.
Проблема тут в тому, що синтаксис, підтримуваний цими СУБД не перетинається на 100% навіть в деяких досить часто використовуються речах.
Фреймворк тут допомагає тим, що прозоро конвертує частину діалекту MySQL таким чином, щоб він підходив для SQLite і PostgreSQL.
Далі приклади здебільшого дублюють документацию.
SQLite
Тут тільки одна зовсім невелика несумісність:
-- до
INSERT IGNORE INTO `table_name`
(
`text`
) VALUES (
?
)
-- після
INSERT OR IGNORE INTO `table_name`
(
`text`
) VALUES (
?
)

PostgreSQL
Тут все складніше, але тим не менш все одно досить прості перетворення.
По-перше це лапки:
-- до
SELECT `id` FROM `table_name`
-- після
SELECT "id" З "table_name"

Далі знову
INSERT IGNORE INTO
, для PostgreSQL перетворюється в
INSERT INTO ... ON CONFLICT DO NOTHING
(і тому фреймворк вимагає для роботи PostgreSQL 9.5+):
-- до
INSERT IGNORE INTO "table_name"
(
"text"
) VALUES (
?
)
-- після
INSERT INTO "table_name"
(
"text"
) VALUES (
?
)
ON CONFLICT DO NOTHING

Ще одна схожа команда
REPLACE INTO
, вона переписується в істотно більш довгу
INSERT INTO ... ON CONFLICT ON CONSTRAINT "{table_name}_primary" DO UPDATE SET ...
:
-- до
REPLACE INTO "table_name"
(
"id",
"item",
"value"
) VALUES (
?,
?,
?
)
-- після
INSERT INTO "table_name"
(
"id",
"item",
"value"
) VALUES (
?,
?,
?
)
ON CONFLICT ON CONSTRAINT "table_name_primary" DO UPDATE SET
"id" = EXCLUDED."id",
"item" = EXCLUDED."item",
"value" = EXCLUDED."value"

Важливо зауважити, що в цьому випадку фреймворк очікує що для таблиці
constraint
(не знаю як краще перевести) з ім'ям таблиці і суфіксом
_primary
, наприклад, для системної таблиці
[prefix]users
він виглядає наступним чином:
ALTER TABLE ONLY "[prefix]users" ADD CONSTRAINT "[prefix]users_primary" PRIMARY KEY ("id");

Останній нюанс пов'язаний з тим, у якому форматі PostgreSQL бажає отримувати серверні підготовлені вираження, так що завжди можна використовувати
?
:
-- до
SELECT "id" З "table_name" WHERE `number` > ? AND `age` < ? LIMIT ?
-- після
SELECT "id" З "table_name" WHERE `number` > $1 AND `age` < $2 LIMIT $3

Трохи про БД в загальному

Фреймворк спочатку має поняття про те, що БД може бути кілька. Кожна БД може використовувати різні движки, чи однакові движки, але з різними конфігураціями. Так само підтримуються дзеркала БД і просте розподіл запитів в конфігураціях master-master і master-slave.
Модулі, які використовують БД, вказують у своєму
meta.json
2 ключа, які відносяться до БД (приклад з системного модуля):
{
...
"db" : [
"keys",
"texts",
"users"
],
"db_support" : [
"MySQLi",
"PostgreSQL",
"SQLite"
],
...}

db_support
вказується, з якими движками модуль в принципі може працювати,
db
зазначаються назви баз даних, які будуть під час установки асоційовані з якою-небудь з існуючих БД.
Різні назви використовуються для того, щоб мати можливість вибрати найбільш оптимальну БД під завдання. Само собою, таблиці повинні бути розподілені таким чином, щоб не робити
JOIN
між різними БД.
Пізніше, коли потрібно отримати id бази даних, асоційованої з назвою можна таким чином:
$db_id = \cs\Config::instance()->module('System')->db('users');

Далі ідентифікатор використовується для отримання об'єкта з підключенням до потрібної БД:
$write_connection = \cs\DB::instance()->db_prime($db_id);
$read_connection = \cs\DB::instance()->db($db_id);

Вже тут розробник явно вказує, чи буде він щось писати в БД, чи ні. Від цього залежить вибір дзеркала при відповідній конфігурації.

DBAL

Тут все просто, як тільки ви вловите принцип — ви зможете дуже продуктивно писати запити з закритими очима.
Просте виконання запиту
Просте виконання запиту:
$result = $read_connection->q('SELECT `id` FROM `table_name`);

q
це скорочення від
query
. У методу є кілька варіантів синтаксису:
->q($query_string : string)
->q($query_string : string, ...$parameters : array)
->q($query_string : string, $parameters : array)
->q($query_string : string[])
->q($query_string : string[], ...$parameters : array)
->q($query_string : string[], $parameters : array)

Самі запити можуть використовувати як серверні підготовлені вирази:
$write_connection->q(
[
'DELETE FROM `items` WHERE `id` = ?'
'DELETE FROM `items_tags` WHERE `item` = ?'
],
$item_id
);

Так і клієнтське форматування у вигляді синтаксису функції
sprintf()
:
$write_connection->q(
[
'DELETE FROM `items` WHERE `id` = %d'
"DELETE FROM `items_tags` WHERE `item` = '%s'"
],
$item_id
);

В останньому прикладі перед підстановкою дані будуть оброблені відповідним чином, так що в
'%s'
SQL-ін'єкції не буде.
Для серверних підготовлених виразів дозволяється використовувати не всі аргументи (на відміну від прямого використання нативних інтерфейсів):
$write_connection->q(
[
"DELETE FROM FROM `[prefix]articles` WHERE `id` = ?",
"DELETE FROM FROM `[prefix]articles_comments` WHERE `article` = ? OR `date` < ?",
"DELETE FROM FROM `[prefix]articles_tags` WHERE `article` = ?"
],
[
$article_to_delete,
time() - 24 * 3600
]
);

Вибірка даних
Другий корисний метод призначений для безпосереднього одержання даних:
$read_connection->f($result);

f
це скорочення від
fetch
. Метод так само є кілька необов'язкових параметрів:
->f($query_result, $single_column = false : bool, $array = false : bool, $indexed = false : bool)

$single_column === true
замість масиву з колонками поверне скалярний значення першої колонки:
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` = 1')
); // ['id' => 1]
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` = 1'),
true
); // 1

$array === true
замість одного рядка вважає все і поверне результат у вигляді масиву:
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` < 3'),
false,
true
); // [['id' => 1], ['id' => 2]]
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` = 1'),
true,
true
); // [1, 2]

$indexed === true
повертає індексований масив замість асоціативного:
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` < 3'),
false,
false,
true
); // [1]
$read_connection->f(
$read_connection->q('SELECT `id` FROM `table_name` WHERE `id` = 1'),
false,
true,
true
); // [[1], [2]]

А тепер цікаві скорочення:
->qf() === ->f(->q(...))
->qfa() === ->f(->q(...), false, true)
->qfs() === ->f(->q(...), true)
->qfas() === ->f(->q(...), true, true)

a
array
, а
s
single
.
наприклад, наступні дві конструкції еквівалентні, хоча другу читати і супроводжувати сильно простіше:
$read_connection->f(
$read_connenction->q('SELECT `id` FROM `table_name` WHERE `id` = ?', 1),
true,
true
); // [1, 2]
$read_connection->qfas(
'SELECT `id` FROM `table_name` WHERE `id` = ?',
1
); // [1, 2]

Вставка даних
Ще є один іноді корисний метод для вставки даних:
$write_connection->insert(
'INSERT INTO `table_name`
(`id`, `value`)
VALUES
(?, ?)',
[
[1, 12],
[2, 13],
[3, 14]
]
);

Синтаксис наступний:
->insert($query : string, $parameters : array|array[], $join = true : bool)

Якщо
$join === true
, то приклад вище буде перед виконанням переписаний як:
$write_connection->q(
'INSERT INTO `table_name`
(`id`, `value`)
VALUES
(?, ?),
(?, ?),
(?, ?)',
[
1, 12,
2, 13,
3, 14
]
);

Інакше рядки будуть вставлятися по одній.
Інші методи
Є ще ряд корисних методів, наприклад,
->id()
повертає ідентифікатор останньої вставленою рядка
->transaction()
дозволяє обернути виконання операцій в транзакцію:
$write_connection->transaction(function ($c) { // `$c` це те ж, що і `$write_connection`
$c->insert(...);
// Вкладені транзакції насправді пустушки, все виконується в рамках батьківського транзакції
$c->transaction(function ($c) {
$c->id();
});
// Якщо кинути виключення або повернути `false` то буде виконаний відкат транзакції, виняток буде проброшено далі
});

Є методи для отримання списку таблиць і стовпців у таблиці, які працюють однаково для всіх підтримуваних БД і деякі інші допоміжні речі.
У цілому, за подробицями звертайтеся до документации.

На цьому введення в базову роботу з БД

У багатьох модулях замість прямих запитів використовуються зручні трейты
cs\CRUD
та
cs\CRUD_helpers
.
Перший окрім безпосередньо 4 банальних операцій з БД під капотом ще вміє займатися многоязычностью, нормалізацією і деякою обробкою даних (наприклад, JSON туди і назад конвертувати при запису і читання), обробкою завантажуваних файлів, а так само підтримує зв'язані таблиці (один до одного, один до багатьох), теж з підтримкою всього цього добра.
Другий же трейт має метод для пошуку (насправді це фільтр) елементів, знову ж таки враховуючи багатомовність деяких полів/таблиць і так само включає підтримку пов'язаних таблиць.
Якщо додати опис обох трейтов в статтю, то вона буде занадто велика на один раз, тому буде наступного разу.
Думки з приводу зручності інтерфейсів і приклади більш зручних (на вашу думку) альтернатив вітаються, буду радий обговорити ці моменти в конструктивному ключі і врахувати зворотний зв'язок в майбутніх релізах.
» GitHub репозиторій
» Документація по фреймфорку
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.