连接数据库 in clojure ---korma

来源:互联网 发布:iphone蜂窝数据用户名 编辑:程序博客网 时间:2024/06/12 19:45

前言:目前本文写的一塌糊涂,还在更新中。。。
先扔上两个链接:http://sqlkorma.com/和https://github.com/korma/Korma/blob/master/src/korma
本文会先从数据库连接池和korma/db的角度入手讲起,最后才讲具体的用法。

首先什么是连接池?

In software engineering, a connection pool is a cache of database connections maintained so that the connections can be reused when future requests to the database are required.[citation needed] Connection pools are used to enhance the performance of executing commands on a database. Opening and maintaining a database connection for each user, especially requests made to a dynamic database-driven website application, is costly and wastes resources. In connection pooling, after a connection is created, it is placed in the pool and it is used again so that a new connection does not have to be established. If all the connections are being used, a new connection is made and is added to the pool. Connection pooling also cuts down on the amount of time a user must wait to establish a connection to the database.

在软件工程中(编程中使用到的技巧),连接池就是数据库连接(s)的cache.

于是这里有一个问题,连接数据库需要有什么消耗呢?
1.创建一个连接是不是需要花时间呢?–》(时间虽然不多,但是有的情况下还是消耗挺大的)
2.关闭一个连接也很花时间?–》(对程序的开销还是有的)
3.过多的连接太消耗内存了,所以只使用最大一定限度的连接数目,过多的用户需要等待?–》可以创建新的连接

那么连接怎么创建?
数据库连接使用TCP/http协议?

MySQL短连接每次请求操作数据库都需要建立与MySQL服务器建立TCP连接,这是需要时间开销的。 TCP/IP套接字连接方式是MySQL在任何平台都提供的一种连接方式,也是网络中使用最多的一种方式。这种方式在TCP/IP连接上建立一个基于网络的连接请求,一般情况下客户端在一台服务器上,而MySQL实例在另外一台服务器上,这两台机器通过TCP/IP网络连接。

在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。
但从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

现在继续回到连接池的话题上面来,
korma中使用的数据库连接池是c3p0,C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等

