jdbc-helper

A JDBC helper for Ruby/Database developers. JDBCHelper::Connection object wraps around a JDBC connection and provides much nicer interface to crucial database operations from primitive selects and updates to more complex ones involving batch updates, prepared statements and transactions. As the name implies, this gem only works on JRuby.

Examples

Prerequisites

Add JDBC driver of the DBMS you’re willing to use to your CLASSPATH

export CLASSPATH=$CLASSPATH:~/lib/mysql-connector-java.jar

Connecting to a database

# :driver and :url must be given
conn = JDBCHelper::Connection.new(
             :driver => 'com.mysql.jdbc.Driver',
             :url    => 'jdbc:mysql://localhost/test')
conn.close

# Optional :user and :password
conn = JDBCHelper::Connection.new(
             :driver   => 'com.mysql.jdbc.Driver',
             :url      => 'jdbc:mysql://localhost/test',
             :user     => 'mysql',
             :password => '')
conn.close

# MySQL shortcut connector
conn = JDBCHelper::MySQLConnector.connect('localhost', 'mysql', '', 'test')
conn.close

Querying database table

conn.query("SELECT a, b, c FROM T") do | row |
    row.labels
    row.rownum

    row.a, row.b, row.c          # Dot-notation
    row[0], row[1], row[2]       # Numeric index
    row['a'], row['b'], row['c'] # String index. Case-insensitive.
    row[:a], row[:b], row[:c]    # Symbol index. Case-insensitive.

    row[0..-1]                   # Range index. Returns an array of values.
    row[0, 3]                    # Offset and length. Returns an array of values.
end

# Returns an array of rows when block is not given
rows = conn.query("SELECT b FROM T")
uniq_rows = rows.uniq

# You can even nest queries
conn.query("SELECT a FROM T") do | row1 |
    conn.query("SELECT * FROM T_#{row1.a}") do | row2 |
        # ...
    end
end

# `enumerate' method returns an Enumerable object if block is not given.
# When the result set of the query is expected to be large and you wish to
# chain enumerators, `enumerate' is much preferred over `query'. (which returns the
# array of the entire rows)
conn.enumerate("SELECT * FROM LARGE_T").each_slice(1000) do | slice |
   slice.each do | row |
       # ...
   end
end

Updating database table

del_count = conn.update("DELETE FROM T")

Transaction

committed = conn.transaction do | tx |
    # ...
    # Transaction logic here
    # ...

    if success
        tx.commit
    else
        tx.rollback
    end
end

Using batch interface

conn.add_batch("DELETE FROM T");
conn.execute_batch
conn.add_batch("DELETE FROM T");
conn.clear_batch

Using prepared statements

p_sel = conn.prepare("SELECT * FROM T WHERE b = ? and c = ?")
p_sel.query(100, 200) do | row |
    p row
end
p_sel.close

p_upd = conn.prepare("UPDATE T SET a = ? WHERE b = ?")
count = 0
100.times do | i |
    count += p_upd.update('updated a', i)
end

p_upd.add_batch('pstmt + batch', 10)
p_upd.add_batch('pstmt + batch', 20)
p_upd.add_batch('pstmt + batch', 30)
p_upd.execute_batch
p_upd.close

Using table wrappers (since 0.2.0)

# For more complex examples, refer to test/test_object_wrapper.rb

table = conn.table('test.data')
table.count
table.empty?
table.select(:c => 3) do |row|
  puts row.a
end
table.update(:a => 1, :b => 2, :where => { :c => 3 })
table.insert(:a => 10, :b => 20, :c => 30)
table.insert_ignore(:a => 10, :b => 20, :c => 30)
table.replace(:a => 10, :b => 20, :c => 30)
table.delete(:c => 3)
table.truncate_table!
table.drop_table!

Using function wrappers (since 0.2.2)

conn.function(:mod).call 5, 3
conn.function(:coalesce).call(nil, nil, 'king')

Using procedure wrappers (since 0.3.0)

# Working with IN/INOUT/OUT parameteres
# Bind by ordinal number
conn.procedure(:update_and_fetch_something).call(

100, [“value”, String], Fixnum)

# Bind by parameter name
conn.procedure(:update_and_fetch_something).call(

:a => 100, :b => [“value”, String], :c => Fixnum)

Notes on vendor-independence

jdbc-helper tries to be a vendor-independent library, so that it behaves the same on any RDBMS, and that is why it’s built on JDBC in the first place which can be used to minimize the amount of vendor-specific code. (Ideally, one codebase for all RDBMSs.) So far so good, but not great. It has small amount of code that is vendor-specific, non-standard JDBC. And it has been confirmed to work the same for both MySQL and Oracle, except for stored procedures. MySQL and Oracle implement parameter binding of CallableStatement differently. See the following example.

# Let's say we have a stored procedure named my_procedure
# which takes 3 parameters, param1, param2, and param3.

# Creates a CallableStatemet
cstmt = conn.prepare_call("{call my_procedure(?, ?, ?)}")

cstmt.call(10, "ten", Fixnum)
  # OK for both MySQL and Oracle. Filling paramters sequentially.

cstmt.call(:param1 => 10, :param2 => "ten", :param3 => Fixnum)
  # MySQL automatically binds values by the parameter names in the original procedure definition.
  # Oracle fails to do so.
cstmt.close

# For Oracle, if you prepare as follows,
cstmt = conn.prepare_call("{call my_procedure(:p1, :p2, :p3)}")

# Then you can do this. 
cstmt.call(:p1 => 10, :p2 => "ten", :p3 => Fixnum)
  # However, p1, p2, p3 have nothing to do with the original
  # parameter names (param1, param2, param3) in the procedure definition.
  # They're just aliases for ordinal positions.
  # So, it's semantically different from the first example.

cstmt.close

There’s no way to find out the original parameter names and their ordinal positions without looking up the metadata of the database, which requires having to write vendor-specific code branches, and that is definitely not desirable. ProcedureWrapper prepares call with ‘?’s as the first example. So it would work fine for MySQL, but you can’t use it with parameter names on Oracle.

# Good for both
conn.procedure(:my_procedure).call(10, "ten", Fixnum)

# Only for MySQL
conn.procedure(:my_procedure).call(:param1 => 10, :param2 => "ten", :param3 => Fixnum)

Contributing to jdbc-helper

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet

  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it

  • Fork the project

  • Start a feature/bugfix branch

  • Commit and push until you are happy with your contribution

  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.

  • 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.

Copyright © 2011 Junegunn Choi. See LICENSE.txt for further details.