clojure-demos/dsl-demo/readme.org

7.8 KiB

Intro to HoneySQL & Hiccup

Hiccup

https://github.com/weavejester/hiccup hiccup is a html DSL, used extensively in the clojure's eco-system, there are others as well but hiccup is the most widely used.

In hiccup everything is a list, this means you can easily compose html using standard language constructs.

Simple examples

If using reagent you don't need to pass into h/html but this is server side reagent also has some helpers to work with react nicer.

(h/html [:span "bar"])

Attributes are added as a map of values styles are also a map

(h/html [:span "bar" {:class "class1 class2" :title "my title" :style {:color "red"}}])

You can use shorthand to add id's and classes

(h/html [:span#id.class1.class2 "bar"])

Compossible components

The main advantage comes from the ability to compose the parts together, so we can break this into function's

In this example its only a hash map, the data is separated out from the html, in reagent you can use atom for live updating.

(defn navbar-link [{:keys [href title text] :or {text nil title nil}}]
  [:a.link.dim.white.dib.mr3 {:href href :title title} text])

(defn navbar [links]
  [:header.bg-black-90.fixed.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l
   [:nav.f6.fw6.ttu.tracked
    (map navbar-link links)]])

(h/html (navbar [{:href "link1" :title "title here"}
                 {:href "link2" :title nil}
                 {:href "link3" :text "link text"}
                 {:href "link4"}]))

You can use clojure core language to manipulate these lists.

place parts inside another containing element

(h/html (into [:div.container] [[:span "span 1"] [:span "span 2"]]))

You could also use merge

(h/html (merge [:span "span 1"] [:span "span 2"] [:span "span 3"]))

We can take advantage of lazyness if we like

(h/html  (take 2 (map navbar-link [{:href "link1" :title "title here"}
                                   {:href "link2" :title nil}
                                   {:href "link3" :text "link text"}
                                   {:href "link4"}])))

HoneySQL

https://github.com/seancorfield/honeysql HoneySQL is a DSL specifically for building SQL queries, much like hiccup you build up a data structure that can be composed, honey has a lot of helper functions available so you don't need to build these maps manually.

Honey does not care about connecting to your database it only builds your queries.

A nice way to write these is using -> which is a threading macro, put simply the result one call is passed to the next.

Basic queries

first tip is sql/format will convert to an sql query, use it to examine your query's or run else where. In this example we create a select but with no from format will still give you a query but it will be incomplete.

(sql/format (sqlh/select :first_name :last_name :email))

You use multiple functions to combine the parts, below we add the missing from.

(sql/format (sqlh/from
             (sqlh/select :first_name :last_name :email)
             :users))

How ever it's much nicer for readability to use the threading macro

(sql/format
 (-> (sqlh/select :first_name :last_name :email)
     (sqlh/from :users)))

The functions understand ordering so your order does not matter.

(sql/format
 (-> (sqlh/from :users)
     (sqlh/select :first_name :last_name :email)))

We can do the usual things like limiting & ordering etc

(sql/format
 (-> (sqlh/select :first_name :last_name :email)
     (sqlh/from :users)
     (sqlh/order-by :first_name)
     (sqlh/limit 10)))

Filtering

Filtering is just as simple and support the usual < > not in type expressions.

(sql/format
 (-> (sqlh/select :first_name :last_name :email)
     (sqlh/from :users)
     (sqlh/where [:= :first_name "spot"]
                 [:= :last_name "dog"])))

Often we want to conditionally filter, this is nice and simple with the knowledge that where will short circuit give nil.

So below no where will not be appended because true is not false so the when return nil which removes the where in the final query.

(sql/format
 (-> (sqlh/select :first_name :last_name :email)
     (sqlh/from :users)
     (sqlh/where (when (true? false) [:= :first_name "spot"]))))

Extending / joins

For all the standard fn's like select and where there are equivalent merge fn's the merge versions append in place of replacing.

A good strategy is to build basic queries extending them when needed.

we can use if when when-let cond-> to help build these.

(def base-sql
  (-> (sqlh/select :first_name :last_name :email)
      (sqlh/from :users)))

(defn user-search [{:keys [first-name last-name] :or {first-name nil last-name nil}}]
  (sql/format
   (-> base-sql
       (sqlh/select :first_name :last_name)
       (sqlh/merge-where (when first-name [:= :first_name first-name]))
       (sqlh/merge-where (when last-name [:= :last_name last-name])))))

(sql/format (user-search {:first-name "spot"}))

Now is a good time to explain aliasing, basically keywords become vectors so :first_name would become [:first_name :fn] to alias the column first_name to fn we can aliases columns tables sub select the lot like in standard sql.

(sql/format
 (-> base-sql
     (select [:first_name :fn] [:last_name :ln] [:email :e])))

We can also do joins to other table's

    (def base-sql
      (-> (sqlh/select :first_name :last_name :email)
          (sqlh/from :users)))

  (def base-join-sql
    (-> base-sql
      (sqlh/join [:address] [:= :users.address_id address.id])))

(sql/format base-join-sql)

or group by's and sql functions like count max min these can be used by appending :%name to the selected column.

(def base-group-sql
  (-> base-sql
      (sqlh/select :first_name [:%count.first_name :count_name])
      (sqlh/group :first_name)))

(sql/format base-group-sql)

Larger query

This is how I like to compose queries, and shows a larger query being generated.

(def big-base-sql
  (-> (sqlh/select :users.* :address.* :products.*)
      (sqlh/from :users)
      (sqlh/join :address [:= :users.address_id :address.id])
      (sqlh/join :products [:= :users.address_id :address.id])
      (sqlh/limit 100)))

(defn big-base-filters [filters]
  (-> big-base-sql
      (sqlh/merge-where
       (when (:first_name filters)
         [:= :first_name (:first_name filters)]))
      (sqlh/merge-where
       (when (:last_name filters)
         [:= :last_name (:last_name filters)]))
      (sqlh/merge-where
       (when (:product_name filters)
         [:= :product.name (:product_name filters)]))
      (sqlh/merge-where
       (when (:active filters)
         [:= :active (:active filters)]))))

(sql/format
 (big-base-filters
  {:first_name "spot"
   :last_name "dog"
   :product_name "lead"
   :active true}))

Don't forget its just data, if you don't use sql/format it just returns a data structure which you can build manually, or manipulate with the standard library.

 {:select (:first_name :last_name :email), :from (:users)}

Extending / raw sql

When all else fails you have a few options, check to see if there is a honeysql db specific library or break out sql/raw or extending honey sql.

Say we want to get people added in the last 14 days this is a bit more tricky

(def base-last-14-days-sql
  (-> base-sql
      (sqlh/where [:>
                   (sql/raw "created")
                   (sql/raw "CURRENT_DATE - INTERVAL '14' DAY")])))


(sql/format base-last-14-days-sql)