Запросы¶
Для примера рассмотрим запросы для агрегата Пост, а именно получение списка постов и поста по идентификатору. При этом пост должен содержать дополнительные аттрибуты: идентификатор и полное имя автора.
Вот абстракция:
(ns publicator.use-cases.abstractions.post-queries
(:require
[publicator.domain.aggregates.user :as user]
[publicator.domain.aggregates.post :as post]
[clojure.spec.alpha :as s]))
(defprotocol GetList
(-get-list [this]))
(declare ^:dynamic *get-list*)
(s/def ::post (s/merge ::post/post
(s/keys :req [::user/id ::user/full-name])))
(s/def ::posts (s/coll-of ::post))
(s/fdef get-list
:args nil?
:ret ::posts)
(defn get-list []
(-get-list *get-list*))
(defprotocol GetById
(-get-by-id [this id]))
(declare ^:dynamic *get-by-id*)
(s/fdef get-by-id
:args (s/cat :id ::post/id)
:ret (s/nilable ::post))
(defn get-by-id [id]
(-get-by-id *get-by-id* id))
Напомню миграции создающие таблицы для постов и пользователей:
-- persistence/resources/db/migration/V2__create_post.sql
CREATE TABLE "post" (
"id" bigint PRIMARY KEY,
"title" varchar(255),
"content" text,
"created-at" timestamp
);
-- persistence/resources/db/migration/V3__create_user.sql
CREATE TABLE "user" (
"id" bigint PRIMARY KEY,
"login" varchar(255) UNIQUE,
"full-name" varchar(255),
"password-digest" text,
"posts-ids" bigint[],
"created-at" timestamp
);
Обратите внимание, что пользователь хранит идентификаторы постов с помощью postgresql
массивов.
При этом добавляются
операции
над массивами, например @>
- «содержит».
Вот sql реализация запросов:
-- :name- post-get-list :? :n
SELECT "post".*,
"user"."id" AS "publicator.domain.aggregates.user/id",
"user"."full-name" AS "publicator.domain.aggregates.user/full-name"
FROM "post"
JOIN "user" ON "user"."posts-ids" @> ARRAY["post"."id"]
-- :name- post-get-by-id :? :1
SELECT "post".*,
"user"."id" AS "publicator.domain.aggregates.user/id",
"user"."full-name" AS "publicator.domain.aggregates.user/full-name"
FROM "post"
JOIN "user" ON "user"."posts-ids" @> ARRAY["post"."id"]
WHERE "post"."id" = :id
Отмечу, что БД не содержит индекса для posts-ids
, но если вы будете хранить много данных, то
можете его добавить.
Нам осталось использовать эти запросы и выполнить некоторые преобразования типов:
(ns publicator.persistence.post-queries
(:require
[hugsql.core :as hugsql]
[jdbc.core :as jdbc]
[publicator.use-cases.abstractions.post-queries :as post-q]
[publicator.domain.aggregates.post :as post]))
(hugsql/def-db-fns "publicator/persistence/post_queries.sql")
(defn- sql->post [row]
(post/map->Post row))
(deftype GetList [data-source]
post-q/GetList
(-get-list [this]
(with-open [conn (jdbc/connection data-source)]
(map sql->post (post-get-list conn)))))
(deftype GetById [data-source]
post-q/GetById
(-get-by-id [this id]
(with-open [conn (jdbc/connection data-source)]
(when-let [row (post-get-by-id conn {:id id})]
(sql->post row)))))
(defn binding-map [data-source]
{#'post-q/*get-list* (GetList. data-source)
#'post-q/*get-by-id* (GetById. data-source)})
(ns publicator.persistence.post-queries-test
(:require
[clojure.test :as t]
[publicator.utils.test.instrument :as instrument]
[publicator.use-cases.test.factories :as factories]
[publicator.domain.test.fakes.password-hasher :as fakes.password-hasher]
[publicator.domain.test.fakes.id-generator :as fakes.id-generator]
[publicator.persistence.storage :as persistence.storage]
[publicator.persistence.storage.user-mapper :as user-mapper]
[publicator.persistence.storage.post-mapper :as post-mapper]
[publicator.persistence.test.db :as db]
[publicator.use-cases.abstractions.post-queries :as post-q]
[publicator.persistence.post-queries :as sut]
[publicator.domain.aggregates.user :as user]))
(defn setup [t]
(with-bindings (merge
(fakes.password-hasher/binding-map)
(fakes.id-generator/binding-map)
(persistence.storage/binding-map db/*data-source*
(merge
(user-mapper/mapper)
(post-mapper/mapper)))
(sut/binding-map db/*data-source*))
(t)))
(t/use-fixtures :once
instrument/fixture
db/once-fixture)
(t/use-fixtures :each
db/each-fixture
setup)
(defn post-with-user [post user]
(assoc post
::user/id (:id user)
::user/full-name (:full-name user)))
(t/deftest get-list-found
(let [post (factories/create-post)
user (factories/create-user {:posts-ids #{(:id post)}})
res (post-q/get-list)
item (first res)]
(t/is (= 1 (count res)))
(t/is (= (post-with-user post user)
item))))
(t/deftest get-list-empty
(let [res (post-q/get-list)]
(t/is (empty? res))))
(t/deftest get-by-id
(let [post (factories/create-post)
id (:id post)
user (factories/create-user {:posts-ids #{id}})
item (post-q/get-by-id id)]
(t/is (= (post-with-user post user)
item))))
(t/deftest get-by-id-not-found
(let [item (post-q/get-by-id 42)]
(t/is (nil? item))))