Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

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
Last modified:2007/04/14 01:40:54
Keyword(s):
References: