Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

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 place that(適当に保存してください。) 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.(ファイル名を解決するヒントを直接書いてください)constant.
  RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase rjb_jdbc)

* Edit config/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