March 9, 2016

Boot and Component

When I first read Stuart Sierra's blog post about his Clojure Workflow, Reloaded, a few years ago, I had just started to become comfortable with clojure. After reading it, I was pretty overwhelmed and a little bit skeptical. I wondered if all the extra overhead of thinking about systems and components was worth all the trouble? So I filed the link away to come back to later.

Over the years, as I've played with clojure more and more, I've found myself going back to that article. And each time, I understand and appreciate a little more.

If you're on the fence, or still not sure this component stuff is a good idea, here are some links to learn more about it:

Just last week, I had some time to finally dig in and understand all the pieces. And in hindsight, I wish I would have done it sooner.

Components are useful just by themselves. But they're even better with boot. Here's a quick post on how I configured boot-clj to start an h2 database component.

The Database Component

Here's the code for a simple database component.

(defn db-spec [subprotocol driver-classname subname user password]
  {:subprotocol    subprotocol
   :classname      driver-classname
   :subname        subname
   :user           user
   :password       password
   :make-pool?     true
   :naming         {:keys   clojure.string/lower-case
                    :fields clojure.string/upper-case}})
                    
(defrecord Database [subprotocol driver-classname subname user password]
  component/Lifecycle
  
  (start [component]
    (if-let [dbspec (:connection component)]
      (do
        (println ";; db connection already established")
        component)
      (let [dbspec (db-spec subprotocol 
                            driver-classname
                            subname
                            user
                            password)]
        (println (str ";; connected to " user "@" subprotocol ":" 
                      subname " ..."))
        (ddl/recreate-tables-safe! dbspec)
        (assoc component :connection dbspec))))

  (stop [component]
    (if-let [dbspec (:connection component)]
      (println ";; tearing down datbase connection")      
      (println ";; no db connection exists"))
    (assoc component :connection nil)))

(defn new-db [m]
  (map->Database m))

The start function first checks to see if a connection already exists. If not, a new dbspec is associated to the component. I also wrote a "recreate-tables-safe!" function that can be called to create any tables when then don't exist.

Once my database component is started, it's nice to know that the database connection is all set up and that the tables have been created and are ready to go!

The stop function just closes the db connection.

build.boot

Here's what I added to my project's build.boot file

;;;; Components

(require '[com.stuartsierra.component   :as component]
         '[clojure.tools.namespace.repl :refer [refresh]])

(def system nil)

(def db-defaults {:subprotocol      "h2"
                  :driver-classname "org.h2.Driver"
                  :subname          "file:./db/dev/dev.db"
                  :user             "" 
                  :password         ""})

(defn init [& [opts]]
  (alter-var-root 
   #'system
   (constantly (component/system-map
                :h2db (new-db (merge db-defaults opts))))))

(defn start []
  (alter-var-root #'system component/start))

(defn stop []
  (alter-var-root #'system
                  (fn [s] (when s (component/stop s)))))

(defn go [& [opts]]
  (init opts)
  (start))

(defn reset []
  (stop)
  (refresh :after 'boot.user/go))

(deftask db-component
  "Initialize components system via boot"
  [s subprotocol      VAL str "ex: h2 or postgresql"
   d driver-classname VAL str "ex: org.h2.Driver"
   n subname          VAL str "ex: file:./db/dev.db"
   u user             VAL str "db username"
   p password         VAL str "db password"]
  (fn middleware [next-handler]
    (fn handler [fileset]
      (go {:subprotocol      subprotocol
           :driver-classname driver-classname
           :subname          subname
           :user             user
           :password         password})
      (next-handler fileset))))

I just copied the system, init, start, stop, go, and reset from Stuart's suggestions on the way he likes to manage systems and components.

The db-component is a custom boot task that initializes and starts the system (along with the db component).

boot.user

Stuart mentions as part of his clojure workflow that he likes to stay in the user namespace.

After working with clojure for a while now, I can relate. It can be a pain to jump back and forth between namespaces.

Boot solves this nicely. I really like how boot drops you into the boot.user namespace. It's nice to have functions like clojure.repl/doc already available at your finger tips.

In this case, the init, start, stop and reset functions are all immediately available as well. It's really nice to be able to confidently "restart" all my custom code without restarting the repl.

By combining the db-component task along with the boot repl task, I'm instantly up and running with a nice live database connection to a database with all the tables created.

Here's how I compose the boot tasks

(deftask dev
  []
  (comp
   (repl)
   (db-component)))

Then it's as simpl as starting boot

boot dev

And I'm ready to go

boot.user=> (let [dbspec (:connection (:h2db system))]
       #_=>   (with-open [conn (clojure.java.jdbc/get-connection dbspec)]
       #_=>     (.isValid conn 5)))
true

More nice libraries

After coding all this up, I googled around some more handy libraries. If you like organizing your code this way, I definitely suggest you check these out.

  • weavejester/reloaded is a library that implements Stuart's recommended start, stop, reset, etc functions.
  • danielsz/system is a project thatprovides nicely packaged ready-to-use components. And even has a nice boot example ... to boot (sorry I couldn't resist!)
Tags: clojure software boot-clj