You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ibatis.apache.org by cb...@apache.org on 2006/05/21 05:52:53 UTC

svn commit: r408127 - in /ibatis/trunk/rb/rbatis: ./ generators/ generators/rbatis/ generators/rbatis/templates/ lib/ lib/rbatis/ test/

Author: cbegin
Date: Sat May 20 20:52:53 2006
New Revision: 408127

URL: http://svn.apache.org/viewvc?rev=408127&view=rev
Log:
First import of iBATIS for Ruby (rBatis)

Added:
    ibatis/trunk/rb/rbatis/
    ibatis/trunk/rb/rbatis/generators/
    ibatis/trunk/rb/rbatis/generators/rbatis/
    ibatis/trunk/rb/rbatis/generators/rbatis/USAGE
    ibatis/trunk/rb/rbatis/generators/rbatis/rbatis_generator.rb
    ibatis/trunk/rb/rbatis/generators/rbatis/templates/
    ibatis/trunk/rb/rbatis/generators/rbatis/templates/model.rb
    ibatis/trunk/rb/rbatis/generators/rbatis/templates/unit_test.rb
    ibatis/trunk/rb/rbatis/init.rb
    ibatis/trunk/rb/rbatis/lib/
    ibatis/trunk/rb/rbatis/lib/rbatis/
    ibatis/trunk/rb/rbatis/lib/rbatis.rb
    ibatis/trunk/rb/rbatis/lib/rbatis/rails_integration.rb
    ibatis/trunk/rb/rbatis/lib/rbatis/sanitizer.rb
    ibatis/trunk/rb/rbatis/test/
    ibatis/trunk/rb/rbatis/test/rbatis_test.rb

Added: ibatis/trunk/rb/rbatis/generators/rbatis/USAGE
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/generators/rbatis/USAGE?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/generators/rbatis/USAGE (added)
+++ ibatis/trunk/rb/rbatis/generators/rbatis/USAGE Sat May 20 20:52:53 2006
@@ -0,0 +1,8 @@
+Description:
+    Generates a RBatis::Base stub.
+
+Examples:
+    ./script/generate rbatis order
+    will generate:
+		    /app/models/order.rb
+		    /test/unit/order.rb

Added: ibatis/trunk/rb/rbatis/generators/rbatis/rbatis_generator.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/generators/rbatis/rbatis_generator.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/generators/rbatis/rbatis_generator.rb (added)
+++ ibatis/trunk/rb/rbatis/generators/rbatis/rbatis_generator.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,16 @@
+class RbatisGenerator < Rails::Generator::NamedBase
+  def manifest
+    record do |m|
+      # Check for class naming collisions.
+      m.class_collisions class_path, class_name, "#{class_name}Test"
+
+      # Model, test, and fixture directories.
+      m.directory File.join('app/models', class_path)
+      m.directory File.join('test/unit', class_path)
+
+      # Model class, unit test, and fixtures.
+      m.template 'model.rb',      File.join('app/models', class_path, "#{file_name}.rb")
+      m.template 'unit_test.rb',  File.join('test/unit', class_path, "#{file_name}_test.rb")
+    end
+  end
+end

Added: ibatis/trunk/rb/rbatis/generators/rbatis/templates/model.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/generators/rbatis/templates/model.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/generators/rbatis/templates/model.rb (added)
+++ ibatis/trunk/rb/rbatis/generators/rbatis/templates/model.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,2 @@
+class <%= class_name %> < RBatis::Base
+end

Added: ibatis/trunk/rb/rbatis/generators/rbatis/templates/unit_test.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/generators/rbatis/templates/unit_test.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/generators/rbatis/templates/unit_test.rb (added)
+++ ibatis/trunk/rb/rbatis/generators/rbatis/templates/unit_test.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+  fixtures :<%= table_name %>
+
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end

Added: ibatis/trunk/rb/rbatis/init.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/init.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/init.rb (added)
+++ ibatis/trunk/rb/rbatis/init.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,2 @@
+require 'rbatis'
+require 'rbatis/rails_integration'

