RjbJdbcAdapter
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.
usage
- save such a place 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_ADAPTERS constant.
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase rjb_jdbc)
- Edit 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).
- 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
Keyword(s):
References: