Изменения

Перейти к: навигация, поиск

Funq

2629 байтов добавлено, 12:42, 20 июня 2016
Funq — '''Funq''' — концепция околофункционального, гибкого язык языка запросов к реляционным базам данных, чемчуть-то чуть похожего на LINQ и призванного заменить SQL для разработчиковсложных запросов, оптимизируя их ещё на стадии построения, и давая возможность наложить любое преобразование на запрос в любой момент, к тому же, скрывая диалекты, свойственные отдельным СУБД.
== Идея ==
Вообще-то, внезапно можно осознать, что <code>relation.something().something().something()</code> — функциональный стиль программирования, а точнее, что это очень похоже на [http://www.martinfowler.com/bliki/FluentInterface.html Fluent Interfaces]. Собственно, они и сами являются пародией на ФП, хотя и довольно жалкой.
 
Ещё можно осознать, что по сути '''Funq''' по сути нужен только для тех СУБД, которые не могут хорошо оптимизировать бесконечно длинные цепочки подзапросов. Есть, правда, подозрение, что во многих случаях это ровно ВСЕ СУБД, но в любом случае к таким относится как минимум MySQL.
Кроме того, можно вспомнить язык [http://msdn.microsoft.com/en-us/netframework/aa904594.aspx LINQ] (Language INtegrated Query) от Microsoft. Если в LINQ пользоваться только объектным стилем задания запроса, то LINQ тоже станет похож на Funq. Другое дело, что на практике делать именно так в LINQ практически невозможно и в любом случае неудобно. Важно также, что и цель у создателей LINQ изначально была другая — встроить язык запросов в .NET с помощью обычных его средств и научить языки .NET работать через одни и те же классы и интерфейсы с базами данных, XML-файлами, да и вообще любыми источниками данных, вплоть до обычных массивов.
На данный момент из запланированных идей реализованы ([{{SVN|vitaphoto/head/lib-sway/Funq}} обзор исходных кодов]):
* Собственно работающий компилятор запросов ;) начал с MySQL. Компилятор с полной поддержкой синтаксиса SELECT, с корректной оптимизацией / добавлением подзапросов, разрешением спорных ситуаций по семантике, поддержкой хинтов типа CALC_FOUND_ROWS, SQL_CACHE и т. п., заменой имён таблиц и прочими;.* Связывание именованных параметров (?name) со скалярами, с массивами или массивами массивов значений, с выражениями или подзапросами;.* Корректное кэширование всех артефактов трансляции, чтобы не мучаться этим постоянно;.* «Аппликация» имён таблиц к выражениям (замена `_` на имя нужной таблицы при подстановке выражения вместо `table`.?expression;.* Автоматическое контекстное именование параметров, если имя не указано;.* Возможность разделения каждой строки на несколько хешей, чтобы выбирать отдельные строки одновременно из нескольких таблиц;.
* Удобные функции получения результатов запросов (в основном аналогично DBI): успешность выполнения / массив хешей / массив разделённых хешей / массив массивов / хеш хешей / одна строка / одно значение. Или ручками — тоже можно.
* Разделение функционала на «ядро» и «драйверы»;.* Выбран способ передачи Передача доп. информации типа SELECT FOUND_ROWS(); — в случае MySQL это функции соединения last_insert_id(), thread_id(), found_rows(), affected_rows().
* Поддержка запросов вставки/обновления/удаления.
* Возможность программного последовательного выполнения подзапросов — сначала внутреннего, а потом внешнего, с программной передачей результата первого во второй. В MySQL бывают ситуации, когда это ускоряет выполнение запроса ''в разы''.
Чего пока нетдаже в списке TODO:
* Автоматической генерации алиасов , например , для подзапросов, если они не задаются явно;.* Поддержки подстановки хешей в текст запроса (например , в виде field=value AND field=value);.
* Нет ORM’а, и даже не обдуман. А возможно, стоит. А возможно, и не стоит.
* Поддержки операции вставки в отношения, выраженные посредством Funq-запросов.
 
Список TODO:
 
* Поддержка автоматического удаления join’ов на «ненужные» таблицы, если ни одно из их полей не указано в запросе. Полезно, например, для какой-нибудь Багзиллы, где есть таблица bugs с огромной кучей полей, которые по-разному хранятся. Соответственно, можно будет просто определить «супертаблицу» со всеми-всеми полями и только указывать, какие мы хотим, а какие не хотим, и Funq бы само удалило лишние join’ы.
* Тесты (!)
* Source-фильтр чтобы писать $dbh->do(funq(table->where(«config=?»)->order()), { arg => value, arg => value, … }), хотя полную функциональность так не реализуешь, так как неизвестен тип подставляемых значений.
* Найти места с неочевидным синтаксисом типа передачи параметров-ссылок на скаляр в качестве выражений и т. п., устранить все такие неочевидности введением методов.
* Возможно, не делать трансляцию SELECT * в список полей, а только логически передавать во внешний запрос.
* Простые методы вставки (наподобие [{{SVN|vitaphoto/solstice/lib-sway/VMX/Common.pm|markup}} VMX::Common::insertall_hashref]) / мульти-обновления (для MySQL, INSERT … ON DUPLICATE KEY SET …).
* Все TODO и FIXME из кода.
== Синтаксис Funq ==
Рассмотрим [[Funq]] на примере его Perl-реализации: модулей [{{SVN|vitaphoto/branch/solstice/lib-sway/Funq/DBI.pm}} Funq::DBI] (объект соединения) и [{{SVN|vitaphoto/branch/solstice/lib-sway/Funq/Query.pm}} Funq::Query] (объект запроса). Под <code>$dbh</code> везде понимается объект соединения, а под $query — объект запроса.
=== Формирование запроса ===
; <code>$dbh->query($table[, $alias])</code>: Конструирует и возвращает новый объект запроса. Параметр $table может сам быть объектом (под)запроса.
; <code>$query1query->join($query2)clone</code>: Конструирует новый объект Возвращает клон объекта запроса как соединение запросов <code>$query1</code> и <code>$query2query</code>.; <code>$query1->union($query2)</code>: Конструирует объединение запросов <code>$query1</code> и <code>$query2</code>. Не путать с соединением«Такой же, только другой», не имеющий никаких ссылок на старый объект или его части.; <code>$query1->union_all($query2)</code>: Конструирует неуникальное объединение запросов <code>$query1</code> и <code>$query2</code>. Не путать с соединением.; <code>$query1->intersect($query2)</code>: Конструирует пересечение запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->intersect_all($query2)</code>: Конструирует неуникальное пересечение запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->except($query2)</code>: Конструирует разность запросов <code>$query1</code> и <code>$query2</code>. <span style="color:red">Не реализовано в MySQL</span>.; <code>$query1->except_all($query2)</code>: Конструирует неуникальную разность запросов <code>$query1</code> и <code>$query2</code>. <span style="color:red">Не реализовано в MySQL</span>.== Унарные преобразования ====; <code>$query1->join($query2)</code>: Конструирует внутреннее соединение (Inner Join) запросов <code>$query1</code> и <code>$query2</code>.
; <code>$query->where($condition[, $condition2, ...])</code>: Добавляет все переданные ограничения (в виде строк) к объекту запроса <code>$query</code>.
; <code>$query->select($field_list)</code>: Задаёт список столбцов для объекта запроса. Может включать вычислимые столбцы и placeholder’ы.
; <code>$query->update(@exp)</code>: Превращает запрос из запроса выборки в запрос обновления. Передаваемый массив <code>@exp</code> содержит части выражения, содержащего новые значения полей. При выполнении запроса непустые части соединяются через запятую. Отдельное выражение может содержать в себе placeholder’ы или, что важно, само являться одним placeholder’ом, например, «?values». Массив <code>@exp</code> может вовсе не содержать ни одного значения, в этом случае («по умолчанию») в качестве update-выражения принимается значение параметра <code>?update</code>;
; <code>$query->delete(@tables)</code>: Превращает запрос из запроса выборки в запрос удаления. Если запрос содержит несколько соединённых таблиц, то необязательно удалять подходящие строки из всех этих таблиц — с помощью параметра <code>@tables</code> можно выбрать лишь несколько из них;
; <code>$query->insert_into($table)</code>: Превращает запрос в запрос вставки выбранных строк в таблицу <code>$table</code>. Параметр <code>$table</code> обязательно должен быть именем таблицы. В зависимости от СУБД, может принимать дополнительные параметры — в случае с MySQL это может быть LOW_PRIORITY или DELAYED (см. [[http://dev.mysql.com/doc/refman/5.0/en/insert.html|Синтаксис INSERT MySQL]]);; <code>$query->replace_into($table)</code> : ([http://www.mysql.com/ MySQL]). Превращает запрос в запрос замены выбранных строк в таблицу <code>$table</code>: Аналогично insert_to, но при этом все имеющиеся в таблице записи со значениями полей, входящих в первичный или уникальный ключ, равными значениям одной из вставляемых строк, предварительно удаляются (см. [[http://dev.mysql.com/doc/refman/5.0/en/replace.html|Синтаксис REPLACE MySQL]]).; <code>$query->clone</code>: Возвращает клон объекта запроса <code>$query</code>. «Такой же, только другой», не имеющий никаких ссылок на старый объект или его части.
<span style="color:red">Внимание!</span> Методы update и delete могут быть применены к запросу только один раз, не могут сочетаться, не могут быть применены после insert_into или replace_into, и аннулируют действие на запрос методов select, group, order, distinct, count, value, включая их предыдущие применения к запросу <span style="color:#f00">(!)</#span>. Кроме того, невозможно применение методов update и delete к запросам, имеющим подзапросы в качестве derived table. При попытке любого из таких запрещённых действий генерируются предупреждения, а вызов игнорируется. ==== Бинарные преобразования ==== ; <code>$query1->union($query2)</code>: Конструирует объединение запросов <code>$query1</code> и <code>$query2</code>. Не путать с соединением.; <code>$query1->union_all($query2)</code>: Конструирует неуникальное объединение запросов <code>$query1</code> и <code>$query2</code>. Не путать с соединением.; <code>$query1->intersect($query2)</code>: Конструирует пересечение запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->intersect_all($query2)</code>: Конструирует неуникальное пересечение запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->except($query2)</code>: Конструирует разность запросов <code>$query1</code> и <code>$query2</code>. <span style="color:red">Не реализовано в MySQL</span>.; <code>$query1->except_all($query2)</code>: Конструирует неуникальную разность запросов <code>$query1</code> и <code>$query2</code>. <span style="color:red">Не реализовано в MySQL</span>.; <code>$query1->join($query2)</code>: Конструирует внутреннее соединение (Inner Join) запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->leftjoin($query2)</code>: Конструирует левое соединение (Left Join) запросов <code>$query1</code> и <code>$query2</code>.; <code>$query1->rightjoin($query2)</code>: Конструирует правое соединение (Right Join) запросов <code>$query1</code> и <code>$query2</code>.
=== Выполнение запроса ===
Следующее семейство функций предназначено для выполнения запроса. Их разница в формате возврата строк.
Если %bind не детализирует запрос полностью, то есть так, чтобы незаданных placeholder’ов не оставалось, все функции возвращают undef или пустой список, при этом <span style="color:red">'''объект запроса не изменяется</span>'''. Здесь идея следующая — если передан неполный хеш, значит, кто-то ошибся. Значит, нужно дать ему возможность исправиться и выполнить запрос ещё раз, не создавая объект запроса заново.
Для выполнения запроса нужно сначала выполнить следующую функцию запроса:
Итак, таблица правил группировки (строки — внешние операции, столбцы — внутренние операции):
<tab sep=bar style="bordertext-align:1px solid blackcenter; width: 400px; text-align: center" class="tableborder1black" head="topleft">
— | '''J''' | '''U''' | '''I''' | '''E''' | '''W''' | '''S''' | '''G''' | '''O''' | '''L''' | '''D'''
'''J''' | + | — | — | — | + | * | — | * | — | —
А такого понятия, как оптимизация подзапросов, здесь просто не существует, она здесь не нужна! Потому что программист всегда видит перед собой цепочку преобразований такую, что логически каждое следующее преобразование — это подзапрос из предыдущего. Следовательно, использование явных подзапросов не нужно вообще — если где-то нужен подзапрос, транслятор его вставит сам, а если не нужен, то зачем париться и писать лишний вызов, раз это не меняет результат?! Хотя само преобразование '''Wrap''' остаётся для внутреннего использования транслятором. А «условно совместимые» пары преобразований, то есть пары, поведение которых зависит от того, есть между ними подзапрос или нет, отслеживается с помощью переменной «предпочтения подзапроса».
Далее, существует отдельный класс «группа преобразований» ([{{SVN|vitaphoto/branch/solstice/lib-sway/Funq/mysql/Group.pm}} Funq::mysql::Group] для [http://www.mysql.org/ MySQL]), логически описывающий ОДИН SQL-запрос/подзапросу, и НЕ описывающий его подзапросы. У каждого объекта [[Funq]]-преобразования есть метод compile, который в передаваемую ему группу преобразований записывает информацию о себе. В группу преобразований попадают, во-первых, только совместимые преобразования, а во-вторых, только выражения с реальными именами таблиц и полей. То есть, трансляцией имён таблиц и полей «группа преобразований» не занимается.
На выходе транслятора получается специальное представление «полуфабриката SQL-запроса» — связный список, элементы которого могут быть:
== Примеры запросов ==
 
=== Пример 1: Сложный запрос ===
Вот, например, запрос из начала статьи на Perl:
<source lang="perl">$query = $dbhfunq
->query(album2album => "t1")->where("t1.parent=? and t1.pid=?pid")
->join($dbhfunq->query(albums => "t0")->where("t0.id=t1.child and t0.?t0where"))->leftjoin($dbhfunq->query(album2album => "t3")->where("t3.parent=t1.parent and t3.pid=?pid"))->join($dbhfunq->query(album2album => "t4")->where("t4.parent=t3.child and t4.child=t1.child and t4.pid=?pid"))->select("t0.*", "(t3.parent is null) as t1._direct")->hint({calc-found-rows => 1}'calc_found_rows')
->group("t0.id")
->where("t1._direct=1")
<source lang="perl">
$rows = $query->stmt->hasharray({
parent => $parent,
pid => $pid,
</source>
А вот он же, но с вызовами join у объекта запроса, а не у $dbh: <source lang="perl">$dbh->query(album2album => "t1")->where("t1.parent=? and t1.pid=?pid")->join($dbh->query(albums => "t0")->where("t0.id=t1.child and t0.?t0where"))->join(left => $dbh->query(album2album => "t3")->where("t3.parent=t1.parent and t3.pid=?pid"))->join($dbh->query(album2album => "t4")->where("t4.parent=t3.child and t4.child=t1.child and t4.pidПример 2: Запрос попроще =?pid"))->select("t0.*", "(t3.parent is null) as t1._direct")->hint({calc-found-rows => 1})->group("t0.id")->where("t1._direct=1")->order("t0.ord desc, t0.name")->limit("?offset", "?limit")</source>
Запрос попроще:
->limit("?offset", "?limit")
</source>
 
=== Пример 3: Очень простой ===
Или совсем простой:
$dbh->query("exif")->where("file=?")
</source>
 
=== Пример 4: Подзапрос ===
А вот подзапрос:
->where("t0.?t0where and ?subq in ?tagcount")
->select("t0.*, ?subq as t0.tag_count")
->curryset(subq => $dbh
->query(tag2entity => "t1")
->where("t1.pid=t0.pid and t1.eid=t0.eid")
)
</source>
 
=== Пример 5: Вложенный подзапрос ===
Или даже два вложенных подзапроса:
<source lang="perl">
$dbhfunq->query(entity => "t0")
->where("t0.?t0where and ?subq in ?tagcount")
->select("t0.*, ?subq as t0.tag_count")
->curryset(subq => $dbhfunq
->query(tag2entity => "t1")
->where("t1.pid=t0.pid and t1.eid=t0.eid")
->where("t1.config in ?list")
->curryset(list => $dbhfunq->query("configs"))
->count->value
)
</source>
 
=== Пример 6: Запрос из DBIx::Class::Cookbook ===
SQL-запрос:
Вроде бы не сильно проще. Но вся прелесть в том, что вместо artist можно подставить любой Funq-запрос, и получаемый запрос также можно использовать как часть другого.
[[Категория:SwayPerl]][[Категория:РазработкаАрхив]]

Навигация