09: performing inserts
In this section you'll look at how you might insert the data Specmonstah has generated into a database. We'll be adding the data to an atom, but you can apply the idea to your own database. The code:
(ns reifyhealth.specmonstah-tutorial.09
(:require [reifyhealth.specmonstah.core :as sm]
[clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[reifyhealth.specmonstah.spec-gen :as sg]))
(s/def ::id (s/and pos-int? #(< % 100)))
(s/def ::not-empty-string (s/and string? not-empty #(< (count %) 10)))
(s/def ::username ::not-empty-string)
(s/def ::user (s/keys :req-un [::id ::username]))
(s/def ::name ::not-empty-string)
(s/def ::topic (s/keys :req-un [::id ::name ::owner-id]))
(s/def ::owner-id ::id)
(s/def ::topic-id ::id)
(s/def ::content ::not-empty-string)
(s/def ::post (s/keys :req-un [::id ::owner-id ::topic-id ::content]))
(def schema
{:user {:prefix :u
:spec ::user}
:topic {:prefix :t
:spec ::topic
:relations {:owner-id [:user :id]}}
:post {:prefix :p
:spec ::post
:relations {:topic-id [:topic :id]}}})
(def database (atom []))
(defn insert
[db {:keys [ent-type visit-val spec-gen]}]
(when-not visit-val
(swap! database conj [ent-type spec-gen])))
(defn ex-01
(reset! database [])
(-> (sg/ent-db-spec-gen {:schema schema} {:post [[1]]})
(sm/visit-ents :insert insert))
;; =>
[[:user {:id 7, :username "j29AFqnr"}]
[:topic {:id 2, :name "Qqo04X1Zo", :owner-id 7}]
[:post {:id 70, :owner-id 2, :topic-id 2, :content "al"}]]
The specs and schema should be familiar by now. Looking at the ex-01 function, we see that it calls sg/ent-db-spec-gen. As you saw earlier, this creates the ent db and uses clojure.spec to generate a map for each ent, storing the map under the ent's :spec-gen attribute. The resulting ent db is passed to sm/visit-ents with the visit key :insert and visiting function insert.
insert works by:
  • Checking whether this ent has already been inserted
  • If not, updating the database atom by conjing a vector of the
    ent's type and the value generated by spec-gen.
Let's go through insert line by line:
(defn insert
[db {:keys [ent-type visit-val spec-gen]}]
(when-not visit-val
(swap! database conj [ent-type spec-gen])))
The second argument to all visiting functions is a map. We're only using the keys :ent-type, :visit-val, and :spec-gen. The full map includes:
  • All of the ent's attributes (like :spec-gen)
  • :visit-val, the value from previous visits if there have been any
  • :visit-key - when the visiting function returns, its value is
    added as an ent attr using the key :visit-key
  • :query-opts - any query opts like {:refs {} :spec-gen {}} for
    the ent
  • :visit-query-opts - any query opts meant for this visiting fn
(when-not visit-val ...), checks whether insert has already visited this ent. (I'll explain why you want to perform this check soon.) If the ent hasn't been visited, the database gets updated by conjing a vector of the ent-type and spec-gen. The database atom ends up with a value like this:
[[:user {:id 7, :username "j29AFqnr"}]
[:topic {:id 2, :name "Qqo04X1Zo", :owner-id 7}]
[:post {:id 70, :owner-id 7, :topic-id 2, :content "al"}]]
Each ent is inserted in dependency order: :user first, then :topic, then :post.
Now let's revisit (when-not visit-val ...). You want to perform this check because of Specmonstah's progressive construction feature. As we covered in 05: Progressive construction, it's possible to pass an ent-db to successive calls to sm/add-ents. If you added more ents and wanted to insert, you wouldn't want to re-insert previous ents. ex-02 demonstrates this:
(defn ex-02
(reset! database [])
(-> (sg/ent-db-spec-gen {:schema schema} {:post [[1]]})
(sm/visit-ents :insert insert)
(sg/ent-db-spec-gen {:post [[3]]})
(sm/visit-ents :insert insert))
;; =>
[[:user {:id 6, :username "kUQzVd4S"}]
[:topic {:id 98, :name "I3", :owner-id 6}]
[:post {:id 9, :owner-id 6, :topic-id 98, :content "CEZ"}]
[:post {:id 7, :owner-id 6, :topic-id 98, :content "mU391M"}]
[:post {:id 2, :owner-id 6, :topic-id 98, :content "6ngN"}]
[:post {:id 28, :owner-id 6, :topic-id 98, :content "m4"}]]
The :user, :todo-list, and :todo ents from the first call to ent-db-spec-gen are only inserted once, even though they are visited by insert multiple times.
In fact, recall that ent-db-spec-gen internally calls sm/add-ents and then calls the sg/spec-gen visiting function. sg/spec-gen is written with this same principle in mind: it can visit the ent db multiple times, and won't overwrite any existing values. The pattern is common enough that Specmonstah provides the sm/visit-ents-once which you can use instead of sm/visit-ents:
(defn insert-once
[db {:keys [ent-type spec-gen]}]
(swap! database conj [ent-type spec-gen])
(defn ex-03
(reset! database [])
(-> (sg/ent-db-spec-gen {:schema schema} {:post [[1]]})
(sm/visit-ents-once :insert insert-once)
(sg/ent-db-spec-gen {:post [[3]]})
(sm/visit-ents-once :insert insert-once))
;; =>
[[:user {:id 2, :username "MuGc6"}]
[:topic {:id 87, :name "0Oj9P", :owner-id 2}]
[:post {:id 2, :owner-id 2, :topic-id 87, :content "c6k3Z1HwI"}]
[:post {:id 1, :owner-id 2, :topic-id 87, :content "qEjSKQ"}]
[:post {:id 9, :owner-id 2, :topic-id 87, :content "646nn4bI"}]
[:post {:id 45, :owner-id 2, :topic-id 87, :content "J2vL0Mgi"}]]
Instead of having to write a when-not conditional yourself, visit-ents-once checks whether any value exists for the visiting function (even nil or false), and if it does, it doesn't apply the visiting function to that ent.
With this coverage of visit-ents and visit-ents-once, you should be able to handle most use cases. The rest of the guide covers more specific modeling uses cases, like:
  • Satisfying a database constraint that there be no two records with the same two foreign keys (e.g. if you're building a forum and don't want a user to like the same post twice)
  • Modeling an attribute that's a vector of foreign keys
  • Handling cycles
  • Using binding to specify that all the refs in an entire hierarchy should refer to a specified ent
  • Attribute polymorphism (e.g. if a like can refer to either post or a topic)
Copy link