database_column_names的clojure-variable-names

时间:2013-11-27 13:16:09

标签: macros clojure cassandra destructuring

这是“Clojure中最具惯用性”的问题。

我正在使用Cassandra作为我的数据库,Alia作为我的Clojure司机(Cassandra和Alia工作得非常好 - 不能更快乐)。

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

因为我正在为我的CQL查询使用预准备语句,所以我认为列名包含下划线而不是破折号不仅仅是一个要抽象的实现细节 - 我经常将CQL查询作为字符串放入我的Clojure代码,我认为实际代表CQL很重要。我已经考虑了在查询字符串中自动将短划线转换为下划线的方法,因此有一个Clo的Clojure版本被映射到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字符串,并使用先前准备的语句(如果存在),或者准备语句并将其存储在map,然后执行预准备语句并返回结果集的第一行,如果返回多行,则抛出异常。)

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

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

这是我尝试的第一种方法,但它变得很难看,因为带有下划线的变量名称会渗透代码,并与带有破折号的变量名称正面交锋。混乱。

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

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

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

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

5 个答案:

答案 0 :(得分: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"

答案 1 :(得分:2)

如果您认为您的数据是树结构(n级)并且您需要替换树结构键的“破折号”字符的“下划线”,那么您可以尝试使用设计的库来解决此功能for:clojure.walk

实际上 clojure.walk 带来了类似的功能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)))

然后你只需要更改clojure.string / keyword函数的replace函数

这就是结果:

(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}

与此问题相关:如何最好地处理Cassandra列名称到Clojure变量的映射?我认为这里讨论得很好In Clojure, how to destructure all the keys of a map?

答案 2 :(得分:1)

你可以在你的CQL中隐藏连字符和下划线之间的转换,如果你想通过使用带引号的标识符,特别是如果你正在使用Alia的预备语句,那么就可以避免重复使用Clojure关键字,因为Alia supports named parameter bindings for prepared statements自从V2.6.0。

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

  

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

     

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

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

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

  

第二种称为引用标识符的标识符是通过用双引号(")包含任意字符序列来定义的。带引号的标识符绝不是关键字

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

  

selection-list :: =选择器(AS标识符)

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

即。 "SELECT user_id AS \"user-id\" from a_table

通过Alia执行该查询将导致带有键的Clojure映射:user-id和某个值。

类似地,当执行要将值绑定到参数的操作时,语法:

  

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

     

变量可以是匿名(问号(?))或命名(前面带有:)的标识符。两者都为预准备语句声明了绑定变量''

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

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

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

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

使用这种方法,您可以完全用CQL处理连字符/下划线转换。

答案 3 :(得分:0)

在升级到我的Clojure宏功能之后,我发现的答案是使用一个进行解构的宏,包括从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))

和macroexpand-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> 

和macroexpand-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"]))])

答案 4 :(得分:0)

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

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