На счёт БД на первый взгляд может показаться, что функциональность из коробки весьма скудная. Отчасти это правда, но компенсируется тем, арсенал очень хорошо продуман, решает поставленные задачи и ориентирован на производительность.


А если вам нужны более функциональные инструменты — их всегда можно до установить по вкусу, это гораздо проще чем выпилить сложного медлительного монстра.


Данная статья покажет основные интерфейсы, а трейты 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" FROM "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" FROM "table_name" WHERE `number` > ? AND `age` < ? LIMIT ?
-- после
SELECT "id" FROM "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 репозиторий
» Документация по фреймфорку

Поделиться с друзьями
-->

Комментарии (28)


  1. Miraage
    22.08.2016 10:49
    +2

    я никогда не пойму зачем люди пишут такое

    Для того, чтобы абстрагироваться от конкретной БД, и QueryBuilder сам соберет запрос.

    $write_connection->q(
        [
            'DELETE FROM `items` WHERE `id` = ?'
            'DELETE FROM `items_tags` WHERE `item` = ?'
        ],
        $item_id
    );
    

    На мой взгляд, не очень хорошо делать такой байндинг, ибо плейсхолдера два, а в байндинг — одна переменная. Слегка путает.
    Более того, нет смысла экономить на строчках кода, заворачивая пачку запросов в 1 метод. Единственный плюс от этого — уменьшается количество инструкций function-call, однако еще неизвестно, что под капотом этих Ваших методов.


    1. nazarpc
      22.08.2016 11:00
      -3

      Дело в том, что абстрагирования там всего ничего, большинство запросов на 100% повторяют синтаксис SQL запросов. Как по мне, так проще сделать несколько простеньких конвертаций как в примерах, и дальше писать SQL.


      На самом деле бывает больше двух запросов, тогда и больше строчек моэно сэкономить, и читать визуально проще. Пример 3х запросов.


      Под капотом всё очень прямолинейно, но потенциально есть возможность объединять запросы и делать из нескольких запросов один запрос в БД, просто с этим есть некоторые сложности (mysqli_multi_query() ведет себя не совсем так, как хотелось бы, PostgreSQL не желает делать множественные запросы с серверными подготовленными выражениями, SQLite, по-моему, вообще множественные запросы не поддерживает).


  1. zenn
    22.08.2016 11:41
    +4

    По вашим публикациям, пожалуй, можно собрать самый яркий список применения анти-паттернов.

    Query builder так же слишком далёк, к примеру, я никогда не пойму зачем люди пишут такое (Laravel 5.2):

    И жаль что не поймете. Как минимум, QueryBuilder в паре с ActiveRecord (laravel5, yii2) позволяют куда удобней работать с БД чем писать запросы вручную. Кроме того, ООП-синтаксис запросов (builder/ar) куда приятней plain-text'a в sql, а при адекватно настроенной IDE позволяет существенно упростить их написание (при помощи autocomplete). Посмотрите так же на длину и лаконичность запроса через builder/ar либо классический sql. Выше вам так же намекнули на гибкость билдера для разных типов серверов СубДБ.
    Ваша реализация с передачей запросов в ->q() [query], ->f() [fetch] имеет сомнительную полезность по сравнению с классическом pdo_prepared, а следующий код поднимает волосы на заднице на голове дыбом:
    ->qf() === ->f(->q(...))
    ->qfa() === ->f(->q(...), false, true)
    ->qfs() === ->f(->q(...), true)
    ->qfas() === ->f(->q(...), true, true)

    В querybuilder'e вы не увидете таких извращений, а запросы select->fetch далаются в 1 строку и вполне читаемы. Такой конструкцией вы усложняете разработку под вашу cms другим людям, ведь это вовсе не классический вид pdo_prepared запросов и не querybuilder (стороннему разработчику придется тратить время и нервы чтобы приноровиться к такому стилю).


    1. nazarpc
      22.08.2016 11:58
      -2

      Как минимум, QueryBuilder в паре с ActiveRecord (laravel5, yii2) позволяют куда удобней работать с БД чем писать запросы вручную.

      Для этого есть cs\CRUD и cs\CRUD_helpers, которые позволяют вообще обойтись и без SQL и без конструирования запросов. Всё сводится к декларативному описанию структуры таблиц и вызовом одного метода.
      Пример в системном классе: структура, чтение, обновление данных.


      Кроме того, ООП-синтаксис запросов (builder/ar) куда приятней plain-text'a в sql, а при адекватно настроенной IDE позволяет существенно упростить их написание (при помощи autocomplete). Посмотрите так же на длину и лаконичность запроса через builder/ar либо классический sql.

      В том то и дело, что в примерах Query builder не короче, а из автодополнения вы получите только методы. В то время как в SQL вы получите красивую подсветку синтаксиса, автодополнение SQL команд, в том числе тех, о которых Query builder не знает, а так же полей из реальных таблиц. Query builder ну никак здесь не выигрывает у SQL, даже близко. Это не учитывая того, что SQL может читать любой, а в синтаксисе конкретного Query builder-а и его ограничениях нужно ещё разбираться.


      В querybuilder'e вы не увидете таких извращений, а запросы select->fetch далаются в 1 строку и вполне читаемы.

      А здесь, простите, сколько строчек? Чем вам не читаем любезно раскрашенный IDE SQL?


      Такой конструкцией вы усложняете разработку под вашу cms другим людям, ведь это вовсе не классический вид pdo_prepared запросов и не querybuilder (стороннему разработчику придется тратить время и нервы чтобы приноровиться к такому стилю).

      Хорошо, давайте сравним. Для примера беру сниппет из документации:


      $sth = $dbh->prepare('SELECT name, colour, calories
          FROM fruit
          WHERE calories < ? AND colour = ?');
      $sth->execute(array(150, 'red'));
      $red = $sth->fetchAll();
      $sth->execute(array(175, 'yellow'));
      $yellow = $sth->fetchAll();

      А теперь CleverStyle Framework (полная аналогия):


      $query = 'SELECT name, colour, calories
          FROM fruit
          WHERE calories < ? AND colour = ?';
      $red = $dbh->qfa($query, array(150, 'red'));
      $yellow = $dbh->qfa($query, array(175, 'yellow'));

      Вы правда утверждаете, что на это нужно много нервов?


      1. zenn
        22.08.2016 12:43
        +1

        А вот вам сниппет на laravel active record(или замените класс Fruit на DB::table('fruit') если модель не описана):

        $red = Fruit::select('name', 'colour', 'calories')->where('calories', '<', 150)->where('colour', 'red')->get();
        $yellow = Fruit::select('name', 'colour', 'calories')->where('calories', '<', 175)->where('colour', 'yellow')->get();
        

        как по мне — куда удобней и приятней, чем оба варианта выше.
        В том то и дело, что в примерах Query builder не короче, а из автодополнения вы получите только методы

        Не короче? Ну посчитайте, хоть через strlen, однозначно короче. Если вы пойдете дальше и будете использовать ActiveRecord с описанием моделей, в том числе phpdoc «property type $column» у вас будет просто замечательный автокомплит.


        1. nazarpc
          22.08.2016 13:01
          -2

          Я просто выставлю оба примера рядом чтобы было наглядно видно что короче, а что нет. Хотя я бы сказал, что оно соразмерно практически 1 в 1, поскольку, повторюсь, в подобных ситуациях query builder прямо дублирует SQL синтаксис.


          $red = Fruit::select('name', 'colour', 'calories')->where('calories', '<', 150)->where('colour', 'red')->get();
          $red = $dbh->qfa('SELECT name, colour, calories FROM fruit WHERE calories < ? AND colour = ?', 150, 'red');

          Если вы пойдете дальше и будете использовать ActiveRecord с описанием моделей, в том числе phpdoc «property type $column» у вас будет просто замечательный автокомплит.

          Согласен, но мы же говорим пока (имеется ввиду содержимое поста) о произвольных запросах. ActiveRecord слегка уровнем абстракции выше. Его вы, в конце концов, и здесь можете подключить при желании.


      1. franzose
        23.08.2016 07:44

        А если у вас будет запрос, который необходимо собирать по-разному в зависимости от нескольких условий? Привет страшненькая конкатенация.


        1. nazarpc
          23.08.2016 08:28

          Всё верно. Фреймворк предоставляет низкоуровневые интерфейсы, которые, тем не менее, достаточно функциональны из коробки (согласитесь, не так часто запрос собирается динамически), и при этом является высокопроизводительным.
          На базе этих примитивов можно делать что угодно, к примеру, ниже в комментариях интересный пример с Aura.SqlQuery.


          Задача в том, чтобы найти здоровый баланс между самым низким уровнем и огромным количеством абстракций, когда уже не понятно что на самом деле происходит под капотом (к примеру, были у меня несколько раз проблемы с Doctrine во время обновления ownCloud — она неправильно определяла версию СУБД и составляла более изощренные запросы, которые в конкретной версии СУБД ещё не поддерживались, приходилось миграции вручную запускать в консоли; понять что идет не так и где было весьма проблематично).


  1. Zazza
    22.08.2016 12:39

    Призываю не минусовать статью, почитать комментарии к подобным вещам бывает занятно. Если конечно, есть что тут комментировать.


  1. MetaDone
    22.08.2016 13:31

    В каждой публикации про самопальные Query builder'ы скидываю эту ссылку. Компонент от ауры я могу одной строкой подключить в свой проект, Ваш — не знаю как долго и имеет ли смысл выдирать его
    Взять хотя бы insert

    <?php
    $insert = $query_factory->newInsert();
    
    $insert->into('foo')             // insert into this table
        ->cols(array(                // insert these columns and bind these values
            'foo' => 'foo_value',
            'bar' => 'bar_value',
            'baz' => 'baz_value',
        ));
    ?>
    
    


    и Ваш вариант
    $write_connection->q(
        'INSERT INTO `table_name`
            (`id`, `value`)
        VALUES
            (?, ?),
            (?, ?),
            (?, ?)',
        [
            1, 12,
            2, 13,
            3, 14
        ]
    );
    


    1. nazarpc
      22.08.2016 13:49

      Я бы отформатировал иначе, но в целом достаточно приятный вариант, согласен.


      $write_connection->q(
          'INSERT INTO `table_name`
              (`foo`, `bar`, `baz`)
          VALUES
              (?, ?, ?)',
          ['foo_value', 'bar_value', 'baz_value']
      );

      Компонент от ауры я могу одной строкой подключить в свой проект, Ваш — не знаю как долго и имеет ли смысл выдирать его

      Здесь в зависимость модуля одна строчка в meta.json (пример), и ещё одна если ещё нет зависимости от Composer (пример).


      The query objects do not execute queries against a database. When you are done building the query, you will need to pass it to a database connection of your choice.

      Не вижу никаких проблем кроме того, что именованные подготовленные выражения не поддерживаются.


      Пример из документации:


      // a PDO connection
      $pdo = new PDO(...);
      
      // prepare the statment
      $sth = $pdo->prepare($select->getStatement());
      
      // bind the values and execute
      $sth->execute($select->getBindValues());
      
      // get the results back as an associative array
      $result = $sth->fetch(PDO::FETCH_ASSOC);

      А здесь получится следующее:


      $db = \cs\DB::instance()->module('System')->db('users');
      
      $result = $db->qf($select->getStatement(), $select->getBindValues());


      1. MetaDone
        22.08.2016 14:09

        Не вижу никаких проблем кроме того, что именованные подготовленные выражения не поддерживаются.

        Вместо
        $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
            ]
        );
        

        было бы
        $write_connection->q(
            [
                "DELETE FROM FROM `[prefix]articles` WHERE `id` = :article_id",
                "DELETE FROM FROM `[prefix]articles_comments` WHERE `article` = :article_id OR `date` < :clear_time",
                "DELETE FROM FROM `[prefix]articles_tags` WHERE `article` = :article_id"
            ],
            [
                "article_id"=>$article_to_delete,
                "clear_time"=>time() - 24 * 3600 //коряво, но так все равно лучше и понятнее
            ]
        );
        

        The query objects do not execute queries against a database. When you are done building the query, you will need to pass it to a database connection of your choice.

        Это к тому, что тут только собираются запросы, для их исполнения можно использовать https://github.com/auraphp/Aura.Sql/

        Вы привели пример получения результата запроса через PDO, можно сделать это же через Aura.Sql:
        $pdo = new ExtendedPdo(...);
        $result = $pdo->fetchAll($select->getStatement(), $select->getBindValues());
        //или же
        foreach ($pdo->yieldAll($select->getStatement(), $select->getBindValues()) as $row) {
            // ...
        }
        //и т.п.
        

        Здесь в зависимость модуля одна строчка в meta.json (пример), и ещё одна если ещё нет зависимости от Composer (пример).

        Как мне выдернуть Ваше творение в свой проект не на CleverStyle Framework?


        1. nazarpc
          22.08.2016 14:18

          Как мне выдернуть Ваше творение в свой проект не на CleverStyle Framework?

          Во-первых как вы уже упомянули есть куча автономных пакетов, а у CleverStyle Framework цель гибридное, но оптимальное ядро. Соответственно, хотя части и можно разнести по пакетах, есть сомнения, что в этом есть практический смысл.


          Практически вся упомянутая в статье логика содержится в четырех классах в core/engines/DB (один общий абстрактный класс и по одному на каждую СУБД). Из специфичного для фреймворка там только использование константы DEBUG. Всё остальное никак не привязано к фреймворку.
          Отдельно стоит класс cs\DB, который уже привязан к фреймворку. Он занимается определением того, какую конфигурацию использовать для подключения к БД и управляет соединениями (там же простая балансировка).


          1. MetaDone
            22.08.2016 14:32

            хотя части и можно разнести по пакетах, есть сомнения, что в этом есть практический смысл.

            Чтоб быстро и безболезненно установить нужную часть и использовать если она норм


  1. vshemarov
    22.08.2016 13:39
    +3

    Пример из текста

    new \yii\db\Query())
        ->select(['id', 'email'])
        ->from('user')
        ->where(['last_name' => 'Smith'])
        ->limit(10);
    

    Один из плюсов, о котором вообще не упоминается в статье, это возможность конструировать запросы налету:
    new \yii\db\Query())
        ->select($fields)
        ->from($table)
        ->where($condition)
        ->limit($limit);
    

    И эти $fields, $table, $condition, $limit — это все выражения, которые могут определяться, в зависимости от контекста и множества условий. Плюс сама конструкция запроса может так же налету меняться:
    if (!empty($search)) {
        $query->andWhere($condition2);
    }
    if (!empty($needOrder)) {
        $query->orderBy([
            'id' => SORT_ASC,
            'name' => SORT_DESC,
        ]);
    }
    

    Я понимаю, что при большом желании и прямой SQL-запрос можно так же динамически составлять, но это однозначно получится гораздо более громоздко, и читаемости кода ничуть не добавит.


    1. nazarpc
      22.08.2016 13:51

      Согласен, в таком контексте есть преимущество, спасибо за пример


    1. G-M-A-X
      25.08.2016 10:18

      > ->where($condition)

      А как сконструировать условие типа
      (a = 'a') AND (b = 'b' OR c = 'c')
      или
      (a = 'a') OR (b = 'b' AND c = 'c')
      ?


      1. Zhuravljov
        25.08.2016 13:01

        1. G-M-A-X
          25.08.2016 18:02
          -1

          О, норм, спасибо.

          Жаль, что это в документации меньше видно, нежели унылые:
          andWhere()
          и
          orWhere()

          Раньше считал QB унылым, но вот в Yii видимо не такой унылый :)


          1. franzose
            26.08.2016 04:59

            В смысле, чем больше хардкорного SQL, тем лучше?


            1. G-M-A-X
              26.08.2016 08:58
              +1

              Почему Вы сделали такой вывод? :)

              (Если там хардкорный SQL, то какой же это QB? :^) )


              1. franzose
                26.08.2016 09:02

                Жаль, что это в документации меньше видно, нежели унылые: andWhere() и orWhere()

                Может я вас неправильно понял, но почему унылые?


                1. G-M-A-X
                  26.08.2016 10:35

                  Они не позволяют сделать (a = 'a') AND (b = 'b' OR c = 'c') и более сложные. (Хардкод не учитываем)

                  Ну и составлять условия динамически.


                  1. franzose
                    26.08.2016 14:02

                    Эээ. Laravel:


                    $query->where('a', 'a')->where(function($query) {
                         $query->where('b', 'b')->orWhere('c', 'c');
                    });


                    1. G-M-A-X
                      26.08.2016 17:55

                      А динамически как это делать? :)

                      Способ с массивами в where в Yii (по ссылке выше) самый нормальный.


  1. G-M-A-X
    25.08.2016 10:09
    -1

    >Query builder так же слишком далёк

    +1. Во фреймворках они какие-то унылые.

    >Итак, с подходом стало ясно — будем писать SQL.

    Это перебор :)


    1. nazarpc
      25.08.2016 12:53

      Под капотом всё равно SQL. Я вот теперь тоже если что буду как @ MetaDone кидать линк на Aura.SqlQuery — очень добротно выглядит, и никаких проблем использовать с CleverStyle Framework (как и с любым другим).


      1. G-M-A-X
        25.08.2016 17:54
        -1

        > Под капотом всё равно SQL.

        Ну это понятно :)

        > Aura.SqlQuery — очень добротно выглядит, и никаких проблем использовать с CleverStyle Framework (как и с любым другим).

        Я бы не сказал, что ее Query builder чем-то отличается от упомянутых Вами в статье. :)