#+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-by :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/where (when (:first_name filters) [:= :first_name (:first_name filters)])) (sqlh/where (when (:last_name filters) [:= :last_name (:last_name filters)])) (sqlh/where (when (:product_name filters) [:= :product.name (:product_name filters)])) (sqlh/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 [:> [:raw "created"] [:raw "CURRENT_DATE - INTERVAL '14' DAY"]]))) (sql/format base-last-14-days-sql) #+END_SRC