Я считаю, что в Erlang до сих пор нет хорошей библиотеки для работы с SQL базами данных. Есть хорошие конструкторы, типа mekao и sqerl, есть попытки сделать ORM boss_db и texas; но все они подходят лишь для CRUD запросов.
Конструкторы интересные, особенно mekao — он с помощью эрланговских рекордов строит запросы, очень удобная штука когда много полей в таблице, но не умеет делать сложные запросы. Sqerl — это если не хочешь оборачивать запрос в двойные кавычки, а хочешь использовать код эрланга для генерации запросиков, тоже интересная библиотека, но умеет только CRUD.
ORM, как по мне, — недоразумение для функциональной парадигмы, писать ее для языка, где нет объектов — глупость.
Полтора месяца назад я наткнулся на очень интересную библиотеку для Clojure — yesql. Идея очень простая — хранить SQL запросы в файлах, с возможностью получить нужный запрос по его названию:
-- your_project/sql_queries/users.sql
-- name: user-count
-- Counts all the users.
SELECT count(*) FROM users;
-- name: user-by-id
SELECT * FROM users WHERE user_id = ?;
И дальше получать эти запросики с помощью простых функций:
(defqueries "some/where/queryfile.sql")
(user-by-id db-spec 42)
Я, под впечатлением, сначала снова захотел попробовать себя в Clojure, но потом одумался и решил написать нечто подобное для Erlang. Правда некоторые вещи я изменил, чтобы было удобнее. Так появилась на свет библиотека eql.
Пока писал — понял, что встроенный лексер мне не подходит; мне пришлось городить кучу костылей, чтобы просто просканировать токены в файле. Тогда я написал свой, который работает не с токенами, а со строками; т.е. он просто разбивает файл на строки, и, если та начинается с --
, то это однозначно комментарий, иначе — запрос; дальше сканирую комментарии на наличие имени запроса. Результат работы — список отсканированных токенов с их названием, местом в файле и контентом; т.е. почти тоже самое, что отдавал мне встроенный в эрланг лексер — это чтобы парсеру было удобнее.
В итоге получилось очень мало кода и приятный результат:
-- your_project/sql_queries/users.sql
-- Counts all the users.
-- :user_count
SELECT count(*) FROM users;
-- :user_by_id
SELECT * FROM users WHERE user_id = ?;
> {ok, Queries} = eql:compile("your_project/sql_queries/users.sql").
> {ok, Q1} = eql:get_query(user_count, Queries).
> {ok, Q2} = proplists:get_value(user_by_id, Queries).
> Q1.
%> "SELECT count(*) FROM users;"
> Q2.
%> "SELECT * FROM users WHERE user_id = ?;"
И два отличия от yesql:
- Имя запроса указывается не
-- name: foo
, а просто-- :foo
; - У меня нет поддержки плейсхолдеров, сами запросы никак не обрабатываются.
Миграции для PostgreSQL на основе eql
Через месяц после этого мне пришла в голову написать утилиту, которая накатывает изменения схемы РСУБД на основе eql, так появилась на свет библиотека pgsql_migration. Работает она тоже очень просто, и, как видно из названия — только с PostgreSQL.
Создаем директорию где-нибудь в priv/migrations и в ней файл с именем <timestamp>_name.sql
:
-- priv/migrations/1424987801_create_users.sql
-- :up
CREATE TABLE users(
id SERIAL NOT NULL,
username CHARACTER VARYING(255) NOT NULL,
password CHARACTER VARYING(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT users_pkey PRIMARY KEY (id)
);
CREATE INDEX index_users_on_username
ON users (username);
-- :down
DROP TABLE users;
Утилита создает в вашей базе данных таблицу migrations
, в которой хранятся накатанные миграции с временем их применения. Чтобы запустить процесс миграции достаточно выполнить:
> pgsql_migration:migrate(Conn, "priv/migrations").
> pgsql_migration:migrate(Conn, "1424987800", "priv/migrations"). % откат/накат до версии
И все будет сделано за вас. Каждая миграция оборачивается в транзакцию, процесс миграции останавливается, если он завершается с ошибками.
Сам использую две эти библиотеки в работе, очень доволен этим подходом и своим вкладом :-)