Инструменты¶
Здесь перечисляются базовые инструменты, которыми мы будем пользоваться.
clojure.jdbc¶
Для работы с БД воспользуемся clojure.jdbc. Кроме нее есть «стандартная» библиотека clojure/java.jdbc. Первая показалась мне удобнее. Об их различиях можно почитать тут.
Пример использования:
(require '[jdbc.core :as jdbc])
(with-open [conn (jdbc/connection dbspec)]
(jdbc/execute conn "CREATE TABLE foo (id serial, name text);"))
Есть возможность прописать правила преобразования sql типов в clojure типы и обратно, пример.
В качестве dbspec
можно передавать connection pool.
Connection pool¶
clojure.jdbc работает с различными пулами соединений. Я выбрал c3p0.
Оформим пул в компонент:
(ns publicator.persistence.components.data-source
(:require
[com.stuartsierra.component :as component])
(:import
[com.mchange.v2.c3p0 ComboPooledDataSource]))
(defrecord DataSource [config val]
component/Lifecycle
(start [this]
(assoc this :val
(doto (ComboPooledDataSource.)
(.setJdbcUrl (:jdbc-url config))
(.setUser (:user config))
(.setPassword (:password config)))))
(stop [this]
(.close val)
(assoc this :val nil)))
(defn build [config]
(DataSource. config nil))
Query builder¶
Для clojure есть разные SQL query builder. Одни используют data DSL, как honeysql, другие - sql, как hugsql.
Я предпочитаю работать с sql, и не переводить мысленно код из DSL в sql. Также это позволяет без боли использовать расширения синтаксиса postgresql.
Но никто не запрещает использовать оба подхода в одном приложении. Data DSL отлично подходит для построения сложного запроса по большому количеству условий.
Test db¶
В следующих параграфах будут приводиться тесты и нужно разобраться как готовить базу для тестов.
Предполагается, что база данных заранее создана и ее настройки передаются через
переменную окружения TEST_DATABASE_URL
.
Хорошо это или плохо, но тесты в clojure независимы. Нет способа запустить произвольный код перед запуском тестов. Можно только для каждого неймспейса с тестами прописать фикстуры. Иными словами, для каждого неймспейса с тестами нужно явно готовить окружение. В нашем случае это запуск миграций.
Заведем неймспейс с фикстурами, подготавливающими тестовую бд.
publicator.persistence.test.db
экспортирует 2 функции once-fixture
и each-fixture
,
которые мы должны использовать в своих тестах, работающих с БД.
Мы объявляем систему из 2х компонентов: data-source
и migration
.
(ns publicator.persistence.test.db
(:require
[publicator.persistence.components.data-source :as data-source]
[publicator.persistence.components.migration :as migration]
[publicator.persistence.utils.env :as env]
[com.stuartsierra.component :as component]
[jdbc.core :as jdbc]
[hugsql.core :as hugsql]
[hugsql.adapter.clojure-jdbc :as cj-adapter]))
(hugsql/def-db-fns "publicator/persistence/test/db.sql"
{:adapter (cj-adapter/hugsql-adapter-clojure-jdbc)
:quoting :ansi})
(defn- build-system []
(component/system-map
:data-source (data-source/build (env/data-source-opts "TEST_DATABASE_URL"))
:migration (component/using (migration/build)
[:data-source])))
(defn- with-system [f]
(let [system (atom (build-system))]
(try
(swap! system component/start)
(f @system)
(finally
(swap! system component/stop)))))
(declare ^:dynamic *data-source*)
(defn once-fixture [t]
(with-system
(fn [system]
(let [data-source (-> system :data-source :val)]
(binding [*data-source* data-source]
(t))))))
(defn each-fixture [t]
(try
(t)
(finally
(with-open [conn (jdbc/connection *data-source*)]
(truncate-all conn)))))
Т.е. все тесты неймспейса выполняются в рамках одной системы, а после каждого теста происходит очистка.
Тесты получают доступ к пулу коннектов с помощью динамической переменной *data-source*
.
-- db.sql
-- :name- truncate-all :!
DO $$
DECLARE
statements CURSOR FOR
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
AND tablename != 'flyway_schema_history';
BEGIN
FOR stmt IN statements LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename);
END LOOP;
END $$