jdbc-helper 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Junegunn Choi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,114 @@
1
+ = jdbc-helper
2
+
3
+ A JDBC helper for Ruby/Database developers.
4
+ JDBCHelper::Connection object wraps around a JDBC connection and provides much nicer interface to
5
+ crucial database operations from primitive selects and updates to more complex ones involving
6
+ batch updates, prepared statements and transactions.
7
+ As the name implies, only works on JRuby.
8
+
9
+ = Examples
10
+
11
+ == Prerequisites
12
+ Add JDBC driver of the DBMS you're willing to use to your CLASSPATH
13
+ export CLASSPATH=$CLASSPATH:~/lib/mysql-connector-java.jar
14
+
15
+
16
+ == Connecting to a database
17
+
18
+ # :driver and :url must be given
19
+ conn = JDBCHelper::Connection.new(
20
+ :driver => 'com.mysql.jdbc.Driver',
21
+ :url => 'jdbc:mysql://localhost/test')
22
+ conn.close
23
+
24
+
25
+ # Optional :user and :password
26
+ conn = JDBCHelper::Connection.new(
27
+ :driver => 'com.mysql.jdbc.Driver',
28
+ :url => 'jdbc:mysql://localhost/test',
29
+ :user => 'mysql',
30
+ :password => '')
31
+ conn.close
32
+
33
+
34
+ # MySQL shortcut connector
35
+ conn = JDBCHelper::MySQLConnector.connect('localhost', 'mysql', '', 'test')
36
+ conn.close
37
+
38
+ == Querying database table
39
+
40
+ conn.query("SELECT a, b, c FROM T") do | row |
41
+ p row.labels
42
+ p row.rownum
43
+
44
+ puts row.a, row.b, row.c
45
+ puts row[0], row[1], row[2]
46
+ puts row['a'], row['b'], row['c']
47
+ end
48
+
49
+ # Returns an array of rows when block is not given
50
+ rows = conn.query("SELECT b FROM T")
51
+ uniq_rows = rows.uniq
52
+
53
+ # You can even nest queries
54
+ conn.query("SELECT a FROM T") do | row1 |
55
+ conn.query("SELECT * FROM T_#{row1.a}") do | row2 |
56
+ # ...
57
+ end
58
+ end
59
+ == Updating database table
60
+ del_count = conn.update("DELETE FROM T")
61
+
62
+ == Transaction
63
+ committed = conn.transaction do | tx |
64
+ # ...
65
+ # Transaction logic here
66
+ # ...
67
+
68
+ if success
69
+ tx.commit
70
+ else
71
+ tx.rollback
72
+ end
73
+ end
74
+
75
+ == Using batch interface
76
+ conn.add_batch("DELETE FROM T");
77
+ conn.execute_batch
78
+ conn.add_batch("DELETE FROM T");
79
+ conn.clear_batch
80
+
81
+ == Using prepared statements
82
+ p_sel = conn.prepare("SELECT * FROM T WHERE b = ? and c = ?")
83
+ p_sel.query(100, 200) do | row |
84
+ p row
85
+ end
86
+ p_sel.close
87
+
88
+ p_upd = conn.prepare("UPDATE T SET a = ? WHERE b = ?")
89
+ count = 0
90
+ 100.times do | i |
91
+ count += p_upd.update('updated a', i)
92
+ end
93
+
94
+ p_upd.add_batch('pstmt + batch', 10)
95
+ p_upd.add_batch('pstmt + batch', 20)
96
+ p_upd.add_batch('pstmt + batch', 30)
97
+ p_upd.execute_batch
98
+ p_upd.close
99
+
100
+ == Contributing to jdbc-helper
101
+
102
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
103
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
104
+ * Fork the project
105
+ * Start a feature/bugfix branch
106
+ * Commit and push until you are happy with your contribution
107
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
108
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
109
+
110
+ == Copyright
111
+
112
+ Copyright (c) 2011 Junegunn Choi. See LICENSE.txt for
113
+ further details.
114
+
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+ # Junegunn Choi (junegunn.c@gmail.com)
3
+
4
+ if RUBY_PLATFORM.match(/java/).nil?
5
+ raise LoadError, 'JRuby is required for JDBC'
6
+ end
7
+
8
+ require 'java'
9
+
10
+ module JavaLang # :nodoc:
11
+ include_package 'java.lang'
12
+ end
13
+
14
+ module JavaSql # :nodoc:
15
+ include_package 'java.sql'
16
+ end
17
+
18
+ require 'jdbc-helper/constants'
19
+ require 'jdbc-helper/connection'
20
+ require 'jdbc-helper/connector'
21
+
@@ -0,0 +1,395 @@
1
+ # encoding: UTF-8
2
+ # Junegunn Choi (junegunn.c@gmail.com)
3
+
4
+ require 'jdbc-helper/connection/statement_pool'
5
+ require 'jdbc-helper/connection/prepared_statement'
6
+ require 'jdbc-helper/connection/result_set_enumerator'
7
+ require 'jdbc-helper/connection/row'
8
+
9
+ module JDBCHelper
10
+ # Encapsulates JDBC database connection.
11
+ # Lets you easily execute SQL statements and access their results.
12
+ #
13
+ #
14
+ # = Examples
15
+ #
16
+ # == Prerequisites
17
+ # Add JDBC driver of the DBMS you're willing to use to your CLASSPATH
18
+ # export CLASSPATH=$CLASSPATH:~/lib/mysql-connector-java.jar
19
+ #
20
+ #
21
+ # == Connecting to a database
22
+ #
23
+ # # :driver and :url must be given
24
+ # conn = JDBCHelper::Connection.new(
25
+ # :driver => 'com.mysql.jdbc.Driver',
26
+ # :url => 'jdbc:mysql://localhost/test')
27
+ # conn.close
28
+ #
29
+ #
30
+ # # Optional :user and :password
31
+ # conn = JDBCHelper::Connection.new(
32
+ # :driver => 'com.mysql.jdbc.Driver',
33
+ # :url => 'jdbc:mysql://localhost/test',
34
+ # :user => 'mysql',
35
+ # :password => '')
36
+ # conn.close
37
+ #
38
+ #
39
+ # # MySQL shortcut connector
40
+ # conn = JDBCHelper::MySQLConnector.connect('localhost', 'mysql', '', 'test')
41
+ # conn.close
42
+ #
43
+ # == Querying database table
44
+ #
45
+ # conn.query("SELECT a, b, c FROM T") do | row |
46
+ # p row.labels
47
+ # p row.rownum
48
+ #
49
+ # puts row.a, row.b, row.c
50
+ # puts row[0], row[1], row[2]
51
+ # puts row['a'], row['b'], row['c']
52
+ # end
53
+ #
54
+ # # Returns an array of rows when block is not given
55
+ # rows = conn.query("SELECT b FROM T")
56
+ # uniq_rows = rows.uniq
57
+ #
58
+ # # You can even nest queries
59
+ # conn.query("SELECT a FROM T") do | row1 |
60
+ # conn.query("SELECT * FROM T_#{row1.a}") do | row2 |
61
+ # # ...
62
+ # end
63
+ # end
64
+ # == Updating database table
65
+ # del_count = conn.update("DELETE FROM T")
66
+ #
67
+ # == Transaction
68
+ # committed = conn.transaction do | tx |
69
+ # # ...
70
+ # # Transaction logic here
71
+ # # ...
72
+ #
73
+ # if success
74
+ # tx.commit
75
+ # else
76
+ # tx.rollback
77
+ # end
78
+ # end
79
+ #
80
+ # == Using batch interface
81
+ # conn.add_batch("DELETE FROM T");
82
+ # conn.execute_batch
83
+ # conn.add_batch("DELETE FROM T");
84
+ # conn.clear_batch
85
+ #
86
+ # == Using prepared statements
87
+ # p_sel = conn.prepare("SELECT * FROM T WHERE b = ? and c = ?")
88
+ # p_sel.query(100, 200) do | row |
89
+ # p row
90
+ # end
91
+ # p_sel.close
92
+ #
93
+ # p_upd = conn.prepare("UPDATE T SET a = ? WHERE b = ?")
94
+ # count = 0
95
+ # 100.times do | i |
96
+ # count += p_upd.update('updated a', i)
97
+ # end
98
+ #
99
+ # p_upd.add_batch('pstmt + batch', 10)
100
+ # p_upd.add_batch('pstmt + batch', 20)
101
+ # p_upd.add_batch('pstmt + batch', 30)
102
+ # p_upd.execute_batch
103
+ # p_upd.close
104
+ class Connection
105
+ Stat = Struct.new("DBExecStat", :type, :elapsed, :success_count, :fail_count) # :nodoc:
106
+
107
+ # Returns the statistics of the previous operation
108
+ def prev_stat
109
+ @prev_stat.dup
110
+ end
111
+
112
+ # Returns the accumulated statistics of each operation
113
+ attr_reader :stats
114
+
115
+ # Returns the underlying JDBC Connection object.
116
+ # Only use this when you really need to access it directly.
117
+ def jdbc_conn
118
+ @conn
119
+ end
120
+ alias java_obj jdbc_conn
121
+
122
+ # Creates a database connection.
123
+ # - `args` hash must include :driver (or "driver") and :url (or "url")
124
+ # - and takes optional :user and :password tuples (or "user", "password")
125
+ # - You can also specify :timeout (or "timeout") to override the default connection timeout (60 seconds)
126
+ #
127
+ # Must be closed explicitly if not used.
128
+ # If a block is given, the connection is automatically closed after executing the block.
129
+ def initialize(args = {})
130
+ # String-tolerance..
131
+ %w[driver url user password timeout].each do | strk |
132
+ args[strk.to_sym] = args.delete strk if args.has_key? strk
133
+ end
134
+
135
+ raise ArgumentError.new("driver not given") unless args.has_key? :driver
136
+ raise ArgumentError.new("url not given") unless args.has_key? :url
137
+
138
+ begin
139
+ Java::JavaClass.for_name args[:driver]
140
+ rescue Exception
141
+ # TODO
142
+ raise
143
+ end
144
+
145
+ timeout = args.has_key?(:timeout) ? args.delete(:timeout) : Constants::DEFAULT_LOGIN_TIMEOUT
146
+ JavaSql::DriverManager.setLoginTimeout timeout if timeout
147
+
148
+ props = java.util.Properties.new
149
+ (args.keys - [:url, :driver]).each do | key |
150
+ props.setProperty(key.to_s, args[key]) if args[key]
151
+ end
152
+
153
+ @conn = JavaSql::DriverManager.get_connection(args[:url], props)
154
+ @spool = StatementPool.send :new, self
155
+ @pstmts = {}
156
+ @bstmt = nil
157
+
158
+ @stats = Hash.new { | h, k | h[k] = Stat.new(k, 0, 0, 0) }
159
+ @prev_stat = Stat.new(nil, 0, 0, 0)
160
+
161
+ if block_given?
162
+ begin
163
+ yield self
164
+ ensure
165
+ close rescue nil
166
+ end
167
+ end
168
+ end
169
+
170
+ # Creates a prepared statement, which is also an encapsulation of Java PreparedStatement object
171
+ def prepare(qstr)
172
+ check_closed
173
+
174
+ return @pstmts[qstr] if @pstmts.has_key? qstr
175
+
176
+ pstmt = PreparedStatement.send(:new, self, @pstmts, qstr,
177
+ measure(:prepare) { @conn.prepare_statement(qstr) })
178
+ @pstmts[qstr] = pstmt
179
+ pstmt
180
+ end
181
+
182
+ # Executes the given code block as a transaction. Returns true if the transaction is committed.
183
+ # A transaction object is passed to the block, which only has commit and rollback methods.
184
+ # The execution breaks out of the code block when either of the methods is called.
185
+ def transaction
186
+ check_closed
187
+
188
+ raise ArgumentError.new("Transaction block not given") unless block_given?
189
+ tx = Transaction.send :new, @conn
190
+ ac = @conn.get_auto_commit
191
+ status = :unknown
192
+ begin
193
+ @conn.set_auto_commit false
194
+ yield tx
195
+ @conn.commit
196
+ status = :committed
197
+ rescue Transaction::Commit
198
+ status = :committed
199
+ rescue Transaction::Rollback
200
+ status = :rolledback
201
+ ensure
202
+ @conn.rollback if status == :unknown
203
+ @conn.set_auto_commit ac
204
+ end
205
+ status == :committed
206
+ end
207
+
208
+ # Executes an update and returns the count of the updated rows.
209
+ def update(qstr)
210
+ check_closed
211
+
212
+ @spool.with do | stmt |
213
+ ret = measure(:update) { stmt.execute_update(qstr) }
214
+ end
215
+ end
216
+
217
+ # Executes a select query.
218
+ # When a code block is given, each row of the result is passed to the block one by one.
219
+ # If a code block not given, this method will return the array of the entire result rows.
220
+ # (which can be pretty inefficient when the result set is large. In such cases, use enumerate instead.)
221
+ #
222
+ # The concept of statement object of JDBC is encapsulated, so there's no need to do additional task,
223
+ # when you nest select queries, for example.
224
+ #
225
+ # conn.query("SELECT a FROM T") do | trow |
226
+ # conn.query("SELECT * FROM U_#{trow.a}") do | urow |
227
+ # # ... and so on ...
228
+ # end
229
+ # end
230
+ def query(qstr, &blk)
231
+ check_closed
232
+
233
+ @spool.with do | stmt |
234
+ measure(:query) { stmt.execute(qstr) }
235
+ process_and_close_rset(stmt.get_result_set, &blk)
236
+ end
237
+ end
238
+
239
+ # Returns an enumerable object of the query result.
240
+ # "enumerate" method is preferable when dealing with a large result set,
241
+ # since it doesn't have to build a large array.
242
+ #
243
+ # The returned enumerator is automatically closed after enumeration.
244
+ #
245
+ # conn.enumerate('SELECT * FROM T').each_slice(10) do | slice |
246
+ # slice.each { | row | print row }
247
+ # puts
248
+ # end
249
+ #
250
+ def enumerate(qstr, &blk)
251
+ check_closed
252
+
253
+ return query(qstr, &blk) if block_given?
254
+
255
+ stmt = @spool.take
256
+ begin
257
+ measure(:query) { stmt.execute(qstr) }
258
+ rescue Exception
259
+ @spool.give stmt
260
+ raise
261
+ end
262
+
263
+ ResultSetEnumerator.send(:new, stmt.get_result_set) { @spool.give stmt }
264
+ end
265
+
266
+ # Adds a statement to be executed in batch
267
+ def add_batch(qstr)
268
+ check_closed
269
+
270
+ @bstmt ||= @spool.take
271
+ @bstmt.add_batch qstr
272
+ end
273
+
274
+ # Executes batched statements. No effect when no statment is added
275
+ def execute_batch
276
+ check_closed
277
+
278
+ return unless @bstmt
279
+ ret = measure(:execute_batch) { @bstmt.execute_batch }
280
+ @spool.give @bstmt
281
+ @bstmt = nil
282
+ ret
283
+ end
284
+
285
+ # Clears the batched statements
286
+ def clear_batch
287
+ check_closed
288
+
289
+ return unless @bstmt
290
+ @bstmt.clear_batch
291
+ @spool.give @bstmt
292
+ @bstmt = nil
293
+ end
294
+
295
+ # Gives the JDBC driver a hint of the number of rows to fetch from the database by a single interaction.
296
+ # This is only a hint. It may have no effect at all.
297
+ def set_fetch_size(fsz)
298
+ check_closed
299
+
300
+ @fetch_size = fsz
301
+ @spool.each { | stmt | stmt.set_fetch_size @fetch_size }
302
+ end
303
+
304
+ # Closes the connection
305
+ def close
306
+ return if closed?
307
+ @pstmts.each { | q, pstmt | pstmt.close }
308
+ @spool.close
309
+ @conn.close
310
+ @conn = @spool = nil
311
+ end
312
+
313
+ # Returns if this connection is closed or not
314
+ def closed?
315
+ @conn.nil?
316
+ end
317
+
318
+ private
319
+ # Transaction object passed to the code block given to transaction method
320
+ class Transaction
321
+ # Commits the transaction
322
+ def commit
323
+ @conn.commit
324
+ raise Commit
325
+ end
326
+ # Rolls back this transaction
327
+ def rollback
328
+ @conn.rollback
329
+ raise Rollback
330
+ end
331
+ private
332
+ def initialize(conn) # :nodoc:
333
+ @conn = conn
334
+ end
335
+ class Commit < Exception # :nodoc:
336
+ end
337
+ class Rollback < Exception # :nodoc:
338
+ end
339
+ end
340
+
341
+ def create_statement # :nodoc:
342
+ stmt = @conn.create_statement
343
+ stmt.set_fetch_size @fetch_size if @fetch_size
344
+ stmt
345
+ end
346
+
347
+ def process_and_close_rset(rset) # :nodoc:
348
+ enum = ResultSetEnumerator.send :new, rset
349
+ rows = []
350
+
351
+ begin
352
+ enum.each do | row |
353
+ if block_given?
354
+ yield row
355
+ else
356
+ rows << row
357
+ end
358
+ end
359
+ block_given? ? nil : rows
360
+ ensure
361
+ enum.close
362
+ end
363
+ end
364
+
365
+ def update_stat(type, elapsed, success_count, fail_count) # :nodoc:
366
+ @prev_stat.type = type
367
+ @prev_stat.elapsed = elapsed
368
+ @prev_stat.success_count = success_count
369
+ @prev_stat.fail_count = fail_count
370
+
371
+ accum = @stats[type]
372
+ accum.elapsed += elapsed
373
+ accum.success_count += success_count
374
+ accum.fail_count += fail_count
375
+ end
376
+
377
+ def measure(type)
378
+ begin
379
+ st = Time.now
380
+ ret = yield
381
+ elapsed = Time.now - st
382
+ update_stat(type, elapsed, 1, 0)
383
+ rescue Exception
384
+ update_stat(type, 0, 0, 1)
385
+ raise
386
+ end
387
+ ret
388
+ end
389
+
390
+ def check_closed
391
+ raise RuntimeError.new('Connection already closed') if closed?
392
+ end
393
+ end#Connection
394
+ end#JDBCHelper
395
+