The Backyard - RjbJdbcAdapter Diff
- Added parts are displayed like this.
- Deleted parts are displayed
like this.
!Rjb JDBC Adapter
!!prologue
I implement Rjb-JDBC adapter, based on JRuby jdbc adapter(gem search --remote jdbc). This is very mock up code. I have little incentive to more. Please test your self and use this code.
(RjbJDBCアダプタを書いてみたんだけど、使わないし、ばりやる気がないので自己責任で使ってよ。ライセンスは当然オリジナルに従います。)
!!usage
* save such a placethat(適当に保存してください。) that "/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/rjb_jdbc_adapter.rb"
* Open "active_record.rb", 'rjb_jdbc' append to RAILS_CONNECTION_ADAPTERSconstant.(ファイル名を解決するヒントを直接書いてください)constant.
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase rjb_jdbc)
* Editconfig/database.yml(いつもどおり設定してください)config/database.yml
development:
adapter: rjb_jdbc
driver: org.apache.derby.jdbc.EmbeddedDriver
#driver: org.hsqldb.jdbcDriver
username: sa
password:
#url: jdbc:hsqldb:file:/testdb
url: jdbc:derby:/testdb;create=true
primary_key: int generated always as identity
#primary_key: integer generated by default as identity
catalog: nil
schema: SA
* CLASSPATH environment define somewhere( startup shell,application.rb).(クラスパスをどこかで定義してください)application.rb).
*bumble.(嵌ってみてください)bumble.
!!code
require 'active_record/connection_adapters/abstract_adapter'
require 'rjb'
# I not like this code.
module ActiveRecord
class Base
def self.rjb_jdbc_connection(config)
ConnectionAdapters::RjbJdbcAdapter.new(ConnectionAdapters::RjbJdbcConnection.new(config, logger), logger, config)
end
end
module ConnectionAdapters
# If support multi database connection, destined for trouble.
DriverManager = Rjb::import('java.sql.DriverManager')
Statement = Rjb::import('java.sql.Statement')
Types = Rjb::import('java.sql.Types')
# I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to
# use for ActiveRecord's Adapter#native_database_types in a database-independent way,
# but apparently a database driver can return multiple types for a given
# java.sql.Types constant. So this type converter uses some heuristics to try to pick
# the best (most common) type to use. It's not great, it would be better to just
# delegate to each database's existin AR adapter's native_database_types method, but I
# wanted to try to do this in a way that didn't pull in all the other adapters as
# dependencies. Suggestions appreciated.
class RjbJdbcTypeConverter
# The basic ActiveRecord types, mapped to an array of procs that are used to #select
# the best type. The procs are used as selectors in order until there is only one
# type left. If all the selectors are applied and there is still more than one
# type, an exception will be raised.
#TODO: incomplate const defined types
AR_TO_JDBC_TYPES = {
:string => [ proc {|r| Types.VARCHAR == r['data_type']},
proc {|r| r['type_name'] =~ /^varchar$/i} ],
:text => [ proc {|r| [Types.LONGVARCHAR, Types.CLOB].include?(r['data_type'])},
proc {|r| r['type_name'] =~ /^(text|clob)/i} ],
:integer => [ proc {|r| Types.INTEGER == r['data_type']},
proc {|r| r['type_name'] =~ /^integer/i} ],
:float => [ proc {|r| [Types.FLOAT,Types.DOUBLE].include?(r['data_type'])},
proc {|r| r['type_name'] =~ /^float/i},
proc {|r| r['type_name'] =~ /^double$/i} ],
:datetime => [ proc {|r| Types.TIMESTAMP == r['data_type']},
proc {|r| r['type_name'] =~ /^datetime/i} ],
:timestamp => [ proc {|r| Types.TIMESTAMP == r['data_type']},
proc {|r| r['type_name'] =~ /^timestamp/i},
proc {|r| r['type_name'] =~ /^datetime/i} ],
:time => [ proc {|r| Types.TIME == r['data_type']} ],
:date => [ proc {|r| Types.DATE == r['data_type']} ],
:binary => [ proc {|r| Types.LONGVARBINARY == r['data_type']},
proc {|r| r['type_name'] =~ /^blob/i} ],
:boolean => [ proc {|r| Types.TINYINT == r['data_type']},
proc {|r| Types.SMALLINT == r['data_type']},
proc {|r| Types.BOOLEAN == r['data_type']} ]
}
def initialize(types, logger)
@types = types
@logger = logger
end
def choose_best_types
type_map = {}
AR_TO_JDBC_TYPES.each_key do |k|
typerow = choose_type(k)
type_map[k] = { :name => typerow['type_name'] }
type_map[k][:limit] = typerow['precision'] if [:integer,:string].include?(k) and (typerow['fixed_prec_scale'] or typerow['create_params'])
type_map[k][:limit] = 1 if k == :boolean
end
@logger.debug(type_map.inspect)
type_map
end
def choose_type(ar_type)
@logger.debug(ar_type)
procs = AR_TO_JDBC_TYPES[ar_type]
@logger.debug(procs)
types = @types
@logger.debug(types.inspect)
procs.each do |p|
new_types = types.select(&p)
return new_types.first if new_types.length == 1
types = new_types if new_types.length > 0
end
raise "unable to choose type from: #{types.collect{|t| t['type_name']}.inspect}"
end
end
class RjbJdbcConnection
def initialize(config, logger)
config = config.symbolize_keys
driver = config[:driver].to_s
user = config[:username].to_s
pass = config[:password].to_s
url = config[:url].to_s
@config = config
@logger = logger
unless driver && url
raise ArgumentError, "jdbc adapter requires driver class and url"
end
DriverManager.registerDriver(Rjb::import(driver))
@connection = DriverManager.getConnection(url, user, pass)
set_native_database_types
end
def set_native_database_types
types = unmarshal_result(@connection.getMetaData.getTypeInfo)
@native_types = RjbJdbcTypeConverter.new(types, @logger).choose_best_types
end
def native_database_types
types = {
# TODO: this is copied from MySQL -- figure out how to
# generalize the primary key type
:primary_key => @config[:primary_key] || "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
}
@native_types.each_pair {|k,v| types[k] = v.inject({}) {|memo,kv| memo.merge({kv[0] => kv[1..-1]})}}
types
end
def columns(table_name, name = nil)
metadata = @connection.getMetaData
results = metadata.getColumns(@config[:catalog], @config[:schema], table_name, nil)
columns = []
unmarshal_result(results).each do |col|
columns << ActiveRecord::ConnectionAdapters::Column.new(col['column_name'], col['column_def'],
"#{col['type_name']}(#{col['column_size']})", col['is_nullable'] != 'NO')
end
columns
end
def tables
metadata = @connection.getMetaData
results = metadata.getTables(@config[:catalog], @config[:schema], nil, nil)
unmarshal_result(results).collect {|t| t['table_name']}
end
def execute_insert(sql, pk)
stmt = @connection.createStatement
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS)
row = unmarshal_result(stmt.getGeneratedKeys)
row.first && row.first.values.first
ensure
stmt.close
end
def execute_update(sql)
stmt = @connection.createStatement
stmt.executeUpdate(sql)
ensure
stmt.close
end
def execute_query(sql)
stmt = @connection.createStatement
unmarshal_result(stmt.executeQuery(sql))
ensure
stmt.close
end
def begin
@connection.setAutoCommit(false)
end
def commit
@connection.commit
ensure
@connection.setAutoCommit(true)
end
def rollback
@connection.rollback
ensure
@connection.setAutoCommit(true)
end
private
def unmarshal_result(resultset)
metadata = resultset.getMetaData
column_count = metadata.getColumnCount
column_names = ['']
column_types = ['']
column_scale = ['']
1.upto(column_count) do |i|
column_names << metadata.getColumnName(i)
column_types << metadata.getColumnType(i)
column_scale << metadata.getScale(i)
end
results = []
while resultset.next
row = {}
1.upto(column_count) do |i|
row[column_names[i].downcase] = convert_jdbc_type_to_ruby(i, column_types[i], column_scale[i], resultset)
end
results << row
end
results
end
def to_ruby_time(java_date)
if java_date
tm = java_date.getTime
Time.at(tm / 1000, (tm % 1000) * 1000)
end
end
def convert_jdbc_type_to_ruby(row, type, scale, resultset)
if scale != 0
decimal = resultset.getString(row)
decimal.to_f
else
case type
when Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR
resultset.getString(row)
when Types.SMALLINT, Types.INTEGER, Types.NUMERIC, Types.BIGINT
resultset.getInt(row)
when Types.BIT, Types.BOOLEAN, Types.TINYINT
resultset.getBoolean(row)
when Types.TIMESTAMP
to_ruby_time(resultset.getTimestamp(row))
when Types.TIME
to_ruby_time(resultset.getTime(row))
when Types.DATE
to_ruby_time(resultset.getDate(row))
else
types = Types.constants
name = types.find {|t| Types.const_get(t.to_sym) == type}
raise "jdbc_adapter: type #{name} not supported yet"
end
end
end
end
module RjbJdbcAdapterMethods
def initialize(connection, logger, config)
super(connection, logger)
@config = config
end
def adapter_name #:nodoc:
'JDBC'
end
def supports_migrations?
true
end
def native_database_types #:nodoc
@connection.native_database_types
end
def active?
true
end
def reconnect!
@connection.close rescue nil
@connection = RjbJdbcConnection.new(@config, @logger)
end
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
select(sql, name).first
end
def execute(sql, name = nil)
log_no_bench(sql, name) do
if sql =~ /^select/i
@connection.execute_query(sql)
else
@connection.execute_update(sql)
end
end
end
alias :update :execute
alias :delete :execute
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
log_no_bench(sql, name) do
id = @connection.execute_insert(sql, pk)
id_value || id
end
end
def columns(table_name, name = nil)
@connection.columns(table_name)
end
def tables
@connection.tables
end
def begin_db_transaction
@connection.begin
end
def commit_db_transaction
@connection.commit
end
def rollback_db_transaction
@connection.rollback
end
private
def select(sql, name)
log_no_bench(sql, name) { @connection.execute_query(sql) }
end
def log_no_bench(sql, name)
if block_given?
if @logger and @logger.level <= Logger::INFO
result = yield
log_info(sql, name, 0)
result
else
yield
end
else
log_info(sql, name, 0)
nil
end
rescue Exception => e
# Log message and raise exception.
message = "#{e.class.name}: #{e.message}: #{sql}"
log_info(message, name, 0)
raise ActiveRecord::StatementInvalid, message
end
end
class RjbJdbcAdapter < AbstractAdapter
include RjbJdbcAdapterMethods
end
end
end
!!prologue
I implement Rjb-JDBC adapter, based on JRuby jdbc adapter(gem search --remote jdbc). This is very mock up code. I have little incentive to more. Please test your self and use this code.
!!usage
* save such a place
* Open "active_record.rb", 'rjb_jdbc' append to RAILS_CONNECTION_ADAPTERS
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase rjb_jdbc)
* Edit
development:
adapter: rjb_jdbc
driver: org.apache.derby.jdbc.EmbeddedDriver
#driver: org.hsqldb.jdbcDriver
username: sa
password:
#url: jdbc:hsqldb:file:/testdb
url: jdbc:derby:/testdb;create=true
primary_key: int generated always as identity
#primary_key: integer generated by default as identity
catalog: nil
schema: SA
* CLASSPATH environment define somewhere( startup shell,
*
!!code
require 'active_record/connection_adapters/abstract_adapter'
require 'rjb'
# I not like this code.
module ActiveRecord
class Base
def self.rjb_jdbc_connection(config)
ConnectionAdapters::RjbJdbcAdapter.new(ConnectionAdapters::RjbJdbcConnection.new(config, logger), logger, config)
end
end
module ConnectionAdapters
# If support multi database connection, destined for trouble.
DriverManager = Rjb::import('java.sql.DriverManager')
Statement = Rjb::import('java.sql.Statement')
Types = Rjb::import('java.sql.Types')
# I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to
# use for ActiveRecord's Adapter#native_database_types in a database-independent way,
# but apparently a database driver can return multiple types for a given
# java.sql.Types constant. So this type converter uses some heuristics to try to pick
# the best (most common) type to use. It's not great, it would be better to just
# delegate to each database's existin AR adapter's native_database_types method, but I
# wanted to try to do this in a way that didn't pull in all the other adapters as
# dependencies. Suggestions appreciated.
class RjbJdbcTypeConverter
# The basic ActiveRecord types, mapped to an array of procs that are used to #select
# the best type. The procs are used as selectors in order until there is only one
# type left. If all the selectors are applied and there is still more than one
# type, an exception will be raised.
#TODO: incomplate const defined types
AR_TO_JDBC_TYPES = {
:string => [ proc {|r| Types.VARCHAR == r['data_type']},
proc {|r| r['type_name'] =~ /^varchar$/i} ],
:text => [ proc {|r| [Types.LONGVARCHAR, Types.CLOB].include?(r['data_type'])},
proc {|r| r['type_name'] =~ /^(text|clob)/i} ],
:integer => [ proc {|r| Types.INTEGER == r['data_type']},
proc {|r| r['type_name'] =~ /^integer/i} ],
:float => [ proc {|r| [Types.FLOAT,Types.DOUBLE].include?(r['data_type'])},
proc {|r| r['type_name'] =~ /^float/i},
proc {|r| r['type_name'] =~ /^double$/i} ],
:datetime => [ proc {|r| Types.TIMESTAMP == r['data_type']},
proc {|r| r['type_name'] =~ /^datetime/i} ],
:timestamp => [ proc {|r| Types.TIMESTAMP == r['data_type']},
proc {|r| r['type_name'] =~ /^timestamp/i},
proc {|r| r['type_name'] =~ /^datetime/i} ],
:time => [ proc {|r| Types.TIME == r['data_type']} ],
:date => [ proc {|r| Types.DATE == r['data_type']} ],
:binary => [ proc {|r| Types.LONGVARBINARY == r['data_type']},
proc {|r| r['type_name'] =~ /^blob/i} ],
:boolean => [ proc {|r| Types.TINYINT == r['data_type']},
proc {|r| Types.SMALLINT == r['data_type']},
proc {|r| Types.BOOLEAN == r['data_type']} ]
}
def initialize(types, logger)
@types = types
@logger = logger
end
def choose_best_types
type_map = {}
AR_TO_JDBC_TYPES.each_key do |k|
typerow = choose_type(k)
type_map[k] = { :name => typerow['type_name'] }
type_map[k][:limit] = typerow['precision'] if [:integer,:string].include?(k) and (typerow['fixed_prec_scale'] or typerow['create_params'])
type_map[k][:limit] = 1 if k == :boolean
end
@logger.debug(type_map.inspect)
type_map
end
def choose_type(ar_type)
@logger.debug(ar_type)
procs = AR_TO_JDBC_TYPES[ar_type]
@logger.debug(procs)
types = @types
@logger.debug(types.inspect)
procs.each do |p|
new_types = types.select(&p)
return new_types.first if new_types.length == 1
types = new_types if new_types.length > 0
end
raise "unable to choose type from: #{types.collect{|t| t['type_name']}.inspect}"
end
end
class RjbJdbcConnection
def initialize(config, logger)
config = config.symbolize_keys
driver = config[:driver].to_s
user = config[:username].to_s
pass = config[:password].to_s
url = config[:url].to_s
@config = config
@logger = logger
unless driver && url
raise ArgumentError, "jdbc adapter requires driver class and url"
end
DriverManager.registerDriver(Rjb::import(driver))
@connection = DriverManager.getConnection(url, user, pass)
set_native_database_types
end
def set_native_database_types
types = unmarshal_result(@connection.getMetaData.getTypeInfo)
@native_types = RjbJdbcTypeConverter.new(types, @logger).choose_best_types
end
def native_database_types
types = {
# TODO: this is copied from MySQL -- figure out how to
# generalize the primary key type
:primary_key => @config[:primary_key] || "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
}
@native_types.each_pair {|k,v| types[k] = v.inject({}) {|memo,kv| memo.merge({kv[0] => kv[1..-1]})}}
types
end
def columns(table_name, name = nil)
metadata = @connection.getMetaData
results = metadata.getColumns(@config[:catalog], @config[:schema], table_name, nil)
columns = []
unmarshal_result(results).each do |col|
columns << ActiveRecord::ConnectionAdapters::Column.new(col['column_name'], col['column_def'],
"#{col['type_name']}(#{col['column_size']})", col['is_nullable'] != 'NO')
end
columns
end
def tables
metadata = @connection.getMetaData
results = metadata.getTables(@config[:catalog], @config[:schema], nil, nil)
unmarshal_result(results).collect {|t| t['table_name']}
end
def execute_insert(sql, pk)
stmt = @connection.createStatement
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS)
row = unmarshal_result(stmt.getGeneratedKeys)
row.first && row.first.values.first
ensure
stmt.close
end
def execute_update(sql)
stmt = @connection.createStatement
stmt.executeUpdate(sql)
ensure
stmt.close
end
def execute_query(sql)
stmt = @connection.createStatement
unmarshal_result(stmt.executeQuery(sql))
ensure
stmt.close
end
def begin
@connection.setAutoCommit(false)
end
def commit
@connection.commit
ensure
@connection.setAutoCommit(true)
end
def rollback
@connection.rollback
ensure
@connection.setAutoCommit(true)
end
private
def unmarshal_result(resultset)
metadata = resultset.getMetaData
column_count = metadata.getColumnCount
column_names = ['']
column_types = ['']
column_scale = ['']
1.upto(column_count) do |i|
column_names << metadata.getColumnName(i)
column_types << metadata.getColumnType(i)
column_scale << metadata.getScale(i)
end
results = []
while resultset.next
row = {}
1.upto(column_count) do |i|
row[column_names[i].downcase] = convert_jdbc_type_to_ruby(i, column_types[i], column_scale[i], resultset)
end
results << row
end
results
end
def to_ruby_time(java_date)
if java_date
tm = java_date.getTime
Time.at(tm / 1000, (tm % 1000) * 1000)
end
end
def convert_jdbc_type_to_ruby(row, type, scale, resultset)
if scale != 0
decimal = resultset.getString(row)
decimal.to_f
else
case type
when Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR
resultset.getString(row)
when Types.SMALLINT, Types.INTEGER, Types.NUMERIC, Types.BIGINT
resultset.getInt(row)
when Types.BIT, Types.BOOLEAN, Types.TINYINT
resultset.getBoolean(row)
when Types.TIMESTAMP
to_ruby_time(resultset.getTimestamp(row))
when Types.TIME
to_ruby_time(resultset.getTime(row))
when Types.DATE
to_ruby_time(resultset.getDate(row))
else
types = Types.constants
name = types.find {|t| Types.const_get(t.to_sym) == type}
raise "jdbc_adapter: type #{name} not supported yet"
end
end
end
end
module RjbJdbcAdapterMethods
def initialize(connection, logger, config)
super(connection, logger)
@config = config
end
def adapter_name #:nodoc:
'JDBC'
end
def supports_migrations?
true
end
def native_database_types #:nodoc
@connection.native_database_types
end
def active?
true
end
def reconnect!
@connection.close rescue nil
@connection = RjbJdbcConnection.new(@config, @logger)
end
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
select(sql, name).first
end
def execute(sql, name = nil)
log_no_bench(sql, name) do
if sql =~ /^select/i
@connection.execute_query(sql)
else
@connection.execute_update(sql)
end
end
end
alias :update :execute
alias :delete :execute
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
log_no_bench(sql, name) do
id = @connection.execute_insert(sql, pk)
id_value || id
end
end
def columns(table_name, name = nil)
@connection.columns(table_name)
end
def tables
@connection.tables
end
def begin_db_transaction
@connection.begin
end
def commit_db_transaction
@connection.commit
end
def rollback_db_transaction
@connection.rollback
end
private
def select(sql, name)
log_no_bench(sql, name) { @connection.execute_query(sql) }
end
def log_no_bench(sql, name)
if block_given?
if @logger and @logger.level <= Logger::INFO
result = yield
log_info(sql, name, 0)
result
else
yield
end
else
log_info(sql, name, 0)
nil
end
rescue Exception => e
# Log message and raise exception.
message = "#{e.class.name}: #{e.message}: #{sql}"
log_info(message, name, 0)
raise ActiveRecord::StatementInvalid, message
end
end
class RjbJdbcAdapter < AbstractAdapter
include RjbJdbcAdapterMethods
end
end
end