关于宏:database_column_names的clojure变量名称

clojure-variable-names for database_column_names

这是"在Clojure中最常见的问题"问题。

我将Cassandra用于我的数据库,并以Alia作为我的Clojure驱动程序(Cassandra和Alia的工作都非常好-不能再开心了)。

问题是这样的:Cassandra在列名中使用下划线(而不是破折号),而Clojure更喜欢使用破折号而不是下划线。因此Clojure中的"用户密钥"在Cassandra中为" user_key"。如何最好地处理Cassandra列名到Clojure变量的映射?

因为我正在对CQL查询使用准备好的语句,所以我认为列名包含下划线而不是破折号这一事实不仅仅是要提取的实现细节-我经常将CQL查询作为字符串放入我的Clojure代码中,我认为代表CQL的真实存在很重要。我曾考虑过将查询字符串中的破折号自动转换为下划线的方法,以便将CQL的Clojure版本映射到CQL的Cassandra版本,但这似乎是不合适的抽象级别。此外,当您直接在Cassandra中运行CQL查询以进行故障排除时,您仍然需要使用下划线,因此您需要在头部保留两种不同的列名表示形式。似乎是错误的方法。

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

1
2
(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变量名称中会使用下划线:

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

那是我尝试的第一种方法,但是很快变得很丑陋,因为带下划线的变量名会在代码中渗入,并与带破折号的变量并驾齐驱。令人困惑。

面对这个问题已经很长时间了,在销毁地图中进行转换,其中Clojure"变量名"和Cassandra" column_name"并排在一起感觉是最好的解决方案。我也可以根据需要将short_col_nms扩展为更具描述性的变量名称。

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

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

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


camel-snake-kebab对此类转换具有很好的简洁界面。

从示例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
(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"


如果您认为数据是一个树结构(n个级别),并且需要用"下划线"替换树结构键的"破折号"字符,那么可以尝试使用为以下内容设计的库来解决此功能:clojure 。步行

实际上clojure.walk带来了类似的功能keywordize-keys

1
2
3
4
5
6
7
(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 / replace函数的关键字函数

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
(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变量的映射?我认为在Clojure中如何对此进行了充分的讨论,如何使地图的所有键变形?


您可以使用加引号的标识符将连字符和下划线之间的转换隐藏在CQL中,从而避免梦the以求的Clojure关键字的噩梦,尤其是如果您使用带有Alia的预处理语句,因为从v2开始,Alia支持已命名语句的命名参数绑定.6.0。

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

identifier ::= any quoted or unquoted identifier, excluding reserved
keywords

An identifier is a token matching the regular expression [a-zA-Z][a-zA-Z0-9_]*

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

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

There is a second kind of identifiers called quoted identifiers defined by enclosing an arbitrary sequence of characters in double-quotes("). Quoted identifiers are never keywords

在"选择"语法中,可以选择一个字段作为标识符。

selection-list ::= selector (AS identifier)

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

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

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

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

variable ::= '?' | ':' identifier

A variable can be either anonymous (a question mark (?)) or named (an identifier preceded by :). Both declare a bind variables for prepared statements''

尽管看起来有些时髦,但CQL确实支持引用的绑定参数。

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

要么

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

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

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


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

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


升级到Clojure宏后,我发现的答案是使用一个对我进行解构的宏,包括从snake_case到kebab-case的转换。

使用宏的一个辅助优点是,我还可以对CQL列名和参数进行一些基本的编译时验证。验证是非常基本的,但是它将捕获我通常犯下的90%的头扇错误。

这是宏。此宏仅处理单行结果案例(对我而言,在Cassandra中占50%以上的案例)。我将处理一组单独的宏,以处理多行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(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)))

这是宏的使用示例:

1
2
3
4
5
(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):

1
2
3
4
5
6
(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输出的另一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
(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"]))])