246 lines
7.8 KiB
Org Mode
246 lines
7.8 KiB
Org Mode
#+TITLE: 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.
|
|
#+BEGIN_SRC clojure
|
|
(h/html [:span "bar"])
|
|
#+END_SRC
|
|
|
|
Attributes are added as a map of values styles are also a map
|
|
#+BEGIN_SRC clojure
|
|
(h/html [:span "bar" {:class "class1 class2" :title "my title" :style {:color "red"}}])
|
|
#+END_SRC
|
|
|
|
You can use shorthand to add id's and classes
|
|
#+BEGIN_SRC clojure
|
|
(h/html [:span#id.class1.class2 "bar"])
|
|
#+END_SRC
|
|
|
|
** 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.
|
|
|
|
|
|
#+BEGIN_SRC clojure
|
|
(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"}]))
|
|
#+END_SRC
|
|
|
|
You can use clojure core language to manipulate these lists.
|
|
|
|
place parts inside another containing element
|
|
#+BEGIN_SRC clojure
|
|
(h/html (into [:div.container] [[:span "span 1"] [:span "span 2"]]))
|
|
#+END_SRC
|
|
|
|
You could also use merge
|
|
#+BEGIN_SRC clojure
|
|
(h/html (merge [:span "span 1"] [:span "span 2"] [:span "span 3"]))
|
|
#+END_SRC
|
|
|
|
We can take advantage of lazyness if we like
|
|
#+BEGIN_SRC clojure
|
|
(h/html (take 2 (map navbar-link [{:href "link1" :title "title here"}
|
|
{:href "link2" :title nil}
|
|
{:href "link3" :text "link text"}
|
|
{:href "link4"}])))
|
|
#+END_SRC
|
|
|
|
|
|
* 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.
|
|
#+BEGIN_SRC clojure
|
|
(sql/format (sqlh/select :first_name :last_name :email))
|
|
#+END_SRC
|
|
|
|
You use multiple functions to combine the parts, below we add the missing from.
|
|
#+BEGIN_SRC clojure
|
|
|
|
(sql/format (sqlh/from
|
|
(sqlh/select :first_name :last_name :email)
|
|
:users))
|
|
#+END_SRC
|
|
|
|
|
|
How ever it's much nicer for readability to use the threading macro
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> (sqlh/select :first_name :last_name :email)
|
|
(sqlh/from :users)))
|
|
#+END_SRC
|
|
|
|
The functions understand ordering so your order does not matter.
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> (sqlh/from :users)
|
|
(sqlh/select :first_name :last_name :email)))
|
|
#+END_SRC
|
|
|
|
|
|
We can do the usual things like limiting & ordering etc
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> (sqlh/select :first_name :last_name :email)
|
|
(sqlh/from :users)
|
|
(sqlh/order-by :first_name)
|
|
(sqlh/limit 10)))
|
|
#+END_SRC
|
|
|
|
|
|
** Filtering
|
|
|
|
Filtering is just as simple and support the usual < > not in type expressions.
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> (sqlh/select :first_name :last_name :email)
|
|
(sqlh/from :users)
|
|
(sqlh/where [:= :first_name "spot"]
|
|
[:= :last_name "dog"])))
|
|
#+END_SRC
|
|
|
|
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.
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> (sqlh/select :first_name :last_name :email)
|
|
(sqlh/from :users)
|
|
(sqlh/where (when (true? false) [:= :first_name "spot"]))))
|
|
#+END_SRC
|
|
|
|
|
|
** 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.
|
|
|
|
#+BEGIN_SRC clojure
|
|
(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"}))
|
|
#+END_SRC
|
|
|
|
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.
|
|
#+BEGIN_SRC clojure
|
|
(sql/format
|
|
(-> base-sql
|
|
(select [:first_name :fn] [:last_name :ln] [:email :e])))
|
|
#+END_SRC
|
|
|
|
|
|
We can also do joins to other table's
|
|
#+BEGIN_SRC clojure
|
|
(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)
|
|
#+END_SRC
|
|
|
|
or group by's and sql functions like =count= =max= =min= these can be used by appending :%name to the selected column.
|
|
|
|
#+BEGIN_SRC clojure
|
|
(def base-group-sql
|
|
(-> base-sql
|
|
(sqlh/select :first_name [:%count.first_name :count_name])
|
|
(sqlh/group :first_name)))
|
|
|
|
(sql/format base-group-sql)
|
|
#+END_SRC
|
|
|
|
** Larger query
|
|
This is how I like to compose queries, and shows a larger query being generated.
|
|
#+BEGIN_SRC clojure
|
|
(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}))
|
|
#+END_SRC
|
|
|
|
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.
|
|
|
|
#+BEGIN_EXAMPLE
|
|
{:select (:first_name :last_name :email), :from (:users)}
|
|
#+END_EXAMPLE
|
|
|
|
** 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
|
|
#+BEGIN_SRC clojure
|
|
(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)
|
|
#+END_SRC
|