Java 数据库连接,(Java Database
Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法.

The Java Naming and Directory Interface (JNDI) is a Java API for
a directory service that allows Java software clients to discover and
look up data and objects via a name

korma还必须要引入com.mchange.v2.c3p0.ComboPooledDataSource才创建连接池。

(defn connection-pool  "Create a connection pool for the given database spec."  [{:keys [connection-uri subprotocol subname classname           excess-timeout idle-timeout           initial-pool-size minimum-pool-size maximum-pool-size           test-connection-query           idle-connection-test-period           test-connection-on-checkin           test-connection-on-checkout]    :or {excess-timeout (* 30 60)         idle-timeout (* 3 60 60)         initial-pool-size 3         minimum-pool-size 3         maximum-pool-size 15         test-connection-query nil         idle-connection-test-period 0         test-connection-on-checkin false         test-connection-on-checkout false}    :as spec}]  (when-not c3p0-enabled?    (throw (Exception. "com.mchange.v2.c3p0.ComboPooledDataSource not found in class path.")))  {:datasource (doto (resolve-new ComboPooledDataSource)                 (.setDriverClass classname)                 (.setJdbcUrl (or connection-uri (str "jdbc:" subprotocol ":" subname)))                 ...

这就是c3p0的ComboPooledDataSource类创建连接池的方法,在java中也是这样实现的,比如:

   ComboPooledDataSource ds = new ComboPooledDataSource();        String key = "jdbc." + dbname;        ds.setDriverClass(main.config(key + ".driver"));        ds.setJdbcUrl(main.config(key + ".url"));        ds.setUser(main.config(key + ".username"));        ds.setPassword(main.config(key + ".password"));        ds.setIdleConnectionTestPeriod(Integer.parseInt(main.config(key + ".idleConnectionTestPeriod")));        ds.setPreferredTestQuery("SELECT 1");

连接池就是这么创建的,现在回到korma db的实现上来,解决三个问题:怎么定义db?怎样创建连接?怎样获取连接?这三个问题解决后,korma db方面的问题也就都不是问题了。

怎么定义db?

这个问题主要涉及几个方面的问题,是怎样的db?mysql,pgsql,还是别的?然后host和端口号,数据库名,之后还有用户名密码,以及一些其他的配置参数。

(defn create-db  "Create a db connection object manually instead of using defdb. This is often   useful for creating connections dynamically, and probably should be followed   up with:   (default-connection my-new-conn)   If the spec includes `:make-pool? true` makes a connection pool from the spec."  [spec]  (let [spec (complete-spec spec)]    {:pool    (if (:make-pool? spec)                (delay-pool spec)                spec)     :options (extract-options spec)}))

那么,完整的解析spec数据库文件的过程是怎样的?

(defn- complete-spec [{:keys [connection-uri subprotocol] :as spec}]  (if-let [uri-or-subprotocol (or connection-uri subprotocol)]    (let [lookup-key (first (drop-while #{"jdbc"} (clojure.string/split uri-or-subprotocol #":")))]      (merge        {:classname  (subprotocol->classname lookup-key)         :make-pool? true}        (subprotocol->options lookup-key)        spec))    spec))

子协议

(defn extract-options [{:keys [naming                               delimiters                               alias-delimiter]}]  {:naming (->naming naming)   :delimiters (->delimiters delimiters)   :alias-delimiter (->alias-delimiter alias-delimiter)})

所以这个过程其实是会创建连接池的。

怎么创建连接?

使用defdb会在create-db后调用default-connection自动创建连接:

(defonce _default (atom nil))(defn default-connection  "Set the database connection that Korma should use by default when no  alternative is specified."  [conn]  (reset! _default conn))(defmacro defdb  "Define a database specification. The last evaluated defdb will be used by  default for all queries where no database is specified by the entity."  [db-name spec]  `(let [spec# ~spec]     (defonce ~db-name (create-db spec#))     (default-connection ~db-name)))

这个连接其实就是db-name。
另外,顺便在这里,理解一下事务的隔离级别,jdbc创建连接的时候会初始化设置。

事务

我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在
那么什么是事务的加锁和解锁?
据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作

要有事务的隔离级别?

这是为了保证有效的读取数据,隔离级别只是为了保证读取数据而已。。。写的话不存在这方面的顾虑?。(in update—-或者这部分太长,会抽离)

现在,进入第三个问题,

怎样获取连接?

数据库在执行各种操作的时候都是要获取连接的。

(defn get-connection  "Get a connection from the potentially delayed connection object."  [db]  (let [db (or (:pool db) db)]    (if-not db      (throw (Exception. "No valid DB connection selected."))      (if (delay? db)        @db        db))))

如果有连接池就从连接池中获取一个连接,如果没有连接则爆出异常。
比如?

(defmacro with  "在指定db的环境下执行操作. 当前连接从db的连接池中获取.  (with erpdb ...)"  [db & body]  `(if (and (= kd/*current-db* ~db)            (some? kd/*current-conn*))     ~@body     (kd/with-db ~db ~@body)))
do-query (defn do-query [{:keys [db] :as query}]  (if *current-conn*    (exec-sql query)    (with-db (or db @_default)      (exec-sql query))))

korma.core

另外,增删改查过程中的一些api

(defn select*  "Create a select query with fields provided in Ent.  If fields are not provided,  create an empty select query. Ent can either be an entity defined by defentity,  or a string of the table name"  [ent]  (let [default-fields (not-empty (:fields ent))]    (make-query ent {:type :select                     :fields (or default-fields [::*])                     :from [(:ent this-query)]                     :modifiers []                     :joins []                     :where []                     :order []                     :aliases #{}                     :group []                     :results :results})))

调用了make-query函数,这说明make-query函数在这里非常的通用?
make-query是一个宏

(defmacro ^{:private true} make-query [ent m]  `(let [ent# ~ent]     (if (:type ent#)       ent#       (let [~'this-query (empty-query ent#)]         (merge ~'this-query ~m)))))

接受两个参数ent 和m。如果ent中存在:type键值对,则返回ent,否则将ent和m进行某些方式的合并。

那么什么是ent呢?

Ent can either be an entity defined by defentity,
or a string of the table name。

select*在这里只是返回了一个map,没有做任何事情,需要调用select* 或者exec才可以完成实际的查询(do something really later,带* 的都是想达到这样的效果)。

(defmacro select  "Creates a select query, applies any modifying functions in the body and then  executes it. `ent` is either a string or an entity created by defentity.  ex: (select user        (fields :name :email)        (where {:id 2}))"  [ent & body]  (make-query-then-exec #'select* body ent))

select使用到了make-query-then-exec这个宏,以及#’select*,#’的解释我在《clojure知识点梳理》中有提到,

get Var object instead of the value of a symbol (var-quote)

(defn- make-query-then-exec [query-fn-var body & args]  `(let [query# (-> (~query-fn-var ~@args)                    ~@body)]     (exec query#)))

这里#’select*也就是宏里面所谓的query-fn-var。
然后这里最重要的其实就是这个exec啦,其实我一般也是使用select*+exec的语法。
先看看exec的具体实现:

(defn exec  "Execute a query map and return the results."  [query]  (let [query (apply-prepares query)        query (bind-query query (eng/->sql query))        sql (:sql-str query)        params (:params query)]    (cond      (:sql query) sql      (= *exec-mode* :sql) sql      (= *exec-mode* :query) query      (= *exec-mode* :dry-run) (do                                 (println "dry run ::" sql "::" (vec params))                                 (let [fk-key (->> query :ent :rel vals                                                   (map deref)                                                   (filter (comp #{:belongs-to} :rel-type))                                                   (map :fk-key))                                       pk-key (-> query :ent :pk)                                       result-keys (concat                                                     pk-key                                                     (apply concat fk-key))                                       results (apply-posts query [(zipmap result-keys (repeat 1))])]                                   (first results)                                   results))      :else (let [results (db/do-query query)]              (apply-transforms query (apply-posts query results))))))

那么db/do-query的实现是怎样的?

(defn- exec-sql [{:keys [results sql-str params]}]  (let [keys (get-in *current-db* [:options :naming :keys])]    (case results      :results (jdbc/query *current-conn*                           (apply vector sql-str params)                           :identifiers keys)      :keys (jdbc/db-do-prepared-return-keys *current-conn* sql-str params)      (jdbc/db-do-prepared *current-conn* sql-str params))))(defmacro with-db  "Execute all queries within the body using the given db spec"  [db & body]  `(jdbc/with-db-connection [conn# (korma.db/get-connection ~db)]     (binding [*current-db* ~db               *current-conn* conn#]       ~@body)))(defn do-query [{:keys [db] :as query}]  (if *current-conn*    (exec-sql query)    (with-db (or db @_default)      (exec-sql query))))

其实调用的都是jdbc的查询函数。

这里对结果稍微做了一点处理:

:else (let [results (db/do-query query)]              (apply-transforms query (apply-posts query results)))

apply-transforms对返回的结果做了一丢丢变换,如果是update和delete则不变,如果是查询和新增,会对结果对一些处理:

(defn- apply-transforms  [query results]  (if (#{:delete :update} (:type query))    results    (if-let [trans (-> query :ent :transforms seq)]      (let [trans-fn (apply comp trans)]        (if (sequential? results)          (map trans-fn results)          (trans-fn results)))      results)))

虽然不知道:transforms在这里的作用,有什么别有用心?

下面再讲讲with,这个对于关系数据库来说还是很重要的,这个会和defentity相关。

(defn create-entity  "Create an entity representing a table in a database."  [table]  ^{:type ::Entity}  {:table table   :name table   :pk '(:id)   :db nil   :transforms '()   :prepares '()   :fields []   :rel {}})(defmacro defentity  "Define an entity representing a table in the database, applying any modifications in  the body."  [ent & body]  `(let [e# (-> (create-entity ~(name ent))                ~@body)]     (def ~ent e#)))

比如我这么定义一个实体:

(defentity Busmodel           (table :bas_cmdb_busmodel)           (pk :Id)           (has-many ResourcePlan {:fk :Bussiness3Id})           (has-many ResPlanApproveFlows {:fk :Bussiness3Id})           (has-one ViewResPlanApproveFlows {:fk :PlanProductId})           (has-many ViewResourcePlan {:fk :Bussiness3Id}))

这里比如table和pk函数的实现如下:

(defn table  "Set the name of the table and an optional alias to be used for the entity.  By default the table is the name of entity's symbol."  [ent t & [alias]]  (let [tname (if (or (keyword? t)                      (string? t))                (name t)                (if alias                  t                  (throw (Exception. "Generated tables must have aliases."))))        ent (assoc ent :table tname)]    (if alias      (assoc ent :alias (name alias))      ent)))(defn pk  "Set the primary key used for an entity. :id by default."  ([ent & pk]   (assoc ent :pk (map keyword pk))))

基本上都是使用assoc函数来实现的。那么has-many呢?

(defmacro has-many  "Add a has-many relation for the given entity. It is assumed that the foreign key   is on the sub-entity with the format table_id: user.id = email.user_id   Can optionally pass a map with a :fk key collection or use the helper `fk` function   to explicitly set the foreign key.   (has-many users email)   (has-many users email (fk :emailID))"  ([ent sub-ent]   `(has-many ~ent ~sub-ent nil))  ([ent sub-ent opts]   `(rel ~ent (var ~sub-ent) :has-many ~opts)))

这个macro给一个指定的实体增加一个一对多的关系,

看看应该怎么定义两个实体间的关系吧:

(defn rel [ent sub-ent type opts]  (let [var-name (-> sub-ent meta :name)        var-ns (-> sub-ent meta :ns)]    (assoc-in ent [:rel (name var-name)]              (delay               (let [resolved (ns-resolve var-ns var-name)                     sub-ent (when resolved (deref sub-ent))]                 (when-not (map? sub-ent)                   (throw (Exception. (format "Entity used in relationship does not exist: %s" (name var-name)))))                 (create-relation ent sub-ent type opts))))))

至于这关系怎么创建,还是得慢慢道来~还真有点点复杂。

(defn- db-keys-and-foreign-ent [type ent sub-ent opts]  (case type    :many-to-many        [(many-to-many-keys ent sub-ent opts) sub-ent]    (:has-one :has-many) [(get-db-keys ent sub-ent opts) sub-ent]    :belongs-to          [(get-db-keys sub-ent ent opts) ent]))(defn create-relation [ent sub-ent type opts]  (let [[db-keys foreign-ent] (db-keys-and-foreign-ent type ent sub-ent opts)        fk-override (when-let [fk-key (:fk opts)]                      {:fk (to-raw-key foreign-ent fk-key)                       :fk-key fk-key})]    (merge {:table (:table sub-ent)            :alias (:alias sub-ent)            :rel-type type}           db-keys           fk-override)))

create-relation调用 (db-keys-and-foreign-ent type ent sub-ent opts)来得到一个数组[db-keys foreign-ent],为什么要得到这样一个数组呢?db-keys有什么用?foreign-ent有什么用?
db-keys-and-foreign-ent调用下面这些函数都可以得到db-keys:

(defn- to-raw-key [ent k]  (map #(raw (eng/prefix ent %)) k))(defn- many-to-many-keys [parent child {:keys [join-table lfk rfk]}]  {:lpk (to-raw-key parent (:pk parent))   :lfk (delay (to-raw-key {:table (name join-table)} @lfk))   :rfk (delay (to-raw-key {:table (name join-table)} @rfk))   :rpk (to-raw-key child (:pk child))   :join-table join-table})(defn- get-db-keys [parent child {:keys [pk fk]}]  (let [pk-key (or pk (:pk parent))        fk-key (or fk (default-fk-name parent))]    {:pk (to-raw-key parent pk-key)     :fk (to-raw-key child fk-key)     :fk-key fk-key}))

从这两个函数可以看出:db-keys就是一个有关主键、外键的map;回到create-relation,这也是返回一个有关各种关系的map的函数,再回到rel函数,其实也是返回ent的一些环境变量的值assoc-in ent [:rel (name var-name)]... 这样就定义好了两个ent之间的关系。
现在话题回到with(实际用的还比较多):

(defn with* [query sub-ent body-fn]  (let [{:keys [rel-type] :as rel} (get-rel (:ent query) sub-ent)        transforms (seq (:transforms sub-ent))]    (cond      (and (#{:belongs-to :has-one} rel-type)           (not transforms))     (with-one-to-one-now rel query sub-ent body-fn)           (#{:belongs-to :has-one} rel-type) (with-one-to-one-later rel query sub-ent body-fn)           (= :has-many rel-type)     (with-one-to-many rel query sub-ent body-fn)           (= :many-to-many rel-type) (with-many-to-many rel query sub-ent body-fn)           :else (throw (Exception. (str "No relationship defined for table: "                                         (:table sub-ent)))))))(defmacro with  "Add a related entity to the given select query. If the entity has a relationship  type of :belongs-to or :has-one, the requested fields will be returned directly in  the result map. If the entity is a :has-many, a second query will be executed lazily  and a key of the entity name will be assoc'd with a vector of the results.  (defentity email (entity-fields :email))  (defentity user (has-many email))  (select user    (with email) => [{:name \"chris\" :email [{email: \"c@c.com\"}]} ...  With can also take a body that will further refine the relation:  (select user     (with address        (with state)        (fields :address.city :state.state)        (where {:address.zip x})))"  [query ent & body]  `(with* ~query ~ent (fn [q#]                        (-> q#                            ~@body))))

with会判断各种关系,然后进行相应的处理。(这部分还在更新中,比如with是怎么处理各种关系的,后续再分析。)

0 0
原创粉丝点击