Queries¶
Абстракция Storage не позволяет делать произвольные выборки, а выбирает весь агрегат целиком по его id.
Часто этого недостаточно. Например, нужно найти пользователя по его логину, т.е. выбрать весь агрегат по условию. Или выбрать таблицу постов с именами их авторов, т.е. выбрать агрегаты с дополнительными полями. Или собрать аналитику и вернуть простые структуры данных.
Т.е. запрос выбирает данные. Эти данные могут быть в том числе состоянием агрегата (записью).
Рассмотрим абстракцию запросов постов. В этом неймспейсе описаны 2 запроса: Получить список постов и получить пост по id.
(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/fdef get-list
:ret (s/coll-of ::post))
(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))
На самом деле тут выбирается не совсем пост. Этот неймспейс объявляет свой «тип» ::post
,
который кроме атрибутов поста содержит еще и некоторые атрибуты своего автора.
Это работает благодаря тому, что записи в clojure открыты к добавлению новых атрибутов.
Чтобы избежать конфликтов имен, атрибуты пользователя будут содержать неймспейс.
Это видно по объявлению спецификаций. Обратите внимание на req-un
и req
:
(ns publicator.domain.aggregates.post
...)
(s/def ::post (s/keys :req-un [::id ::title ::content ::created-at]))
(ns publicator.use-cases.abstractions.post-queries
(:require
[publicator.domain.aggregates.post :as post]
...))
(s/def ::post (s/merge ::post/post
(s/keys :req [::user/id ::user/full-name])))
Теперь разберем фейковую реализацию. В предыдущем параграфе я показывал, что данные хранятся в атоме, содержащем следующую структуру:
{1 (->User 1 ...)
2 (->Post 2 ...)
3 (->Post 3 ...)}
Мы можем сделать так, чтобы фейк делал выборки из этого атома. В нем нет индексов, кроме первичного ключа, и большинство выборок будут выполняться полным сканированием, но это не критично для использования в тестах. Если в вашем приложени ожидаются сложные выборки, то можно использовать специальные структуры для хранения данных вроде datascript.
(ns publicator.use-cases.test.fakes.post-queries
(:require
[publicator.use-cases.abstractions.post-queries :as post-q]
[publicator.domain.aggregates.post :as post]
[publicator.domain.aggregates.user :as user]
[publicator.domain.services.user-posts :as user-posts]))
(defn- author-for-post [db post]
(->> @db
(vals)
(filter user/user?)
(filter #(user-posts/author? % post))
(first)))
(defn- assoc-user-fields [post user]
(assoc post
::user/id (:id user)
::user/full-name (:full-name user)))
(deftype GetList [db]
post-q/GetList
(-get-list [_]
(->> @db
(vals)
(filter post/post?)
(map #(when-some [author (author-for-post db %)]
(assoc-user-fields % author)))
(remove nil?))))
(deftype GetById [db]
post-q/GetById
(-get-by-id [_ id]
(when-some [post (get @db id)]
(when-some [author (author-for-post db post)]
(assoc-user-fields post author)))))
(defn binding-map [db]
{#'post-q/*get-list* (->GetList db)
#'post-q/*get-by-id* (->GetById db)})
Самостоятельно ознакомьтесь с выборками пользователей: