Как стать автором
Обновить

Есть ли жизнь без ORM?

Время на прочтение3 мин
Количество просмотров2.6K
После перехода в наших проектах с java на 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 стала проще, гибче и удобнее, чем была раньше.
Теги:
Хабы:
Всего голосов 18: ↑11 и ↓7+4
Комментарии13

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань