Let us assume the following - admittedly flawed - schema, for which we will add gradual support:
All the following examples can be reproduced in the
test/seql/readme_test.clj
integration test. To perform queries, an
environment must be supplied, which consists of a schema, and a JDBC
config. In test/seql/fixtures.clj
, code is provided to experiment with an H2
database.
For all schemas displayed below, we assume an env set up in the following manner:
(def env {:schema ... :jdbc your-database-config})
(require '[seql.query :as q])
(require '[seql.lister :as l])
(require '[seql.mutation :as m])
(require '[clojure.spec.alpha :as s])
(require '[seql.helpers :refer [make-schema ident idents field mutation
has-many condition from-spec]])
Seql assumes you are familiar with clojure.spec
if that is not
the case, please refer to: https://clojure.org/guides/spec
We can start by providing specs for the individual fields in each table:
(create-ns 'my.entities)
(create-ns 'my.entities.account)
(create-ns 'my.entities.user)
(create-ns 'my.entities.invoice)
(create-ns 'my.entities.invoice-line)
(create-ns 'my.entities.product)
(alias 'account 'my.entities.account)
(alias 'user 'my.entities.user)
(alias 'invoice 'my.entities.invoice)
(alias 'invoice-line 'my.entities.invoice-line)
(alias 'product 'my.entities.product)
(s/def ::account/name string?)
(s/def ::account/state #{:active :suspended :terminated})
(s/def ::account/account (s/keys :req [::account/name ::account/state]))
(s/def ::user/name string?)
(s/def ::user/email string?)
(s/def ::user/user (s/keys :req [::user/name ::user/email]))
(s/def ::invoice/state keyword?)
(s/def ::invoice/total nat-int?)
(s/def ::invoice/invoice (s/keys :req [::invoice/state ::invoice/total]))
(s/def ::invoice-line/quantity nat-int?)
(s/def ::invoice-line/invoice-line (s/keys :req [::invoice-line/quantity]))
(s/def ::product/name string?)
(s/def ::product/product (s/keys :req [::product/name]))
At first, accounts need to be looked up. We can build a minimal schema:
(make-schema
(entity ::account/account
(field :name)
(field :state)))
Let's unpack things here:
[entity-name table-name]
can be providedWith this, simple queries can be performed:
(query env ::account/account [::account/name ::account/state])
;; or to fetch all default fields:
(query env ::account/account)
;; =>
[#::account{:name "a0" :state :active}
#::account{:name "a1" :state :active}
#::account{:name "a2" :state :suspended}]
Idents can also be looked up:
(query env [::account/id 0] [::account/name ::account/state])
;; =>
#::account{:name "a0" :state :active}
Notice how the last query yielded a single value instead of a collection. It is expected that idents will yield at most a single value (as a corollary, idents should only be used for database fields which enforce this guarantee).
Also notice how there was no prior mention of ::account/id
A first concrete improvement we can bring to the schema build step when
an s/keys
spec is available for our entity is to infer most of the schema
from it:
(make-schema
(from-spec ::account/account))
While here, we can also add a short name to provide a shortcut condition
for [::account/state :active]
:
(make-schema
(from-spec ::account/account
(condition :active :state :active)
(condition :state)))
We can now perform the following query:
(query env ::account/account [::account/name] [[::account/active]])
;; =>
[#::account{:name "a0"}
#::account{:name "a1"}]
(query env ::account/account [::account/name] [[::account/state :suspended]])
;; =>
[#::account{:name "a2"}]
For queries, seql's strength lies in its ability to understand the way entities are tied together. Seql offers support for one-to-many (has many), one-to-one (has one), and many-to-many (has many through) relations.
Let's start with a single relation before building larger nested trees. Since no assumption is made on schemas, the relations must specify foreign keys explictly:
(make-schema
(from-spec ::account/account
(has-many ::users [:id ::user/account-id])
(condition :active :state :active))
(from-spec ::user/user))
This will allow doing tree lookups, fetching arbitrary fields from the nested entity as well:
(query env
::account/account
[::account/name
::account/state
{::account/users [::user/name ::user/email]}])
;; =>
[#::account{:name "a0"
:state :active
:users [#::user{:name "u0a0" :email "u0@a0"}
#::user{:name "u1a0" :email "u1@a0"}]}
#::account{:name "a1"
:state :active
:users [#::user{:name "u2a1" :email "u2@a1"}
#::user{:name "u3a1" :email "u3@a1"}]}
#::account{:name "a2" :state :suspended}]
We've now covered full capabilities of the query part of the schema, were we saw that:
With this in mind, here's a complete schema for the above database schema:
(make-schema
(from-spec ::account/account
(has-many :users [:id ::user/account-id])
(has-many :invoices [:id ::invoice/account-id])
(condition :active :state :active)
(add-create-mutation))
(mutation :account/update
::account
[{:keys [id] :as params}]
(-> (h/update :account)
;; values are fed unqualified
(h/set (dissoc params :id))
(h/where [:= :id id]))))
(from-spec ::user/user)
(from-spec ::invoice/invoice
(has-many :lines [:id ::invoice-line/invoice-id])
(condition :unpaid :state :unpaid)
(condition :paid :state :paid))
(from-spec ::product/product)
(from-spec [::invoice-line/invoice-line :invoiceline]
(has-one :product [:product-id ::product/id])))
With querying sorted, mutations need to be expressed. Here, seql takes the approach of making mutations separate, explict, and validated. As with most other seql features, mutations are implemented with a key inside the entity description.
At its core, mutations expect two things:
To simplify matters, add-create-mutation
can be used for inserts.
(from-spec ::account/account
(has-many :users [:id ::user/account-id])
(has-many :invoices [:id ::invoice/account-id])
(condition :active :state :active)
(add-create-mutation))
(mutation :account/update
::account
[{:keys [id] :as params}]
(-> (h/update :account)
;; values are fed unqualified
(h/set (dissoc params :id))
(h/where [:= :id id]))))
The implicit mutation created by add-create-mutation
will be
named: ::account/create
, a spec has to exist for it:
(s/def ::account/create ::account/account)
Adding new accounts can now be done through mutate!
:
(mutate! env ::account/create {::account/name "a3"
::account/state :active})
(query env [::account/name "a3"] [::account/state])
;; =>
#::account{:state :active}
To provide for clean CQRS type workflows, listeners can be added to mutations. Each listener will subsequently be called on sucessful transactions with a map of:
:mutation
: the name of the mutation called:result
: the result of the transaction:params
: input parameters given to the mutation:metadata
: metadata supplied to the mutation, if any(def last-result (atom nil))
(defn store-result
[details]
(reset! last-result (select-keys details [:mutation :result])))
(let [env (l/add-listener env ::account/create store-result)]
(mutate! env ::account/create {::account/name "a4"
::account/state :active}))
@last-result
;; => {:result [1] :mutation :account/create}
Can you improve this documentation? These fine people already did:
Pierre-Yves Ritschard, Juan E. Maya, Miguel Ping & Max PenetEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close