Funq

Версия от 16:40, 23 августа 2009; VitaliyFilippov (обсуждение | вклад)

Funq — концепция околофункционального, гибкого язык запросов к реляционным базам данных, чем-то похожего на LINQ и призванного заменить SQL для разработчиков, к тому же, скрывая диалекты, свойственные отдельным СУБД.

Do you want to try some new features? By joining the beta, you will get access to experimental features, at the risk of encountering bugs and issues.

Ок Нет, спасибо

Идея

Сейчас стандартом языка запросов к реляционным базам данных является SQL — громоздкий, некрасивый и тяжёлый язык. SQL — декларативный язык, но он не оперирует сущностями каких-то явных типов, а синтаксис команд очень жёсткий, то есть, негибкий. Например, чтобы автоматически наложить на уже созданный запрос какое-то ограничение, нужно сначала понять, в какое место и в каком виде его добавлять, а способа адекватно и надёжно сделать это без синтаксического анализа запроса не существует.

Таким образом, было бы неплохо создать не SQL-подобный язык запросов, который был бы к тому же элегантен и удобен для использования в клиентских приложениях. Например, можно максимально приблизить формирование запросов к заданию выражения над таблицами, как алгебраическими объектами, с использованием различных операций реляционного исчисления — JOIN-ов, ограничений, проекций и т.д, а также других нереляционных преобразований — они необходимы потому, что возможностей построения запросов разработчики различных СУБД создали великое множество, а на замену SQL годится по меньшей мере равномощный ему язык. Сразу оговоримся, что основные проблемы написания запросов к базам данных связаны с запросами выборки. Запросы удаления/обновления часто используют базу просто как хеш-таблицу, а запросы вставки и DDL-запросы просто имеют достаточно бедный синтаксис. Их также нужно реализовать, как часть языка запросов, но основной интерес представляют запросы выборки. Ими-то мы в первую очередь и займёмся.

Например, рассмотрим вот такой SQL-запрос. Можно заметить, что это не чистый SQL — здесь ещё присутствуют подстановки имён таблиц из хеша $t и имён полей $child и $parent.

SELECT t0.*, (t3.`$parent` IS NULL) AS `_direct`
FROM `$t->{album2album}` AS t1
INNER JOIN `$t->{albums}` AS t0 ON t0.`id`=t1.`$child` AND $t0_where
LEFT JOIN (`$t->{album2album}` AS t3
INNER JOIN `$t->{album2album}` AS t4)
ON t3.`pid`=? AND t4.`pid`=? AND t3.`$parent`=t1.`$parent`
AND t3.`$child`=t4.`$parent` AND t4.`$child`=t1.`$child`
WHERE t1.`$parent`=? AND t1.`pid`=?
GROUP BY t0.`id`
HAVING `_direct`=1
ORDER BY t0.`ord` DESC, t0.`name`
LIMIT ?, ?

А теперь попробуем представить, как этот запрос должен был бы выглядеть на языке Funq:

