连接数据库 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是怎么处理各种关系的,后续再分析。)
- 连接数据库 in clojure ---korma
- Clojure实现的简单短网址服务(Compojure、Ring、Korma库示例)
- XPath implementation in Clojure
- clojure 连接mysql
- Clojure Support in Cloud Foundry
- Learn clojure in Y minutes
- clojure
- Clojure
- clojure
- clojure
- Clojure
- Persistent and Transient Data Structures in Clojure
- namespace/symbol/:keyword/::keyword in Clojure
- Clojure 学习入门(7)- 连接mysql
- Clojure 学习入门(8)- 连接mongodb
- Clojure 学习入门(9)- 连接redis
- 使用clojure访问SQL Server数据库
- Clojure 学习入门(7)—— 连接mysql
- Leetcode Populating Next Right Pointers in Each Node
- Leetcode Length of Last Word
- Leetcode Multiply Strings
- 关于使用开源库或jar包时可能遇到ndk冲突错误
- 最小点割集
- 连接数据库 in clojure ---korma
- 【日志7.19】
- Learning Python 010 函数 1
- 关于mysql存储过程的definer的问题
- Linux进阶命令
- 【手游】少年西游记 美术资源加密分析
- bash 快捷键
- windows NET命令的使用
- 从架构的角度看如何写好代码