6

这是一个“什么是 Clojure 中最惯用的”问题。

我将 Cassandra 用于我的数据库,并将 Alia 作为我的 Clojure 驱动程序(Cassandra 和 Alia 都工作得非常好——再高兴不过了)。

问题是这样的:Cassandra 在列名中使用下划线(不是破折号),而 Clojure 更喜欢破折号而不是下划线。所以 Clojure 中的“user-key”是 Cassandra 中的“user_key”。如何最好地处理 Cassandra 列名到 Clojure 变量的映射?

因为我为我的 CQL 查询使用准备好的语句,所以我认为列名包含下划线而不是破折号这一事实不仅仅是要抽象出来的实现细节——我经常将 CQL 查询作为字符串放入我的 Clojure 代码中,而且我认为按实际情况表示 CQL 很重要。我已经考虑过在查询字符串中自动将破折号转换为下划线的方法,因此有一个 Clojure 版本的 CQL 映射到 CQL 的 Cassandra 版本,但这似乎是一个不适当的抽象级别。此外,当您直接在 Cassandra 中运行 CQL 查询以进行故障排除时,您仍然需要使用下划线,因此您需要在脑海中保留两种不同的列名表示。似乎是错误的方法。

我最终采用的方法是在 Clojure 解构映射中执行映射,如下所示:

(let [{user-key :user_key, user-name :user_name} 
    (conn/exec-1-row-ps "select user_key,user_name from users limit 1")] )

(“conn/exec-1-row-ps”是我的便利函数,它只是在映射中查找 CQL 字符串,并使用先前准备的语句(如果存在),或者准备语句并将其存储在映射中,并且然后执行准备好的语句并返回结果集的第一行,如果返回多行则抛出异常)。

如果我使用更简洁的 {:keys []} 解构方法,那么我的 Clojure 变量名中就会出现下划线:

(let [{:keys [user_key user_name]} ...

这是我尝试的第一种方法,但它变得非常难看,因为带有下划线的变量名渗透到代码中,并与带有破折号的变量名正面交锋。令人困惑。

长期以来一直面临这个问题,在解构映射中进行转换,其中 Clojure "variable-name" 和 Cassandra "column_name" 并排感觉是最好的解决方案。它还允许我在需要时将 short_col_nms 扩展为更具描述性的变量名称。

这与 Clojure 将文件名中的下划线映射到命名空间中的破折号有一些相似之处,因此感觉像这样进行映射有一些先例。在文件名/命名空间的情况下,Clojure 自动进行映射,因此直接模拟可能是 {:keys []} 解构的一个版本,它将破折号映射到下划线。

我是 Clojure 的新手,所以我意识到可能有更好的方法来做到这一点。因此我的问题。

我考虑过的一项改进是编写一个在编译时动态构建解构映射的宏。但我不知道如何编写一个在编译过程早期运行的宏。

4

5 回答 5

5

camel-snake-kebab对这类转换有一个非常干净的界面。

从例子:

(use 'camel-snake-kebab)

(->CamelCase 'flux-capacitor)
; => 'FluxCapacitor

(->SNAKE_CASE "I am constant")
; => "I_AM_CONSTANT"

(->kebab-case :object_id)
; => :object-id

(->HTTP-Header-Case "x-ssl-cipher")
; => "X-SSL-Cipher"
于 2013-11-28T08:47:12.527 回答
2

If you think your data as a tree structure (of n levels) and you need to replace the "underscore" for the "dash" character of the tree structure's keys, then you can try to solve this functionality using the library designed for: clojure.walk

Actually clojure.walk brings a similar functionality keywordize-keys

(defn keywordize-keys
  "Recursively transforms all map keys from strings to keywords."
  {:added "1.1"}
  [m]
  (let [f (fn [[k v]] (if (string? k) [(keyword k) v] [k v]))]
    ;; only apply to maps
    (postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))

Then you only have to change the keyword function for the clojure.string/replace function

and this is the result:

(defn underscore-to-dash-string-keys
  "Recursively transforms all map keys from strings to keywords."
  {:added "1.1"}
  [m]
  (let [f (fn [[k v]] (if (string? k) [(clojure.string/replace k "_" "-") v] [k v]))]
    ;; only apply to maps
    (clojure.walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))


(underscore-to-dash-string-keys {"_a" 1 "_b" 2 "_c" 3})

=> {"-a" 1, "-b" 2, "-c" 3}

Related to this question : How best to handle the mapping of Cassandra column names to Clojure variables? I think it is well discussed here In Clojure, how to destructure all the keys of a map?

于 2013-11-27T16:03:06.103 回答
1

您可以在 CQL 中隐藏连字符和下划线之间的转换,如果您想通过使用带引号的标识符来避免混淆 Clojure 关键字的噩梦,特别是如果您将准备好的语句与 Alia 一起使用,因为 Alia从 v2开始支持准备好的语句的命名参数绑定.6.0。

如果您查看CQL 语法,您会注意到第一行:

标识符 ::= 任何带引号或不带引号的标识符,不包括保留关键字

标识符是匹配正则表达式 [a-zA-Z][a-zA-Z0-9_]* 的标记

其中一些标识符被保留为关键字(SELECT、AS、IN 等)

但是,还有另一类标识符 - 带引号的- 它可以包含任何字符,包括连字符,并且永远不会被视为保留。

还有第二种标识符称为带引号的标识符,通过将任意字符序列括在双引号(“)中来定义。带引号的标识符永远不是关键字

在 Select 语法中,您可以选择将字段选择为标识符。

选择列表 ::= 选择器(AS 标识符)

如果您选择 SELECT x 作为带引号的标识符,您可以将下划线转换为连字符:

IE"SELECT user_id AS \"user-id\" from a_table

通过 Alia 执行该查询将生成一个具有键 :user-id 和一些值的 Clojure 映射。

同样,在执行要将值绑定到参数的操作时,语法:

变量 ::= '?' | ':' 标识符

变量可以是匿名的(问号 (?))或命名的(以: 开头的标识符)。两者都为准备好的语句声明了一个绑定变量''

虽然它可能看起来有点时髦,但 CQL 确实支持带引号的绑定参数。

IE

INSERT into a_table (user_id) VALUES (:\"user-id\")

或者

SELECT * from a_table WHERE user_id = :\"user-id\"

使用 Alia 执行的这两个查询都可以传递一个包含 :user-id 的映射,并且该值将被正确绑定。

使用这种方法,您可以完全在 CQL 中处理连字符/下划线翻译。

于 2015-10-21T12:25:46.423 回答
0

您还可以在 hayt 中扩展协议以强制将标识符编码为引用值。但这会将更改应用于所有标识符。

https://github.com/mpenet/hayt/blob/master/src/clj/qbits/hayt/cql.clj#L87

于 2014-12-31T18:05:38.557 回答
0

升级到我的 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"]))])
于 2013-12-05T09:51:01.470 回答