jdbc-helper 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.txt +20 -0
- data/README.rdoc +114 -0
- data/lib/jdbc-helper.rb +21 -0
- data/lib/jdbc-helper/connection.rb +395 -0
- data/lib/jdbc-helper/connection/prepared_statement.rb +141 -0
- data/lib/jdbc-helper/connection/result_set_enumerator.rb +100 -0
- data/lib/jdbc-helper/connection/row.rb +110 -0
- data/lib/jdbc-helper/connection/statement_pool.rb +61 -0
- data/lib/jdbc-helper/connector.rb +6 -0
- data/lib/jdbc-helper/connector/mysql_connector.rb +28 -0
- data/lib/jdbc-helper/connector/oracle_connector.rb +29 -0
- data/lib/jdbc-helper/constants.rb +26 -0
- data/test/database.yml +13 -0
- data/test/helper.rb +17 -0
- data/test/test_jdbc-helper.rb +292 -0
- metadata +105 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Junegunn Choi (junegunn.c@gmail.com)
|
3
|
+
|
4
|
+
module JDBCHelper
|
5
|
+
# An encapsulation of Java PreparedStatment object.
|
6
|
+
# Used to execute parameterized queries efficiently.
|
7
|
+
# Has a very similar set of interface as JDBCHelper::Connection.
|
8
|
+
#
|
9
|
+
# e.g.
|
10
|
+
#
|
11
|
+
# pstmt = conn.prepare("SELECT * FROM T WHERE a = ? and b = ?")
|
12
|
+
# rows = pstmt.query(10, 20)
|
13
|
+
# enum = pstmt.enumerate(10, 20)
|
14
|
+
#
|
15
|
+
class Connection
|
16
|
+
class PreparedStatement
|
17
|
+
attr_reader :sql
|
18
|
+
|
19
|
+
# Returns the encapsulated JDBC PreparedStatement object.
|
20
|
+
def java_obj
|
21
|
+
@pstmt
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the number of parameters required
|
25
|
+
def parameter_count
|
26
|
+
@pmd ||= @pstmt.get_parameter_meta_data
|
27
|
+
@pmd.get_parameter_count
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(*params)
|
31
|
+
check_closed
|
32
|
+
|
33
|
+
set_params(params)
|
34
|
+
measure(:p_update) { @pstmt.execute_update }
|
35
|
+
end
|
36
|
+
|
37
|
+
def query(*params, &blk)
|
38
|
+
check_closed
|
39
|
+
|
40
|
+
set_params(params)
|
41
|
+
# sorry, ignoring privacy
|
42
|
+
@conn.send(:process_and_close_rset,
|
43
|
+
measure(:p_query) { @pstmt.execute_query }, &blk)
|
44
|
+
end
|
45
|
+
|
46
|
+
def enumerate(*params, &blk)
|
47
|
+
check_closed
|
48
|
+
|
49
|
+
return query(*params, &blk) if block_given?
|
50
|
+
|
51
|
+
set_params(params)
|
52
|
+
ResultSetEnumerator.new(measure(:p_query) { @pstmt.execute_query })
|
53
|
+
end
|
54
|
+
|
55
|
+
# batch interface
|
56
|
+
def add_batch(*params)
|
57
|
+
check_closed
|
58
|
+
|
59
|
+
set_params(params)
|
60
|
+
@pstmt.add_batch
|
61
|
+
end
|
62
|
+
def execute_batch
|
63
|
+
check_closed
|
64
|
+
|
65
|
+
measure(:p_execute_batch) {
|
66
|
+
@pstmt.executeBatch
|
67
|
+
}
|
68
|
+
end
|
69
|
+
def clear_batch
|
70
|
+
check_closed
|
71
|
+
|
72
|
+
@pstmt.clear_batch
|
73
|
+
end
|
74
|
+
|
75
|
+
# Gives the JDBC driver a hint of the number of rows to fetch from the database by a single interaction.
|
76
|
+
# This is only a hint. It may no effect at all.
|
77
|
+
def set_fetch_size(fsz)
|
78
|
+
check_closed
|
79
|
+
|
80
|
+
@pstmt.set_fetch_size fsz
|
81
|
+
end
|
82
|
+
|
83
|
+
def close
|
84
|
+
return if closed?
|
85
|
+
@pstmt.close
|
86
|
+
@pstmts.delete @sql
|
87
|
+
@pstmt = @pstmts = nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def closed?
|
91
|
+
@pstmt.nil?
|
92
|
+
end
|
93
|
+
private
|
94
|
+
def initialize(conn, pstmts, sql, pstmt) # :nodoc:
|
95
|
+
@conn = conn
|
96
|
+
@pstmts = pstmts
|
97
|
+
@sql = sql
|
98
|
+
@pstmt = pstmt
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_params(params) # :nodoc:
|
102
|
+
idx = 0
|
103
|
+
params.each do | param |
|
104
|
+
if param.nil?
|
105
|
+
@pstmt.set_null(idx += 1, java.sql.Types::NULL)
|
106
|
+
elsif setter = SETTER_MAP[param.class.to_s]
|
107
|
+
if setter == :setBinaryStream
|
108
|
+
@pstmt.send(setter, idx += 1,
|
109
|
+
param.getBinaryStream, param.length)
|
110
|
+
else
|
111
|
+
@pstmt.send(setter, idx += 1, param)
|
112
|
+
end
|
113
|
+
else
|
114
|
+
@pstmt.set_string(idx += 1, param.to_s)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def measure(type, &blk) # :nodoc:
|
120
|
+
@conn.send(:measure, type, &blk)
|
121
|
+
end
|
122
|
+
|
123
|
+
def check_closed
|
124
|
+
raise RuntimeError.new("Prepared statement already closed") if closed?
|
125
|
+
end
|
126
|
+
|
127
|
+
SETTER_MAP =
|
128
|
+
{
|
129
|
+
'java.sql.Date' => :setDate,
|
130
|
+
'java.sql.Time' => :setTime,
|
131
|
+
'java.sql.Timestamp' => :setTimestamp,
|
132
|
+
'java.sql.Blob' => :setBinaryStream,
|
133
|
+
|
134
|
+
'com.mysql.jdbc.Blob' => :setBinaryStream
|
135
|
+
# FIXME-MORE
|
136
|
+
} # :nodoc:
|
137
|
+
|
138
|
+
end#PreparedStatment
|
139
|
+
end#Connection
|
140
|
+
end#JDBCHelper
|
141
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Junegunn Choi (junegunn.c@gmail.com)
|
3
|
+
|
4
|
+
module JDBCHelper
|
5
|
+
# Class for enumerating query results
|
6
|
+
# Automatically closed after used. When not used, you must close it explicitly by calling "close".
|
7
|
+
class Connection
|
8
|
+
class ResultSetEnumerator
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def each
|
12
|
+
return if closed?
|
13
|
+
|
14
|
+
count = -1
|
15
|
+
begin
|
16
|
+
while @rset.next
|
17
|
+
yield Connection::Row.new(
|
18
|
+
@col_labels,
|
19
|
+
(1..@num_col).map { | i |
|
20
|
+
v = @rset.send(GETTER_MAP.fetch(@cols_meta[i-1], :get_string), i)
|
21
|
+
@rset.was_null ? nil : v
|
22
|
+
},
|
23
|
+
count += 1)
|
24
|
+
end
|
25
|
+
ensure
|
26
|
+
close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def close
|
31
|
+
return if closed?
|
32
|
+
|
33
|
+
@rset.close
|
34
|
+
@close_callback.call if @close_callback
|
35
|
+
@closed = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def closed?
|
39
|
+
@closed
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def initialize(rset, &close_callback) # :nodoc:
|
44
|
+
unless rset.respond_to? :get_meta_data
|
45
|
+
rset.close if rset
|
46
|
+
@closed = true
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
@close_callback = close_callback
|
51
|
+
@rset = rset
|
52
|
+
@rsmd = @rset.get_meta_data
|
53
|
+
@num_col = @rsmd.get_column_count
|
54
|
+
@cols_meta = []
|
55
|
+
@col_labels = []
|
56
|
+
(1..@num_col).each do | i |
|
57
|
+
@cols_meta << @rsmd.get_column_type(i)
|
58
|
+
@col_labels << @rsmd.get_column_label(i)
|
59
|
+
end
|
60
|
+
|
61
|
+
@closed = false
|
62
|
+
end
|
63
|
+
|
64
|
+
GETTER_MAP =
|
65
|
+
{
|
66
|
+
java.sql.Types::TINYINT => :get_int,
|
67
|
+
java.sql.Types::SMALLINT => :get_int,
|
68
|
+
java.sql.Types::INTEGER => :get_int,
|
69
|
+
java.sql.Types::BIGINT => :get_long,
|
70
|
+
|
71
|
+
java.sql.Types::CHAR => :get_string,
|
72
|
+
java.sql.Types::VARCHAR => :get_string,
|
73
|
+
java.sql.Types::LONGVARCHAR => :get_string,
|
74
|
+
(java.sql.Types::NCHAR rescue nil) => :get_string,
|
75
|
+
(java.sql.Types::NVARCHAR rescue nil) => :get_string,
|
76
|
+
(java.sql.Types::LONGNVARCHAR rescue nil) => :get_blob, # FIXME: MySQL
|
77
|
+
java.sql.Types::BINARY => :get_string,
|
78
|
+
java.sql.Types::VARBINARY => :get_string,
|
79
|
+
java.sql.Types::LONGVARBINARY => :get_blob, # FIXME: MySQL
|
80
|
+
|
81
|
+
java.sql.Types::REAL => :get_double,
|
82
|
+
java.sql.Types::FLOAT => :get_float,
|
83
|
+
java.sql.Types::DOUBLE => :get_double,
|
84
|
+
java.sql.Types::NUMERIC => :get_string, # FIXME
|
85
|
+
java.sql.Types::DECIMAL => :get_string, # FIXME
|
86
|
+
|
87
|
+
java.sql.Types::DATE => :get_date,
|
88
|
+
java.sql.Types::TIME => :get_time,
|
89
|
+
java.sql.Types::TIMESTAMP => :get_timestamp,
|
90
|
+
|
91
|
+
java.sql.Types::BLOB => :get_blob,
|
92
|
+
java.sql.Types::CLOB => :get_string,
|
93
|
+
(java.sql.Types::NCLOB rescue nil) => :get_string,
|
94
|
+
|
95
|
+
java.sql.Types::BOOLEAN => :get_boolean
|
96
|
+
} # :nodoc:
|
97
|
+
end#ResultSetEnumerator
|
98
|
+
end#Connection
|
99
|
+
end#JDBCHelper
|
100
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Junegunn Choi (junegunn.c@gmail.com)
|
3
|
+
|
4
|
+
module JDBCHelper
|
5
|
+
# The result of database queries.
|
6
|
+
# Designed to be very flexible on its interface.
|
7
|
+
# You can access it like an array, a hash or an ORM object.
|
8
|
+
#
|
9
|
+
# e.g.
|
10
|
+
#
|
11
|
+
# conn.query('SELECT a, b, c FROM t') do | row |
|
12
|
+
# puts row.a
|
13
|
+
# puts row[1]
|
14
|
+
# puts row['c']
|
15
|
+
#
|
16
|
+
# row.each do | value |
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# labels = row.labels # equal to `row.keys`
|
21
|
+
# values = row.to_a # equal to `row.values`
|
22
|
+
#
|
23
|
+
# puts row.rownum
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
class Connection
|
27
|
+
class Row
|
28
|
+
attr_reader :labels, :values, :rownum
|
29
|
+
alias_method :keys, :labels
|
30
|
+
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
def [](idx)
|
34
|
+
if idx.is_a? Fixnum
|
35
|
+
raise RangeError.new("Index out of bound") if idx >= @values.length
|
36
|
+
@values[idx]
|
37
|
+
else
|
38
|
+
# case-insensitive, assuming no duplicate labels
|
39
|
+
vidx = @labels_d.index(idx.downcase) or
|
40
|
+
raise NameError.new("Unknown column label: #{idx}")
|
41
|
+
@values[vidx]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def each(&blk)
|
46
|
+
@values.each { | v | yield v }
|
47
|
+
|
48
|
+
# @labels.each_with_index do | label, idx |
|
49
|
+
# yield label, @values[idx]
|
50
|
+
# end
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
strs = []
|
55
|
+
@labels.each do | col |
|
56
|
+
strs << "#{col}: #{self[col] || '(null)'}"
|
57
|
+
end
|
58
|
+
'[' + strs.join(', ') + ']'
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
@values.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_a
|
66
|
+
@values
|
67
|
+
end
|
68
|
+
|
69
|
+
def join(sep = $,)
|
70
|
+
to_a.join(sep)
|
71
|
+
end
|
72
|
+
|
73
|
+
def eql?(other)
|
74
|
+
self.hash == other.hash
|
75
|
+
end
|
76
|
+
|
77
|
+
def hash # :nodoc:
|
78
|
+
@labels.zip(@values).sort.hash
|
79
|
+
end
|
80
|
+
|
81
|
+
alias :== :eql?
|
82
|
+
|
83
|
+
private
|
84
|
+
def initialize(col_labels, values, rownum) # :nodoc:
|
85
|
+
@labels = col_labels
|
86
|
+
@labels_d = col_labels.map { | l | l.downcase }
|
87
|
+
@values = values
|
88
|
+
@rownum = rownum
|
89
|
+
end
|
90
|
+
|
91
|
+
def method_missing(symb, *args)
|
92
|
+
if vidx = @labels_d.index(symb.to_s.downcase)
|
93
|
+
@values[vidx]
|
94
|
+
elsif @values.respond_to?(symb)
|
95
|
+
@values.send(symb, *args)
|
96
|
+
else
|
97
|
+
raise NameError.new("Unknown method: #{symb}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove dangerous default methods
|
102
|
+
# excluding :object_id, :hash
|
103
|
+
[:id, :tap, :gem, :display, :class, :method, :methods, :trust].
|
104
|
+
select { | s | method_defined? s }.each do | m |
|
105
|
+
undef_method m
|
106
|
+
end
|
107
|
+
end#Row
|
108
|
+
end#Connection
|
109
|
+
end#JDBCHelper
|
110
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Junegunn Choi (junegunn.c@gmail.com)
|
3
|
+
|
4
|
+
module JDBCHelper
|
5
|
+
# Internal implementation for supporting query nesting.
|
6
|
+
# Not thread-safe. Well, sharing a JDBC connection between threads is not the best idea anyway.
|
7
|
+
class Connection
|
8
|
+
class StatementPool # :nodoc:
|
9
|
+
def initialize(conn, max_size = 20)
|
10
|
+
@conn = conn
|
11
|
+
@max_size = max_size # TODO
|
12
|
+
@free = []
|
13
|
+
@occupied = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def with
|
17
|
+
begin
|
18
|
+
yield stmt = take
|
19
|
+
ensure
|
20
|
+
give stmt
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def take
|
25
|
+
if @free.empty?
|
26
|
+
raise Exception.new("Nesting too deep") if
|
27
|
+
@occupied.length >= @max_size
|
28
|
+
@occupied << nstmt = @conn.send(:create_statement)
|
29
|
+
nstmt
|
30
|
+
else
|
31
|
+
stmt = @free.pop
|
32
|
+
@occupied << stmt
|
33
|
+
stmt
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def give(stmt)
|
38
|
+
raise Exception.new("Not my statement") unless
|
39
|
+
@occupied.include? stmt
|
40
|
+
|
41
|
+
@occupied.delete stmt
|
42
|
+
@free << stmt
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
(@free + @occupied).each do | stmt |
|
47
|
+
stmt.close
|
48
|
+
end
|
49
|
+
@conn = nil
|
50
|
+
@free = []
|
51
|
+
@occupied = []
|
52
|
+
end
|
53
|
+
|
54
|
+
def each
|
55
|
+
(@free + @occupied).each do | stmt |
|
56
|
+
yield stmt
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end#StatementPool
|
60
|
+
end#Connection
|
61
|
+
end#JDBCHelper
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Junegunn Choi (junegunn.c@gmail.com)
|
3
|
+
|
4
|
+
module JDBCHelper
|
5
|
+
# Shortcut connector for MySQL
|
6
|
+
class MySQLConnector
|
7
|
+
include Constants
|
8
|
+
include Constants::Connector
|
9
|
+
|
10
|
+
def self.connect(host, user, password, db,
|
11
|
+
timeout = DEFAULT_LOGIN_TIMEOUT,
|
12
|
+
extra_params = DEFAULT_PARAMETERS[:mysql])
|
13
|
+
|
14
|
+
if extra_params && extra_params.is_a?(Hash) == false
|
15
|
+
raise ArgumentError.new('extra_params must be a hash')
|
16
|
+
end
|
17
|
+
|
18
|
+
Connection.new(
|
19
|
+
(extra_params || {}).merge(
|
20
|
+
:driver => JDBC_DRIVER[:mysql],
|
21
|
+
:url => "jdbc:mysql://#{host}/#{db}",
|
22
|
+
:user => user,
|
23
|
+
:password => password,
|
24
|
+
:timeout => timeout))
|
25
|
+
end
|
26
|
+
end#MySQLConnector
|
27
|
+
end#JDBCHelper
|
28
|
+
|