Clojure - variable names for database_column_names

This is "the most idiomatic question in Clojure."

I use Cassandra for my database, and Alia, as my Clojure driver (both Cassandra and Alia work phenomenally well - cannot be happier).

The problem is this: Cassandra uses underscores (not dashes) in column names, and Clojure prefers underscores. So the "user key" in Clojure is "user_key" in Kassandra. What is the best way to handle mapping Cassandra column names to Clojure variables?

Since I use prepared statements for my CQL queries, I think the fact that column names contain underscores rather than dashes is more than implementation details that need to be abstracted - I often put CQL queries as strings in my Clojure , and I consider it important to present CQL as it really is. I looked at approaches that automatically change dashes to emphasize query strings, so there is a version of Clojure CQL that maps to a version of Cassandra CQL, but this seems like an unacceptable level of abstraction. In addition, you will need to use underscores when you run CQL queries directly in Cassandra to troubleshoot, so you need to keep two different representations of the column names in your head. Sounds like the wrong approach.

The approach I got is to do a mapping in a destructive Clojure map, for example:

(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" is my convenient function, which simply looks at the CQL string on the map and uses the previously prepared statement, if present, or prepares the statement and forces it into map, and then executes the prepared statement and returns the first row of the result set or throws an exception if more than one row is returned).

if I use the more concise method {: keys []} destructuring, then I am stuck with underscores in Clojure variable names:

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

This was the first approach I tried, but it becomes ugly very fast, as variable names with underscores leak through the code and go head to head with dashes with dashes. Mixing.

You have run into this problem for a long time, doing a conversion on a destructuring map where Clojure "name-variable" and Cassandra "column_name" feel the best solution side by side. It also allows me to extend short_col_nms to more descriptive name variables when I want.

This has some similarities to the mapping that Clojure does to underline file names for dashes in namespaces, so there seems to be some use case for doing this mapping. In the case of a Clojure file / namespace name, automatic mapping is performed, so the direct analogue may be a version of the destructuring {: keys []} that displays a dash to underline.

I am a relative newbie to Clojure, so I understand that there may be better ways to do this. Hence my question.

One of the improvements that I have reviewed is the macro recording, which dynamically creates a destructuring map at compile time. But I do not know how to write a macro that works at the beginning of the compilation process.

+6
source share
5 answers

After upgrading to my Clojure macro-fu, the answer I found was to use a macro that performs destructuring, including conversion from snake_case to kebab-case, for me.

An additional benefit of using a macro is that I can also do a basic check of the time of my name and CQL column parameters. Validation is very simple, but it will catch 90% of the errors that I usually make.

Here is the macro. This macro only processes single-line results (which for me in Kassandra is more than 50% of cases). I will work on a separate set of macros to process results with multiple lines.

 (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, eg: {: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 '? 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 ))) 

and here is an example of using a macro:

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

and macro-instance-1 (minus 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]))]) 

here is another example with exiting 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> 

and macro instance 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"]))]) 
0
source

camel-snake-kebab has a nice clean interface for this kind of conversion.

From the examples:

 (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" 
+5
source

If you consider your data as a tree structure (from n levels), and you need to replace the underscore with a dash of a tree structure symbol, you can try to solve this function using the created library for: clojure.walk

In fact clojure.walk provides similar functionality to keywordize-keys

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

Then you only need 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 [[kv]] (if (string? k) [(clojure.string/replace k "_" "-") v] [kv]))] ;; only apply to maps (clojure.walk/postwalk (fn [x] (if (map? x) (into {} (map fx)) x)) m))) (underscore-to-dash-string-keys {"_a" 1 "_b" 2 "_c" 3}) => {"-a" 1, "-b" 2, "-c" 3} 

Related to this question: What is the best way to handle mapping Cassandra column names to Clojure variables? I think that is well discussed here. In Clojure, how to destroy all the keys of a card?

+2
source

You can bury the conversion between hyphens and underscores in your CQL and avoid the munging Clojure nightmare if you want to use quoted identifiers, especially if you use prepared statements with Alia, since Alia supports named parameter bindings for prepared statements , starting from version v2.6.0 .

If you look at the CQL grammar , you will see the very first line:

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

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

Some of these identifiers are reserved as keywords (SELECT, AS, IN, etc.)

However, there is another class of identifier β€” quoted β€” that can contain any character, including hyphens, and is never considered reserved.

There is a second type of identifier, called quoted identifier, defined by enclosing an arbitrary sequence of characters in double quotation marks ("). Quoted identifiers are never keywords

In Select syntax, you can select the AS field as an identifier.

selection-list :: = selector (AS identifier)

If you selected SELECT x AS for the cited identifier, you can translate underscores to hyphens:

i.e. "SELECT user_id AS \"user-id\" from a_table

Running this request through Alia will display a Clojure card with a key: user ID and some value.

Similarly, when performing an operation in which you want to bind a value to a parameter, the grammar:

variable :: = '?' | ID ':'

The variable can be anonymous (question mark (?)) Or named (identifier preceded by :). Both declare bind variables for prepared statements. ''

Although this may seem a little funky, CQL supports quoted binding parameters.

i.e.

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

or

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

Both of these requests made with Alia can be sent to a card containing: user-id, and the value will be correctly associated.

With this approach, you can deal with the translation of the hyphen / underscore entirely in CQL.

+1
source

you can also extend the protocol in hayt to encode identifiers as quoted values. But that would apply the change to all identifiers.

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

0
source

Source: https://habr.com/ru/post/958925/


All Articles