Macros 数据库列名称的clojure变量名称

Macros 数据库列名称的clojure变量名称,macros,clojure,cassandra,destructuring,Macros,Clojure,Cassandra,Destructuring,这是一个“Clojure中最惯用的是什么”的问题 我将Cassandra用于我的DB,Alia作为我的Clojure驱动程序(Cassandra和Alia都工作得非常好,再高兴不过了) 问题在于:Cassandra在列名中使用下划线(而不是破折号),Clojure更喜欢破折号而不是下划线。所以Clojure中的“用户密钥”在Cassandra中是“用户密钥”。如何最好地处理Cassandra列名到Clojure变量的映射 因为我在CQL查询中使用准备好的语句,所以我认为列名包含下划线而不是破折号

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

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

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

因为我在CQL查询中使用准备好的语句,所以我认为列名包含下划线而不是破折号这一事实不仅仅是一个需要抽象的实现细节——我经常将CQL查询作为字符串放入Clojure代码中,我认为按实际情况表示CQL很重要。我已经考虑过将破折号自动神奇地转换为查询字符串中的下划线的方法,因此有一个Clojure版本的CQL映射到Cassandra版本的CQL,但这似乎是一个不合适的抽象级别。此外,在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”并排在一起感觉是最好的解决方案。它还允许我在需要时将短列扩展到更具描述性的变量名

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

我是Clojure的新手,所以我意识到可能有更好的方法。这就是我的问题


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

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

实际上,clojure.walk提供了类似的功能

然后只需更改clojure.string/函数的函数

这就是结果:

(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变量的映射?我认为这里讨论得很好

对于此类转换有一个很好的干净接口

从示例中可以看出:

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

升级我的Clojure宏fu后,我找到的答案是使用一个宏进行解构,包括从snake_案例到kebab案例的转换

使用宏的一个辅助优势是,我还可以对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))
和宏扩展-1(减去println):

下面是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"]))])

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


请参见

您可以在CQL中隐藏连字符和下划线之间的转换,如果您想使用带引号的标识符,尤其是如果您在Alia中使用已准备好的语句(自Alia v2.6.0以来),可以避免咀嚼Clojure关键字的噩梦

如果你看一下,你会注意到第一行:

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

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

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

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

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

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

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

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

i、 e.<公司
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> 
(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"]))])
INSERT into a_table (user_id) VALUES (:\"user-id\")
SELECT * from a_table WHERE user_id = :\"user-id\"