(album2album as t1)
.where (t1.?parent=? and t1.pid=?pid)
.join ((albums as t0).where(t0.id=t1.?child and t0.?t0where))
.join[left] ((album2album as t3).where(t3.?parent=t1.?parent and t3.pid=?pid))
.join ((album2album as 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]
.group (t0.id)
.where (t1._direct=1)
.order (t0.ord desc, t0.name)
.limit (?offset, ?limit)

Заметно две основных идеи: во-первых, логически после применения каждого преобразования .something() к отношению мы снова получаем отношение, имеющее все те же самые методы; и во-вторых, язык должен уметь интерполировать различные конструкции в различные места запросов.

Вообще-то, внезапно можно осознать, что relation.something().something().something() — функциональный стиль программирования, а точнее, что это очень похоже на Fluent Interfaces. Собственно, они и сами являются пародией на ФП, хотя и довольно жалкой.

Кроме того, можно вспомнить язык LINQ (Language INtegrated Query) от Microsoft. Если в LINQ пользоваться только объектным стилем задания запроса, то LINQ тоже станет похож на Funq. Другое дело, что на практике делать именно так в LINQ практически невозможно и в любом случае неудобно. Важно также, что и цель у создателей LINQ изначально была другая — встроить язык запросов в .NET с помощью обычных его средств и научить языки .NET работать через одни и те же классы и интерфейсы с базами данных, XML-файлами, да и вообще любыми источниками данных, вплоть до обычных массивов.

А ещё, раз уж мы вспомнили про LINQ, можно сразу заметить, что описанная идея позволяет легко реализовать «язык запросов» на объектном интерфейсе в родном синтаксисе многих языков программирования. Конкретно автора (меня — VitaliyFilippov 13:00, 13 июля 2009 (UTC)) интересует язык Perl, он-то и будет рассмотрен в данной статье.

Нужно отметить, что формирование запроса в родном синтаксисе языка имеет и недостаток — затруднение кэширования скомпилированных запросов. Из-за этого может пострадать производительность; однако, если объекты запросов кэшируются самим приложением, этого не произойдёт. Этот недостаток говорит нам о том, что имеет смысл реализовать одновременно и объектный подход к формированию запроса, и просто трансляцию из Funq-кода.

Другие нужные фичи

  • Автоматическая трансляция имён таблиц, и вообще запросов к таблицам. Это значит, что всегда можно переопределить
    $dbh->query("some_table")
    так, что на самом деле этот конструктор будет возвращать не новый, девственно чистый, объект запроса к таблице some_table, а некоторый другой объект запроса — с заданными дополнительными ограничениями, к другой таблице, или вообще результат сложного запроса (хотя так делать, наверное, не надо). То есть, по сути, «программные представления».
  • Именованные параметры (placeholder’ы) — задаются как ?name — и автоматическое определение имени из контекста в простых выражениях. Например, параметр из условия config=? будет, очевидно, называться config, если только уже не задан другой параметр с таким именем. Кроме того, один и тот же явно именованный параметр можно использовать в различных местах выражения и даже в разных операторах в процессе формирования запроса.
  • Персонализация условий заданием имени таблицы (или имён таблиц) — к примеру, если в запросе записано условие exif.?placeholder, а значение placeholder-а равняется .file=? and .config=?, то в запрос будет подставлено преобразованное условие exif.file=? and exif.config=?. Если аналогично требуется сделать для нескольких таблиц (ситуация редкая), синтаксис усложняется — теперь условие в запросе, например, может звучать как [exif, configs].?placeholder, а в placeholder-е, например, как _1.file=? and _1.config=_2.id. При этом _1 будет заменено на имя первой таблицы, _2 — на имя второй, и так далее.
  • Наличие хинта (подсказки) для последовательного выполнения подзапросов, то есть выполнения сначала внутреннего запроса, а потом внешнего.
  • Автоматическое кэширование компилируемых из Funq-кода (то есть **не** формируемых объектно) запросов. Кэширование детализированных условиями и порядками сортировки/группировки запросов. Следует отметить, что если в процессе выполнения появляется слишком много (неограниченное относительно вариации входных данных количество) детализированных условиями и порядками запросов, значит, что-то //«Defective By Design»//, а точнее, кто-то забыл про placeholder’ы…
  • Автоматическая проверка соединения, переподключение к БД и пересоздание кэшированных SQL-объектов запроса в случае необходимости.
  • Поддержка разделения соединённых отношений на отдельные хеши в результатах запроса.
  • Поддержка различных диалектов языка SQL.
  • Поддержка возможности тихо игнорировать заданные при вставках/обновлениях несуществующие поля.

Статус реализации

На данный момент из запланированных идей реализованы (обзор исходных кодов):

  • Собственно работающий компилятор запросов ;) начал с MySQL. Компилятор с полной поддержкой синтаксиса SELECT, с корректной оптимизацией / добавлением подзапросов, разрешением спорных ситуаций по семантике, поддержкой хинтов типа CALC_FOUND_ROWS, SQL_CACHE и т. п., заменой имён таблиц и прочими;
  • Связывание именованных параметров (?name) со скалярами, с массивами или массивами массивов значений, с выражениями или подзапросами;
  • Корректное кэширование всех артефактов трансляции, чтобы не мучаться этим постоянно;
  • «Аппликация» имён таблиц к выражениям (замена `_` на имя нужной таблицы при подстановке выражения вместо `table`.?expression;
  • Автоматическое контекстное именование параметров, если имя не указано;
  • Возможность разделения каждой строки на несколько хешей, чтобы выбирать отдельные строки одновременно из нескольких таблиц;
  • Удобные функции получения результатов запросов (в основном аналогично DBI): успешность выполнения / массив хешей / массив разделённых хешей / массив массивов / хеш хешей / одна строка / одно значение. Или ручками — тоже можно.
  • Разделение функционала на «ядро» и «драйверы»;
  • Выбран способ передачи доп. информации типа SELECT FOUND_ROWS();
  • Поддержка запросов вставки/обновления/удаления.

Чего пока нет:

  • Автоматической генерации алиасов например для подзапросов, если они не задаются явно;
  • Поддержки подстановки хешей в текст запроса (например в виде field=value AND field=value);
  • Возможности программного последовательного выполнения подзапросов — сначала внутреннего, а потом внешнего, с программной передачей результата первого во второй (в MySQL бывают ситуации когда это ускоряет запрос В РАЗЫ);
  • Нет ORM’а, и даже не обдуман. А возможно, стоит. А возможно, и не стоит.

Примеры

Объектное формирование запросов

Вот, например, запрос из начала статьи на Perl:

$query = $dbh
->query(album2album => "t1")->where("t1.parent=? and t1.pid=?pid")
->join($dbh->query(albums => "t0")->where("t0.id=t1.child and t0.?t0where"))
->leftjoin($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=?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")

Вот пример его выполнения:

$rows = $query->hasharray({
    parent  => $parent,
    pid     => $pid,
    t0where => "config=?config",
    config  => 1,
    offset  => 0,
    limit   => 20,
});

А вот он же, но с вызовами join у объекта запроса, а не у $dbh:

$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=?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")

Запрос попроще:

$dbh->query(tag2entity_cl => "t1")->where("t1.pid=? and t1.tag=?")
->join($dbh->query(entity => "t0")->where("t0.id=t1.eid and t0.config=? and t0.?t0where"))
->select("t0.*")
->order("t0.?t0order")
->limit("?offset", "?limit")

Или совсем простой:

$dbh->query("exif")->where("file=?")

А вот подзапрос:

$dbh->query(entity => "t0")
->where("t0.?t0where and ?subq in ?tagcount")
->select("t0.*, ?subq as t0.tag_count")
->curry(subq => $dbh
    ->query(tag2entity => "t1")
    ->where("t1.pid=t0.pid and t1.eid=t0.eid")
    ->count->value
)

Или даже два вложенных подзапроса:

$dbh->query(entity => "t0")
->where("t0.?t0where and ?subq in ?tagcount")
->select("t0.*, ?subq as t0.tag_count")
->curry(subq => $dbh
    ->query(tag2entity => "t1")
    ->where("t1.pid=t0.pid and t1.eid=t0.eid")
    ->where("t1.config in ?list")
    ->curry(list => $dbh->query("configs"))
    ->count->value
)