После перехода в наших проектах с java на clojure нам необходимо было найти замену привычным средствам работы с базами данных.
В clojure есть стандартная библиотека работы с бд clojure.java.jdbc и несколько библиотек, основанных на ней и позволяющих писать запросы в предоставляемом ими eDSL. Но для «ежедневного пользования» нам хотелось что-то по удобству напоминающее jpa и работу с ним в IDE.
Мы подумали, почему бы не написать свою библиотеку, которая бы идеально подходила нашим требованиям. А требования были следующие:
Тогда работать с бд можно было бы так же удобно, как и раньше. Итак, после 2-3 дней библиотека была готова.
Были реализованы следующие публичные функции:
— generate-table-column-names: генерация имен таблиц и информации о их полях в виде переменных в конкретном пространства имен;
— generate-column-value-constants: генерация констант из полей заданной таблицы в конкретном пространстве имен;
— with-db: макрос, создающий новую коннекцию и транзакцию с которыми выполняет свое тело, при срабатывании исключительной ситуации происходит rollback;
— select: выборка с использованием текущей коннекции;
— select-with-db: выборка с созданием новой коннекции по указанному описанию;
— select-deep: выборка с созданием новой коннекции по указанному описанию со связыванием с другими таблицами по внешним ключам;
— get-field-from-row: получение поля из записи с возможностью работы с цепочкой полей таблиц, связанных по внешнему ключу;
— update, insert.
Например, у нас есть описание соединения:
Генерируем таблицы рабочей базы, их поля и некоторые константы из таблиц:
В итоге у нас получись следующие исходники:
entities.clj:
recordtypes.clj:
Допустим, мы хотим знать пользователь или бот обладает рекордом наибольшего количества убийств в течении раунда:
Получаем тип игрока через связанные внешним ключом таблицы:
Результат: обладатель рекорда — «бот».
И самое главное, везде сработало дополнение (не дополняются только :keywordы),
Плюсы библиотеки
— автодополнение констант, таблиц и полей;
— вставка определенным образом вышеприведенной генерации в макросы при изменении наименований таблиц, полей или констант позволяет получать ошибки именно во время компиляции, а не на этапе исполнения;
— удобный eDSL (update, insert, select-..., with-db), умеющий работать со сгенерированными данными.
В итоге работа с базами данных в наших проектах на clojure стала проще, гибче и удобнее, чем была раньше.
В clojure есть стандартная библиотека работы с бд clojure.java.jdbc и несколько библиотек, основанных на ней и позволяющих писать запросы в предоставляемом ими eDSL. Но для «ежедневного пользования» нам хотелось что-то по удобству напоминающее jpa и работу с ним в IDE.
Мы подумали, почему бы не написать свою библиотеку, которая бы идеально подходила нашим требованиям. А требования были следующие:
- автодополнение таблиц, полей;
- автодополнение констант из определенных таблиц (более позднее);
- удобный eDSL запросов;
- возможность без ручного запроса получать значения из таблиц, связанные по внешнему ключу (более позднее).
Тогда работать с бд можно было бы так же удобно, как и раньше. Итак, после 2-3 дней библиотека была готова.
Были реализованы следующие публичные функции:
— generate-table-column-names: генерация имен таблиц и информации о их полях в виде переменных в конкретном пространства имен;
— generate-column-value-constants: генерация констант из полей заданной таблицы в конкретном пространстве имен;
— with-db: макрос, создающий новую коннекцию и транзакцию с которыми выполняет свое тело, при срабатывании исключительной ситуации происходит rollback;
— select: выборка с использованием текущей коннекции;
— select-with-db: выборка с созданием новой коннекции по указанному описанию;
— select-deep: выборка с созданием новой коннекции по указанному описанию со связыванием с другими таблицами по внешним ключам;
— get-field-from-row: получение поля из записи с возможностью работы с цепочкой полей таблиц, связанных по внешнему ключу;
— update, insert.
Пример использования
Например, у нас есть описание соединения:
(def db {:classname "com.mysql.jdbc.Driver"
:subprotocol "mysql"
:subname "//localhost:3306/clj_query"
:user "clj"
:password "clj"})
Генерируем таблицы рабочей базы, их поля и некоторые константы из таблиц:
user> (require '[ libs.db.gentablecolumns :as gen ])
nil
user> (require '[ libs.db.gencolumnvalues :as genval ])
nil
user> (gen/generate-table-column-names db)
nil
user> (genval/generate-column-value-constants db table-recordtypes (:name recordtypes-name))
nil
В итоге у нас получись следующие исходники:
entities.clj:
(ns db.entities)
;;;; players
(def table-players "players")
(def players-id {:type {:size 10, :name "INT UNSIGNED"}, :table "players", :name "id"})
(def players-name {:type {:size 255, :name "VARCHAR"}, :table "players", :name "name"})
(def players-type_id {:type {:size 10, :name "INT UNSIGNED"}, :table "players", :name "type_id"})
;;;; playertypes
(def table-playertypes "playertypes")
(def playertypes-id {:type {:size 10, :name "INT UNSIGNED"}, :table "playertypes", :name "id"})
(def playertypes-name {:type {:size 255, :name "VARCHAR"}, :table "playertypes", :name "name"})
;;;; records
(def table-records "records")
(def records-id {:type {:size 10, :name "INT UNSIGNED"}, :table "records", :name "id"})
(def records-type_id {:type {:size 10, :name "INT UNSIGNED"}, :table "records", :name "type_id"})
(def records-score {:type {:size 19, :name "BIGINT"}, :table "records", :name "score"})
(def records-player_id {:type {:size 10, :name "INT UNSIGNED"}, :table "records", :name "player_id"})
;;;; recordtypes
(def table-recordtypes "recordtypes")
(def recordtypes-id {:type {:size 10, :name "INT UNSIGNED"}, :table "recordtypes", :name "id"})
(def recordtypes-name {:type {:size 255, :name "VARCHAR"}, :table "recordtypes", :name "name"})
recordtypes.clj:
(ns db.recordtypes)
(def name-kills-per-round-id 1)
(def name-kills-per-round "kills per round")
(def name-kills-per-game-id 2)
(def name-kills-per-game "kills per game")
(def name-longest-undead-time-id 3)
(def name-longest-undead-time "longest undead time")
Допустим, мы хотим знать пользователь или бот обладает рекордом наибольшего количества убийств в течении раунда:
user> (require '[ db.recordtypes :as rectypes ])
nil
user> (use 'db.entities)
nil
user> (def record (first (q/select-deep db table-records
:join [[table-recordtypes [:= recordtypes-id records-type_id]]]
:where [:= recordtypes-id rectypes/name-kills-per-round-id])))
#'user/record
Получаем тип игрока через связанные внешним ключом таблицы:
user> (q/get-field-from-row record records-player_id players-type_id playertypes-name)
"bot"
Результат: обладатель рекорда — «бот».
И самое главное, везде сработало дополнение (не дополняются только :keywordы),
Плюсы библиотеки
— автодополнение констант, таблиц и полей;
— вставка определенным образом вышеприведенной генерации в макросы при изменении наименований таблиц, полей или констант позволяет получать ошибки именно во время компиляции, а не на этапе исполнения;
— удобный eDSL (update, insert, select-..., with-db), умеющий работать со сгенерированными данными.
В итоге работа с базами данных в наших проектах на clojure стала проще, гибче и удобнее, чем была раньше.