Added: ibatis/trunk/rb/rbatis/lib/rbatis.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/lib/rbatis.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/lib/rbatis.rb (added)
+++ ibatis/trunk/rb/rbatis/lib/rbatis.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,327 @@
+require 'rbatis/sanitizer'
+
+def Fixnum.from_database(record, column)
+  record[column].to_i
+end
+
+def String.from_database(record, column)
+  record[column].to_s
+end
+
+def Time.from_database(record, column)
+  Time.parse(record[column])
+end
+
+module RBatis
+  class BooleanMapper
+    def from_database(record, column)
+      return null if record[column].nil?
+      return true if record[column] == '1'
+      return false if record[column] == '0'
+      
+      raise "can't parse boolean value for column " + column
+    end
+  end
+
+  class Statement
+    attr_accessor :connection_provider
+    attr_reader :proc
+    attr_reader :execution_count
+    
+    include Sanitizer
+    
+    def initialize(params, &proc)
+      params.each{|k,v| send("#{k}=", v)}
+      @proc = proc
+      reset_statistics
+    end
+    
+    def connection
+      connection_provider.connection
+    end
+    
+    def execute(*args)
+      @execution_count += 1
+      sql = sanitize_sql(proc.call(*args))
+      do_execute(sql)
+    end
+        
+    def reset_statistics
+      @execution_count = 0
+    end
+    
+    def validate
+    end
+  end
+  
+  class SelectValue < Statement
+    attr_accessor :result_type
+    
+    def do_execute(sql)
+      raise "result_type must be specified" unless result_type
+      record = connection.select_one(sql)
+      result_type.from_database(record, record.keys.first)
+    end
+  end
+  
+  class Insert < Statement
+    def do_execute(sql)
+      connection.insert(sql)
+    end
+  end
+
+  class Delete < Statement
+    def do_execute(sql)
+      connection.delete(sql)
+    end
+  end
+  
+  class Update < Statement
+    def do_execute(sql)
+      connection.update(sql)
+    end
+  end
+
+  class Select < Statement
+    attr_accessor :resultmap
+    
+    def do_execute(sql)
+      connection.select_all(sql).collect{|record| resultmap.map(record)}.uniq
+    end
+
+    def validate
+      raise 'resultmap has not been specified' unless resultmap
+    end
+  end
+
+  class SelectOne < Statement
+    attr_accessor :resultmap
+    
+    def do_execute(sql)
+      record = connection.select_one(sql)
+      return nil unless record
+      resultmap.map(record)
+    end
+  end
+  
+  class Statement
+    SHORTCUTS = {
+      :select => Select,
+      :select_one => SelectOne,
+      :select_value => SelectValue,
+      :insert => Insert,
+      :delete => Delete,
+      :update => Update,
+    }
+  end
+  
+  class ResultMap
+    attr_reader :fields
+    attr_reader :factory
+    
+    def initialize(factory, fields)
+      @factory = factory
+      @fields = {}
+      fields.each do |name, field_spec|
+        if field_spec.is_a?(Array)
+          @fields[name] = Column.new(*field_spec)
+        else
+          @fields[name] = field_spec
+        end
+        @fields[name].name = name
+      end
+    end
+    
+    def map(record)
+      hydrate(factory.get_or_allocate(self, record), record)
+    end
+    
+    def hydrate(result, record)
+      fields.each_value{|f| f.map(record, result)}
+      result.on_load if result.respond_to?(:on_load)
+      result
+    end
+    
+    def value_of(name, record)
+      fields[name].value(record)
+    end
+    
+    # Creates a new ResultMap that is identical to the previous one
+    # except that all columns are prefixed with the specified +prefix+.
+    # Use with EagerAssociation to fetch associated items from an OUTER JOIN fetch
+    # to accomplish eager loading and avoiding the N+1 select problem.
+    def prefix(prefix)
+      ResultMap.new(factory, fields.collect{|n,f| [n, f.prefix(prefix)]})
+    end
+    
+    # Creates a new ResultMap containing all the same fields except those overriden
+    # by +fields+.
+    def extend(overriding_fields)
+      ResultMap.new(factory, fields.merge(overriding_fields))
+    end
+    
+    def all_nil?(record)
+      fields.each_value{|f| return false if !f.value(record).nil?}
+      return true
+    end
+  end
+  
+  class Column
+    attr_accessor :name
+    attr_reader :column
+    attr_reader :type
+
+    def initialize(column, type)
+      @column = column
+      @type = type
+    end
+    
+    def map(record, result)
+      result.instance_variable_set("@#{name}".to_sym, value(record))
+    end
+    
+    def value(record)
+      type.from_database(record, column)
+    end
+    
+    # Creates a new column mapping with the column name prefixed with +prefix+.
+    def prefix(prefix)
+      self.class.new(prefix + column, type)
+    end
+  end
+  
+  class LazyLoadProxy
+    def initialize(loader, container)
+      @loader = loader
+      @container = container
+    end
+    
+    def method_missing(name, *args, &proc)
+      maybe_load
+      @target.send(name, *args, &proc)
+    end
+    
+    def to_s
+      maybe_load
+      @target.to_s
+    end
+    
+    private
+    
+    def maybe_load
+      @target = load if !defined?(@target)
+    end
+    
+    def load
+      @loader.load(@container)
+    end
+  end
+
+  class LazyAssociation
+    attr_accessor :name
+
+    def initialize(options={}, &loader)
+      @options = options
+      @options[:keys] = @options[:keys] || [@options[:key]]
+      @loader = loader
+    end
+    
+    def map(record, result)
+      # association has already been loaded, don't overwrite with proxy
+      return if result.instance_variable_get("@#{name}".to_sym)
+      
+      result.instance_variable_set("@#{name}".to_sym, LazyLoadProxy.new(self, result))
+    end
+    
+    def load(container)
+      return @loader.call if @loader
+      
+      keys = @options[:keys].collect{|key| container.instance_variable_get("@#{key}".to_sym)}
+      @options[:to].send(@options[:select], *keys)
+    end
+  end
+  
+  class EagerAssociation
+    attr_accessor :name
+    attr_reader :resultmap
+    
+    def initialize(resultmap)
+      @resultmap = resultmap
+    end
+    
+    def map(record, result)
+      ary = result.instance_variable_get("@#{name}".to_sym)
+      ary = [] if ary.nil?
+      ary << resultmap.map(record) unless resultmap.all_nil?(record)
+      result.instance_variable_set("@#{name}".to_sym, ary)
+    end
+  end
+
+  module Repository
+    
+    def self.included(included_into)
+      included_into.instance_variable_set(:@resultmaps, {})
+      included_into.instance_variable_set(:@statements, {})
+      class <<included_into
+
+        def statements
+          @statements
+        end
+        
+        alias selects statements
+        alias inserts statements
+        alias updates statements
+        
+        def resultmaps
+          @resultmaps
+        end
+
+        def maps(cls)
+          @maps = cls
+        end
+        
+        def mapped_class
+          @maps || self
+        end
+        
+        def boolean
+          BooleanMapper.new
+        end
+        
+        def get_or_allocate(recordmap, record)
+          mapped_class.allocate
+        end
+        
+        def resultmap(name = :default, fields = {})
+          resultmaps[name] = ResultMap.new(self, fields)
+        end
+
+        def extend_resultmap(name, base, fields)
+          resultmaps[name] = base.extend(fields)
+        end
+        
+        def statement(statement_type, name = statement_type, params = {}, &proc)
+          statement_type = Statement::SHORTCUTS[statement_type] unless statement_type.respond_to?(:new)
+          statement = statement_type.new(params, &proc)
+          statement.connection_provider = self
+          statement.resultmap = resultmaps[:default] if statement.respond_to?(:resultmap=) && statement.resultmap.nil?
+          statement.validate
+          statements[name] = statement
+          eval <<-EVAL
+            def #{name}(*args)
+              statements[:#{name}].execute(*args)
+            end
+          EVAL
+        end
+
+        def create(*args, &proc)
+          mapped_class.new(*args, &proc)
+        end
+        
+        def reset_statistics
+          selects.each_value{|s| s.reset_statistics}
+        end
+      end
+    end
+  end
+end

Added: ibatis/trunk/rb/rbatis/lib/rbatis/rails_integration.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/lib/rbatis/rails_integration.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/lib/rbatis/rails_integration.rb (added)
+++ ibatis/trunk/rb/rbatis/lib/rbatis/rails_integration.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,74 @@
+class Dispatcher
+  def reset_application!
+    Controllers.clear!
+    Dependencies.clear
+    ActiveRecord::Base.reset_subclasses
+    Dependencies.remove_subclasses_for(ActiveRecord::Base, ActiveRecord::Observer, ActionController::Base)
+    Dependencies.remove_subclasses_for(RBatis::Base)
+    Dependencies.remove_subclasses_for(ActionMailer::Base) if defined?(ActionMailer::Base)
+  end
+end
+
+module RBatis
+  class Base
+    include Repository
+    include ::Reloadable::Subclasses
+  
+    cattr_accessor :logger
+    
+    def initialize(attributes={})
+      self.attributes = attributes
+    end
+    
+    def attributes=(attributes)
+      attributes.each do |key, value|
+        send("#{key}=", value)
+      end
+    end
+  
+    def self.inherited(inherited_by)
+      RBatis::Repository.included(inherited_by)
+      class <<inherited_by
+        def connection
+          ActiveRecord::Base.connection
+        end
+
+        def human_attribute_name(field)
+          field
+        end
+      end
+    end
+    
+    def save!
+      save
+    end
+    
+    def save
+      if new_record?
+        id = self.class.insert(self)
+        @new_record = false
+        id
+      else
+        self.class.update(self)
+      end
+    end
+    
+    def update_attribute(name, value)
+      send(name.to_s + '=', value)
+      save
+    end
+    
+    def on_load
+      @new_record = false
+    end
+    
+    def new_record?
+      return true if not defined?(@new_record)
+      @new_record
+    end
+
+    include ActiveRecord::Validations
+  end
+end
+
+RBatis::Base.logger = ActiveRecord::Base.logger
\ No newline at end of file

Added: ibatis/trunk/rb/rbatis/lib/rbatis/sanitizer.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/lib/rbatis/sanitizer.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/lib/rbatis/sanitizer.rb (added)
+++ ibatis/trunk/rb/rbatis/lib/rbatis/sanitizer.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,64 @@
+module RBatis
+  module Sanitizer
+    # Accepts an array or string.  The string is returned untouched, but the array has each value
+    # sanitized and interpolated into the sql statement.
+    #   ["name='%s' and group_id='%s'", "foo'bar", 4]  returns  "name='foo''bar' and group_id='4'"
+    def sanitize_sql(ary)
+      return ary unless ary.is_a?(Array)
+
+      statement, *values = ary
+      if values.first.is_a?(Hash) and statement =~ /:\w+/
+        replace_named_bind_variables(statement, values.first)
+      elsif statement.include?('?')
+        replace_bind_variables(statement, values)
+      else
+        statement % values.collect { |value| connection.quote_string(value.to_s) }
+      end
+    end
+
+    module_function :sanitize_sql
+
+    def replace_bind_variables(statement, values)
+      raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
+      bound = values.dup
+      statement.gsub('?') { quote_bound_value(bound.shift) }
+    end
+
+    def replace_named_bind_variables(statement, bind_vars)
+      raise_if_bind_arity_mismatch(statement, statement.scan(/:(\w+)/).uniq.size, bind_vars.size)
+      statement.gsub(/:(\w+)/) do
+        match = $1.to_sym
+        if bind_vars.has_key?(match)
+          quote_bound_value(bind_vars[match])
+        else
+          raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+        end
+      end
+    end
+
+    def quote_bound_value(value)
+      case value
+        when Array
+          value.map { |v| connection.quote(v) }.join(',')
+        else
+          connection.quote(value)
+      end
+    end
+
+    def raise_if_bind_arity_mismatch(statement, expected, provided)
+      unless expected == provided
+        raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
+      end
+    end
+
+    def extract_options_from_args!(args)
+      if args.last.is_a?(Hash) then args.pop else {} end
+    end
+
+    def encode_quoted_value(value)
+      quoted_value = connection.quote(value)
+      quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'")          
+      quoted_value
+    end
+  end
+end
\ No newline at end of file

Added: ibatis/trunk/rb/rbatis/test/rbatis_test.rb
URL: http://svn.apache.org/viewvc/ibatis/trunk/rb/rbatis/test/rbatis_test.rb?rev=408127&view=auto
==============================================================================
--- ibatis/trunk/rb/rbatis/test/rbatis_test.rb (added)
+++ ibatis/trunk/rb/rbatis/test/rbatis_test.rb Sat May 20 20:52:53 2006
@@ -0,0 +1,161 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../../rails/activerecord/lib/active_record')
+require 'test/unit'
+require 'test/unit/ui/console/testrunner'
+$: << File.dirname(__FILE__) + '/../lib/'
+require 'rbatis'
+require 'rbatis/rails_integration'
+
+RAILS_ENV = "development"
+ActiveRecord::Base.configurations = {
+  RAILS_ENV => {
+    'adapter' => 'sqlite3',
+    'database' => ":memory:"
+  }
+}
+ActiveRecord::Base.establish_connection
+
+ActiveRecord::Base.connection.create_table :cars do |t|
+  t.column :make, :string
+  t.column :owner_id, :integer
+end
+ActiveRecord::Base.connection.create_table :people do |t|
+  t.column :name, :string
+end
+ActiveRecord::Base.connection.insert("INSERT INTO people (id, name) VALUES (1, 'Jon Tirsen')")
+ActiveRecord::Base.connection.insert("INSERT INTO people (id, name) VALUES (2, 'Asa Holmstrom')")
+ActiveRecord::Base.connection.insert("INSERT INTO people (id, name) VALUES (3, 'Ben Hogan')")
+ActiveRecord::Base.connection.insert("INSERT INTO cars (id, make, owner_id) VALUES (1, 'Honda', 1)")
+ActiveRecord::Base.connection.insert("INSERT INTO cars (id, make, owner_id) VALUES (2, 'Audi', 1)")
+ActiveRecord::Base.connection.insert("INSERT INTO cars (id, make, owner_id) VALUES (3, 'Hyundai', 3)")
+
+
+class Car < RBatis::Base
+  attr_reader :make
+
+  def to_s
+    @make
+  end
+  
+  resultmap :default, 
+    :id => ["id", Fixnum],
+    :make => ["make", String]
+  statement :select, :find_by_owner_id do |person_id|
+    ["SELECT * FROM cars WHERE owner_id = ?", person_id]
+  end
+end
+
+class Person < RBatis::Base
+  attr_reader :person_id
+  attr_accessor :name
+  attr_accessor :cars
+  
+  def initialize(name)
+    @name = name
+  end
+  
+  def to_s
+    "#{@name}(#{@cars.join(',')})"
+  end
+  
+  resultmap :default,
+    :person_id => ["id", Fixnum],
+    :name => ["name", String],
+    :cars => RBatis::LazyAssociation.new(:to => Car, :select => :find_by_owner_id, :key => :person_id)
+  
+  extend_resultmap :fetch_cars, resultmaps[:default],
+    :cars => RBatis::EagerAssociation.new(Car.resultmaps[:default].prefix("car_"))
+  
+  statement :select, :find_all do 
+    "SELECT * FROM people ORDER BY id"
+  end
+
+  statement :select_one, :find_by_id do |id|
+    ["SELECT * FROM people WHERE id = ?", id]
+  end
+
+  statement :select, :find_all_fetch_cars, :resultmap => Car.resultmaps[:fetch_cars] do %{
+    SELECT p.*, c.id car_id, c.make car_make
+    FROM people p
+    LEFT OUTER JOIN cars c ON p.id = c.owner_id
+  } end
+  
+  statement :insert do |person|
+    ["INSERT INTO people (name) VALUES (?)", person.name]
+  end
+
+  statement :update do |person|
+    ["UPDATE people SET name = ? WHERE id = ?", person.name, person.person_id]
+  end
+  
+  statement :delete do |person|
+    ["DELETE FROM people WHERE id = ?", person.person_id]
+  end
+
+  statement :delete, :delete_temporary_data do
+    ["DELETE FROM people WHERE id > ?", 3]
+  end
+end
+
+class RBatisTest < Test::Unit::TestCase
+  def setup
+    Person.reset_statistics
+    Car.reset_statistics
+  end
+
+  def test_lazy_load
+    assert_correct_people_and_cars(Person.find_all)
+    assert_equal(1, Person.selects[:find_all].execution_count)
+    assert_equal(3, Car.selects[:find_by_owner_id].execution_count)
+  end
+
+  def doesnt_work_yet_test_eager_load
+    assert_correct_people_and_cars(Person.find_all_fetch_cars)    
+  
+    assert_equal(1, Person.selects[:find_all_fetch_cars].execution_count)
+    assert_equal(0, Car.selects[:find_by_owner_id].execution_count)
+  end
+
+  def doesnt_work_yet_test_load_twice_gives_same_object
+    RBatis::Base.transaction do
+      assert_all_same(PersonRepository.find_all, PersonRepository.find_all)
+    end
+  end
+  
+  def test_create_update_and_delete
+    person = Person.new("Jon Tirsen")
+    id = person.save
+    assert(!person.new_record?)
+    assert(id > 0)
+
+    person = Person.find_by_id(id)
+    assert_equal("Jon Tirsen", person.name)
+    person.name = "Julian Boot"
+    person.save
+
+    person = Person.find_by_id(id)
+    assert_equal("Julian Boot", person.name)
+    Person.delete(person)
+
+    assert_nil(Person.find_by_id(id))
+  end
+
+  def assert_all_same(ary1, ary2)
+    ary1.each_with_index do |el1, index|
+      assert_same(el1, ary2[index])
+    end
+  end
+  
+  def assert_correct_people_and_cars(all)
+    assert_equal('Jon Tirsen', all[0].name)
+    assert_equal(2, all[0].cars.size)
+    assert_equal('Honda', all[0].cars[0].make)
+    assert_equal('Audi', all[0].cars[1].make)
+    assert_equal('Asa Holmstrom', all[1].name)
+    assert_equal(0, all[1].cars.size)
+    assert_equal('Ben Hogan', all[2].name)
+    assert_equal(1, all[2].cars.size)
+    assert_equal('Hyundai', all[2].cars[0].make)
+    p all.collect{|p| p.name}
+    assert_equal(3, all.size)
+  end
+end