升级到我的 Clojure 宏 fu 后,我找到的答案是使用一个宏来进行解构,包括从 snake_case 到 kebab-case 的转换,对我来说。
使用宏的一个辅助优势是我还可以对我的 CQL 列名称和参数进行一些基本的编译时验证。验证是非常基础的,但它会捕获我通常犯的 90% 的令人头疼的错误。
这是宏。这个宏只处理单行结果案例(对我来说,在 Cassandra 中超过 50% 的案例)。我将使用一组单独的宏来处理多行结果。
(defmacro with-single-row-cql-selects
"given a vector of one or more maps of the form:
{:bindings [title doc-key version]
:cql \"SELECT * from dtl_blog_entries where blog_key=? and n=?\"
:params [ blog-key (int n) ]}
evaluates body with the symbols in :bindings bound to the results of the CQL in :cql executed with the params in :params
the CQL should be 'single-row' CQL that returns only one row. in any case, the macro will take only the first row of the results1
notes:
1) the macro handles the conversion from kebab-case (Clojure) to snake_case (Cassandra) automagically. specify your bindings using camel-case
2) to bind to a different symbol than the variable name, use the form symbol-name:column-name in the bindings vector, e.g.:
{:bindings [blog-name:title]
:cql \"select title from dtl_blogs where blog_key=? and comm_key=? and user_key=?\"
:params [ blog-key comm-key user-key]}
3) the macro will do very basic compile-time checking of your cql, including
a) validating that you have the same number of '?'s in your cql as params
b) validating that the column names corresponding to the bindings are present in the CQL (or that this is a 'select *' query)
"
[select-bindings & body]
(let [let-bindings#
(into []
(letfn ((make-vec#
;; puts a single element into a vector, passes a vector straight through, and complains if v is some other kind of collection
[v#]
(cond
;; missing, just use an empty vector
(not v#) []
(vector? v#) v#
(coll? v#) (throw (IllegalArgumentException. (str v# " should be a vector")))
:else [v#])))
(apply concat
(for [{:keys [cql params bindings]} select-bindings]
(let [vec-bindings# (make-vec# bindings)
vec-params# (make-vec# params)
binding-names# (map #(-> % name (clojure.string/split #":" ) first symbol) vec-bindings#)
col-names# (map #(-> (or (-> % name (clojure.string/split #":" ) second ) %)
(clojure.string/replace \- \_) ) vec-bindings#)
destructuring-map# (zipmap binding-names# (map keyword col-names#))
fn-call# `(first (prep-and-exec ~cql ~vec-params#))]
;; do some *very basic* validation to catch the some common typos / head slappers
(when (empty? vec-bindings#)
(throw (IllegalArgumentException. "you must provide at least one binding")))
;; check that there are as many ?s as there are params
(let [cql-param-count (count (re-seq #"\?" cql))]
(when (not= cql-param-count (count vec-params#))
(throw (IllegalArgumentException. (str "you have " cql-param-count
" param placeholders '?' in your cql, but "
(count vec-params#) " params defined; cql: " cql ", params:" vec-params#)))))
;; validate that the col-names are present
(when (empty? (re-seq #"(?i)\s*select\s+\*\s+from" cql)) ;; if a 'select *' query, no validation possible
(doseq [c col-names#]
(when (empty? (re-seq (re-pattern (str "[\\s,]" c "[\\s,]")) cql))
(throw (IllegalArgumentException. ( str "column " c " is not present in the CQL"))))))
[destructuring-map# fn-call#])))))]
`(let ~let-bindings#
~@body)))
这是宏的使用示例:
(conn/with-single-row-cql-selects
[{:bindings [blog-title]
:cql "select blog_title from dtl_blogs where blog_key=? and comm_key=? and user_key=?"
:params [ blog-key comm-key user-key]}]
(println "blog title is " blog-title))
和宏expand-1(减去println):
(clojure.core/let [{blog-title :blog_title} (clojure.core/first
(dreamtolearn.db.conn/prep-and-exec
"select blog_title from dtl_blogs where blog_key=? and comm_key=? and user_key=?"
[blog-key
comm-key
user-key]))])
这是 REPL 输出的另一个示例:
dreamtolearn.db.conn> (with-conn
(with-single-row-cql-selects
[{:cql "select * from dtl_users limit 1"
:bindings [user-key name date-created]}
{:cql "select badges,founder_user_key,has_p_img from dtl_communities where comm_key=?"
:bindings [badges founder-user-key has-profile-image:has-p-img]
:params "5LMO8372ZDKHF798RKGNA57O3"}]
(println "user-key: " user-key " name: " name " date-created: " date-created " badges: " badges
" founder-user-key: " founder-user-key " has-profile-image: " has-profile-image)))
user-key: 9MIGXXW2QJWPGL0WJL4X0NGWX name: Fred Frennant date-created: 1385131440791 badges: comm-0 founder-user-key: F2V3YJKBEDGOLLG11KTMPJ02QD has-profile-image: true
nil
dreamtolearn.db.conn>
和宏expand-1:
(clojure.core/let [{date-created :date_created,
name :name,
user-key :user_key} (clojure.core/first
(dreamtolearn.db.conn/prep-and-exec
"select * from dtl_users limit 1"
[]))
{has-profile-image :has_p_img,
founder-user-key :founder_user_key,
badges :badges} (clojure.core/first
(dreamtolearn.db.conn/prep-and-exec
"select badges,founder_user_key,has_p_img from dtl_communities where comm_key=?"
["5LMO8372ZDKHF798RKGNA57O3"]))])