-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
connection.clj
278 lines (226 loc) · 11.6 KB
/
connection.clj
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
(ns toucan2.connection
"#### Connection Resolution
The rules for determining which connection to use are as follows. These are tried in order until one returns
non-nil:
1. The connectable specified in the function arguments.
2. The [[toucan2.connection/*current-connectable*]], if bound. This is bound automatically when
using [[with-connection]] or [[with-transaction]]
3. The [[toucan2.model/default-connectable]] for the model resolved from the `modelable` in the function arguments;
4. The `:default` implementation of [[toucan2.connection/do-with-connection]]
You can define a 'named' connectable such as `::db` by adding an implementation
of [[toucan2.connection/do-with-connection]], or use things like JDBC URL connection strings or [[clojure.java.jdbc]]
connection properties maps directly.
IMPORTANT CAVEAT! Positional connectables will be used in preference to [[*current-connectable*]], even when it was
bound by [[with-transaction]] -- this means your query will run OUTSIDE of the current transaction! Sometimes, this is
what you want, because maybe a certain query is meant to run against a different database! Usually, however, it is
not! So in that case you can either do something like
```clj
(t2/query (or conn/*current-connectable* ::my-db) ...)
```
to use the current connection if it exists, or define your named connectable method like
```clj
(m/defmethod conn/do-with-connection ::my-db
[_connectable f]
(conn/do-with-connection
(if (and conn/*current-connectable*
(not= conn/*current-connectable* ::my-db))
conn/*current-connectable*
\"jdbc:postgresql://...\")
f))
```
This, however, is super annoying! So I might reconsider this behavior in the future.
For reducible queries, the connection is not resolved until the query is executed, so you may create a reducible query
with no default connection available and execute it later with one bound. (This also means
that [[toucan2.execute/reducible-query]] does not capture dynamic bindings such
as [[toucan2.connection/*current-connectable*]] -- you probably wouldn't want it to, anyway, since we have no
guarantees and open connection will be around when we go to use the reducible query later.
The default JDBC implementations for methods here live in [[toucan2.jdbc.connection]]."
(:require
[clojure.spec.alpha :as s]
[methodical.core :as m]
[pretty.core :as pretty]
[toucan2.log :as log]
[toucan2.protocols :as protocols]
[toucan2.types :as types]
[toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment types/keep-me)
(def ^:dynamic *current-connectable*
"The current connectable or connection. If you get a connection with [[with-connection]] or [[with-transaction]], it
will be bound here. You can also bind this yourself to a connectable or connection, and Toucan methods called without
an explicit will connectable will use it rather than the `:default` connection."
nil)
(m/defmulti do-with-connection
"Take a *connectable*, get a connection of some sort from it, and execute `(f connection)` with an open connection. A
normal implementation might look something like:
```clj
(m/defmethod t2.conn/do-with-connection ::my-connectable
[_connectable f]
(with-open [conn (get-connection)]
(f conn)))
```
Another common use case is to define a 'named' connectable that acts as an alias for another more complicated
connectable, such as a JDBC connection string URL. You can do that like this:
```clj
(m/defmethod t2.conn/do-with-connection ::a-connectable
[_connectable f]
(t2.conn/do-with-connection
\"jdbc:postgresql://localhost:5432/toucan2?user=cam&password=cam\"
f))
```"
{:arglists '([connectable₁ f])
:defmethod-arities #{2}
:dispatch-value-spec ::types/dispatch-value.keyword-or-class}
u/dispatch-on-first-arg
:default-value ::default)
(defn- bind-current-connectable-fn
"Wrap functions as passed to [[do-with-connection]] or [[do-with-transaction]] in a way that
binds [[*current-connectable*]]."
[f]
{:pre [(fn? f)]}
(^:once fn* [conn]
(binding [*current-connectable* conn]
(f conn))))
(m/defmethod do-with-connection :around ::default
"Do some debug logging/context capture. Bind [[*current-connectable*]] to the connection `f` is called with inside of
`f`."
[connectable f]
(assert (fn? f))
;; add the connection class or pretty representation rather than the connection type itself to avoid leaking sensitive
;; creds
(let [connectable-class (if (instance? pretty.core.PrettyPrintable connectable)
(pretty/pretty connectable)
(protocols/dispatch-value connectable))]
(log/debugf "Resolve connection %s" connectable-class)
(u/try-with-error-context ["resolve connection" {::connectable connectable-class}]
(next-method connectable (bind-current-connectable-fn f)))))
(defmacro with-connection
"Execute `body` with an open connection. There are three ways to use this.
With no args in the bindings vector, `with-connection` will use the *current connection* -- [[*current-connectable*]]
if one is bound, or the *default connectable* if not. See docstring for [[toucan2.connection]] for more information.
```clj
(t2/with-connection []
...)
```
With one arg, `with-connection` still uses the *current connection*, but binds it to something (`conn` in the example
below):
```clj
(t2/with-connection [conn]
...)
```
If you're using the default JDBC backend, `conn` will be an instance of `java.sql.Connection`. Since Toucan 2 is also
written to work with other backend besides JDBC, `conn` does *not* include `java.sql.Connection` `:tag` metadata! If
you're doing Java interop with `conn`, make sure to tag it yourself:
```clj
(t2/with-connection [^java.sql.Connection conn]
(let [metadata (.getMetaData conn)]
...))
```
With a connection binding *and* a connectable:
```clj
(t2/with-connection [conn ::my-connectable]
...)
```
This example gets a connection by calling [[do-with-connection]] with `::my-connectable`, ignoring the *current
connection*."
{:arglists '([[connection-binding] & body]
[[connection-binding connectable] & body])}
[[connection-binding connectable] & body]
`(do-with-connection
~connectable
(^:once fn* with-connection* [~(or connection-binding '_)] ~@body)))
(s/fdef with-connection
:args (s/cat :bindings (s/spec (s/cat :connection-binding (s/? symbol?)
:connectable (s/? any?)))
:body (s/+ any?))
:ret any?)
;;; method if this is called with something we don't know how to handle or if no default connection is defined. This is
;;; separate from `:default` so if you implement `:default` you don't accidentally have that get called for unknown
;;; connectables
(m/defmethod do-with-connection ::default
[connectable _f]
(throw (ex-info (format "Don't know how to get a connection from ^%s %s. Do you need to implement %s for %s?"
(some-> connectable class .getCanonicalName)
(pr-str connectable)
`do-with-connection
(protocols/dispatch-value connectable))
{:connectable connectable})))
;;; method called if there is no current connection.
(m/defmethod do-with-connection :default
[_connectable _f]
(throw (ex-info (str "No default Toucan connection defined. "
(format "You can define one by implementing %s for :default. "
`do-with-connection)
(format "You can also implement %s for a model, or bind %s."
'toucan2.model/default-connectable
`*current-connectable*))
{})))
(m/defmethod do-with-connection nil
"`nil` means use the current connection.
The difference between `nil` and using [[*current-connectable*]] directly is that this waits until it gets resolved
by [[do-with-connection]] to get the value for [[*current-connectable*]]. For a reducible query this means you'll get
the value at the time you reduce the query rather than at the time you build the reducible query."
[_connectable f]
(let [current-connectable (if (nil? *current-connectable*)
:default
*current-connectable*)]
(do-with-connection current-connectable f)))
;;;; connection string support
(defn connection-string-protocol
"Extract the protocol part of a `connection-string`.
```clj
(connection-string-protocol \"jdbc:postgresql:...\")
=>
\"jdbc\"
```"
[connection-string]
(when (string? connection-string)
(second (re-find #"^(?:([^:]+):)" connection-string))))
(m/defmulti do-with-connection-string
"Implementation of [[do-with-connection]] for strings. Dispatches on the [[connection-string-protocol]] of the string,
e.g. `\"jdbc\"` for `\"jdbc:postgresql://localhost:3000/toucan\"`."
{:arglists '([^java.lang.String connection-string f])
:defmethod-arities #{2}
:dispatch-value-spec string?}
(fn [connection-string _f]
(connection-string-protocol connection-string)))
(m/defmethod do-with-connection String
"Implementation for Strings. Hands off to [[do-with-connection-string]]."
[connection-string f]
(do-with-connection-string connection-string f))
;;; JDBC implementations live in [[toucan2.jdbc.connection]]
(m/defmulti do-with-transaction
"`options` are options for determining what type of transaction we'll get. See dox for [[with-transaction]] for more
information."
{:arglists '([connection₁ options f])
:defmethod-arities #{3}
:dispatch-value-spec ::types/dispatch-value.keyword-or-class}
u/dispatch-on-first-arg
:default-value ::default)
(m/defmethod do-with-transaction :around ::default
"Bind [[*current-connectable*]] to the connection `f` is called with inside of `f`."
[connection options f]
(log/debugf "do with transaction %s %s" options (some-> connection class .getCanonicalName symbol))
(next-method connection options (bind-current-connectable-fn f)))
(defmacro with-transaction
"Gets a connection with [[with-connection]], and executes `body` within that transaction.
An `options` map, if specified, determine what sort of transaction we're asking for (stuff like the read isolation
level and what not). One key, `:nested-transaction-rule`, is handled directly in Toucan 2; other options are passed
directly to the underlying implementation, such as [[next.jdbc.transaction]].
`:nested-transaction-rule` must be one of `#{:allow :ignore :prohibit}`, a set of possibilities borrowed from
`next.jdbc`. For non-JDBC implementations, you should treat `:allow` as the default behavior if unspecified."
{:style/indent 1, :arglists '([[conn-binding connectable options?] & body])}
[[conn-binding connectable options] & body]
`(with-connection [conn# ~connectable]
(do-with-transaction conn# ~options (^:once fn* with-transaction* [~(or conn-binding '_)] ~@body))))
(s/def :toucan2.with-transaction-options/nested-transaction-rule
(s/nilable #{:allow :ignore :prohibit}))
(s/def ::with-transaction-options
(s/keys :opt-un [:toucan2.with-transaction-options/nested-transaction-rule]))
(s/fdef with-transaction
:args (s/cat :bindings (s/spec (s/cat :connection-binding (s/? symbol?)
:connectable (s/? any?)
:options (s/? ::with-transaction-options)))
:body (s/+ any?))
:ret any?)
;;; JDBC implementation lives in [[toucan2.jdbc.connection]]