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