1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Added that Base#find takes an optional options hash, including :conditions. Base#find_on_conditions deprecated in favor of #find with :conditions #407 [bitsweat]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@305 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson 2005-01-01 19:50:23 +00:00
parent 86df396491
commit 6bd672eb0d
8 changed files with 247 additions and 135 deletions

View file

@ -1,5 +1,7 @@
*SVN*
* Added that Base#find takes an optional options hash, including :conditions. Base#find_on_conditions deprecated in favor of #find with :conditions #407 [bitsweat]
* Added a db2 adapter that only depends on the Ruby/DB2 bindings (http://raa.ruby-lang.org/project/ruby-db2/) #386 [Maik Schmidt]
* Added the final touches to the Microsoft SQL Server adapter by DeLynn Berry that makes it suitable for actual use #394 [DeLynn Barry]

View file

@ -100,13 +100,25 @@ module ActiveRecord
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
def sanitize_sql(sql)
@association_class.send(:sanitize_sql, sql)
end
def extract_options_from_args!(args)
@owner.send(:extract_options_from_args!, args)
end
private
def load_collection
begin
@collection = find_all_records unless loaded?
rescue ActiveRecord::RecordNotFound
@collection = []
if loaded?
@collection
else
begin
@collection = find_all_records
rescue ActiveRecord::RecordNotFound
@collection = []
end
end
end
@ -114,25 +126,10 @@ module ActiveRecord
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
end
def load_collection_to_array
return unless @collection_array.nil?
begin
@collection_array = find_all_records
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
@collection_array = []
end
end
def duplicated_records_array(records)
records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
records.dup
end
# Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
end
end
end
end
end

View file

@ -38,15 +38,42 @@ module ActiveRecord
self
end
def find(association_id = nil, &block)
if block_given? || @options[:finder_sql]
load_collection
@collection.find(&block)
else
if loaded?
find_all { |record| record.id == association_id.to_i }.first
def find_first
load_collection.first
end
def find(*args)
# Return an Array if multiple ids are given.
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq
# If no block is given, raise RecordNotFound.
if ids.empty?
raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID#{conditions}"
# If using a custom finder_sql, scan the entire collection.
elsif @options[:finder_sql]
if ids.size == 1
id = ids.first
record = load_collection.detect { |record| id == record.id }
expects_array? ? [record] : record
else
find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = #{@owner.send(:quote, association_id)} ORDER BY")).first
load_collection.select { |record| ids.include?(record.id) }
end
# Otherwise, construct a query.
else
ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
records = find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
if records.size == ids.size
if ids.size == 1 and !expects_array
records.first
else
records
end
else
raise RecordNotFound, "Couldn't find #{@association_class.name} with ID in (#{ids_list})"
end
end
end
@ -70,10 +97,9 @@ module ActiveRecord
records = @association_class.find_by_sql(sql)
@options[:uniq] ? uniq(records) : records
end
def count_records
load_collection
@collection.size
load_collection.size
end
def insert_record(record)

View file

@ -3,12 +3,13 @@ module ActiveRecord
class HasManyAssociation < AssociationCollection #:nodoc:
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
super(owner, association_name, association_class_name, association_class_primary_key_name, options)
@conditions = @association_class.send(:sanitize_conditions, options[:conditions])
@conditions = sanitize_sql(options[:conditions])
if options[:finder_sql]
@finder_sql = interpolate_sql(options[:finder_sql])
else
@finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id} #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
@finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
@finder_sql << " AND #{@conditions}" if @conditions
end
if options[:counter_sql]
@ -35,29 +36,46 @@ module ActiveRecord
record
end
def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
if block_given? || @options[:finder_sql]
load_collection
@collection.find_all(&block)
def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
if @options[:finder_sql]
records = @association_class.find_by_sql(@finder_sql)
else
@association_class.find_all(
"#{@association_class_primary_key_name} = #{@owner.quoted_id}" +
"#{@conditions ? " AND " + @conditions : ""}#{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
orderings,
limit,
joins
)
sql = @finder_sql
sql << " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
orderings ||= @options[:order]
records = @association_class.find_all(sql, orderings, limit, joins)
end
end
def find(association_id = nil, &block)
if block_given? || @options[:finder_sql]
load_collection
@collection.find(&block)
# Find the first associated record. All arguments are optional.
def find_first(conditions = nil, orderings = nil)
find_all(conditions, orderings, 1).first
end
def find(*args)
# Return an Array if multiple ids are given.
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq
# If no ids given, raise RecordNotFound.
if ids.empty?
raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID"
# If using a custom finder_sql, scan the entire collection.
elsif @options[:finder_sql]
if ids.size == 1
id = ids.first
record = load_collection.detect { |record| id == record.id }
expects_array? ? [record] : record
else
load_collection.select { |record| ids.include?(record.id) }
end
# Otherwise, delegate to association class with conditions.
else
@association_class.find_on_conditions(association_id,
"#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + @conditions : ""}"
)
args << { :conditions => "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}" }
@association_class.find(*args)
end
end
@ -71,11 +89,7 @@ module ActiveRecord
protected
def find_all_records
if @options[:finder_sql]
@association_class.find_by_sql(@finder_sql)
else
@association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
end
find_all
end
def count_records

View file

@ -236,44 +236,58 @@ module ActiveRecord #:nodoc:
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
# Person.find([1]) # returns an array for objects the object with ID = 1
#
# The last argument may be a Hash of find options. Currently, +conditions+ is the only option, behaving the same as with +find_all+.
# Person.find(1, :conditions => "associate_id='5'"
# Person.find(1, 2, 6, :conditions => "status='active'"
# Person.find([7, 17], :conditions => ["sanitize_me='%s'", "bare'quote"]
#
# +RecordNotFound+ is raised if no record can be found.
def find(*ids)
expects_array = ids.first.kind_of?(Array)
ids = ids.flatten.compact.uniq
def find(*args)
# Return an Array if ids are passed in an Array.
expects_array = args.first.kind_of?(Array)
if ids.length > 1
ids_list = ids.map{ |id| "#{sanitize(id)}" }.join(", ")
objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
# Extract options hash from argument list.
options = extract_options_from_args!(args)
conditions = " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
if objects.length == ids.length
return objects
ids = args.flatten.compact.uniq
case ids.size
# Raise if no ids passed.
when 0
raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
# Find a single id.
when 1
unless result = find_first("#{primary_key} = #{sanitize(ids.first)}#{conditions}")
raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
end
# Box result if expecting array.
expects_array ? [result] : result
# Find multiple ids.
else
raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})"
end
elsif ids.length == 1
id = ids.first
sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = #{sanitize(id)}"
sql << " AND #{type_condition}" unless descends_from_active_record?
if record = connection.select_one(sql, "#{name} Find")
expects_array ? [instantiate(record)] : instantiate(record)
else
raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
end
else
raise RecordNotFound, "Couldn't find #{name} without an ID"
ids_list = ids.map { |id| sanitize(id) }.join(',')
result = find_all("#{primary_key} IN (#{ids_list})#{conditions}", primary_key)
if result.size == ids.size
result
else
raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})#{conditions}"
end
end
end
# This method is deprecated in favor of find with the :conditions option.
# Works like find, but the record matching +id+ must also meet the +conditions+.
# +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
# Example:
# Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
def find_on_conditions(id, conditions)
find_first("#{primary_key} = #{sanitize(id)} AND #{sanitize_conditions(conditions)}") ||
raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
def find_on_conditions(ids, conditions)
find(ids, :conditions => conditions)
end
# Returns an array of all the objects that could be instantiated from the associated
# table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
# such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
@ -287,7 +301,7 @@ module ActiveRecord #:nodoc:
add_conditions!(sql, conditions)
sql << "ORDER BY #{orderings} " unless orderings.nil?
connection.add_limit!(sql, sanitize_conditions(limit)) unless limit.nil?
connection.add_limit!(sql, sanitize_sql(limit)) unless limit.nil?
find_by_sql(sql)
end
@ -296,8 +310,7 @@ module ActiveRecord #:nodoc:
# Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
# Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
def find_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
connection.select_all(sanitize_sql(sql), "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
end
# Returns the object for the first record responding to the conditions in +conditions+,
@ -306,14 +319,7 @@ module ActiveRecord #:nodoc:
# +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
# Employee.find_first "income > 50000", "income DESC, name"
def find_first(conditions = nil, orderings = nil)
sql = "SELECT * FROM #{table_name} "
add_conditions!(sql, conditions)
sql << "ORDER BY #{orderings} " unless orderings.nil?
connection.add_limit!(sql, 1)
record = connection.select_one(sql, "#{name} Load First")
instantiate(record) unless record.nil?
find_all(conditions, orderings, 1).first
end
# Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
@ -613,7 +619,7 @@ module ActiveRecord #:nodoc:
# Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
def add_conditions!(sql, conditions)
sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
sql << "WHERE #{sanitize_sql(conditions)} " unless conditions.nil?
sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
end
@ -656,51 +662,49 @@ module ActiveRecord #:nodoc:
end
end
# Accepts either a condition array or string. The string is returned untouched, but the array has each of
# the condition values sanitized.
def sanitize_conditions(conditions)
return conditions unless conditions.is_a?(Array)
# 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 = conditions
if values[0].is_a?(Hash) && statement =~ /:\w+/
replace_named_bind_variables(statement, values[0])
elsif statement =~ /\?/
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
alias_method :sanitize_conditions, :sanitize_sql
def replace_bind_variables(statement, values)
orig_statement = statement.clone
expected_number_of_variables = statement.count('?')
provided_number_of_variables = values.size
unless expected_number_of_variables == provided_number_of_variables
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided_number_of_variables} for #{expected_number_of_variables})"
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided_number_of_variables} for #{expected_number_of_variables}) in: #{statement}"
end
until values.empty?
statement.sub!(/\?/, encode_quoted_value(values.shift))
end
statement.gsub('?') { |all, match| connection.quote(values.shift) }
bound = values.dup
statement.gsub('?') { connection.quote(bound.shift) }
end
def replace_named_bind_variables(statement, values_hash)
orig_statement = statement.clone
values_hash.keys.each do |k|
if statement.sub!(/:#{k.id2name}/, encode_quoted_value(values_hash.delete(k))).nil?
raise PreparedStatementInvalid, ":#{k} is not a variable in [#{orig_statement}]"
def replace_named_bind_variables(statement, bind_vars)
statement.gsub(/:(\w+)/) do
match = $1.to_sym
if bind_vars.has_key?(match)
connection.quote(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
if statement =~ /(:\w+)/
raise PreparedStatementInvalid, "No value provided for #{$1} in [#{orig_statement}]"
end
return statement
def extract_options_from_args!(args)
if args.last.is_a?(Hash) then args.pop else {} end
end
def encode_quoted_value(value)

View file

@ -183,9 +183,31 @@ class HasManyAssociationsTest < Test::Unit::TestCase
assert_equal 0, Firm.find_first.clients_using_zero_counter_sql.size
end
def test_find_ids
firm = Firm.find_first
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
client = firm.clients.find(2)
assert_kind_of Client, client
client_ary = firm.clients.find([2])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
client_ary = firm.clients.find(2, 3)
assert_kind_of Array, client_ary
assert_equal 2, client_ary.size
assert_equal client, client_ary.first
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
def test_find_all
assert_equal 2, Firm.find_first.clients.find_all("type = 'Client'").length
assert_equal 1, Firm.find_first.clients.find_all("name = 'Summit'").length
firm = Firm.find_first
assert_equal firm.clients, firm.clients.find_all
assert_equal 2, firm.clients.find_all("type = 'Client'").length
assert_equal 1, firm.clients.find_all("name = 'Summit'").length
end
def test_find_all_sanitized
@ -193,9 +215,18 @@ class HasManyAssociationsTest < Test::Unit::TestCase
assert_equal firm.clients.find_all("name = 'Summit'"), firm.clients.find_all(["name = '%s'", "Summit"])
end
def test_find_first
firm = Firm.find_first
assert_equal firm.clients.first, firm.clients.find_first
assert_equal Client.find(2), firm.clients.find_first("type = 'Client'")
end
def test_find_first_sanitized
assert_equal Client.find(2), Firm.find_first.clients.find_first(["type = ?", "Client"])
end
def test_find_in_collection
assert_equal Client.find(2).name, @signals37.clients.find(2).name
assert_equal Client.find(2).name, @signals37.clients.find {|c| c.name == @signals37.clients.find(2).name }.name
assert_raises(ActiveRecord::RecordNotFound) { @signals37.clients.find(6) }
end

View file

@ -286,9 +286,11 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
natural = Client.create("name" => "Natural Company")
apple.clients << natural
assert_equal apple.id, natural.firm_id
assert_equal Client.find(natural.id), Firm.find(apple.id).clients.find { |c| c.id == natural.id }
assert_equal Client.find(natural.id), Firm.find(apple.id).clients.find(natural.id)
apple.clients.delete natural
assert_nil Firm.find(apple.id).clients.find { |c| c.id == natural.id }
assert_raises(ActiveRecord::RecordNotFound) {
Firm.find(apple.id).clients.find(natural.id)
}
end
def test_natural_adding_of_has_and_belongs_to_many
@ -299,17 +301,21 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
rails.developers << john
rails.developers << mike
assert_equal Developer.find(john.id), Project.find(rails.id).developers.find { |d| d.id == john.id }
assert_equal Developer.find(mike.id), Project.find(rails.id).developers.find { |d| d.id == mike.id }
assert_equal Project.find(rails.id), Developer.find(mike.id).projects.find { |p| p.id == rails.id }
assert_equal Project.find(rails.id), Developer.find(john.id).projects.find { |p| p.id == rails.id }
assert_equal Developer.find(john.id), Project.find(rails.id).developers.find(john.id)
assert_equal Developer.find(mike.id), Project.find(rails.id).developers.find(mike.id)
assert_equal Project.find(rails.id), Developer.find(mike.id).projects.find(rails.id)
assert_equal Project.find(rails.id), Developer.find(john.id).projects.find(rails.id)
ap.developers << john
assert_equal Developer.find(john.id), Project.find(ap.id).developers.find { |d| d.id == john.id }
assert_equal Project.find(ap.id), Developer.find(john.id).projects.find { |p| p.id == ap.id }
assert_equal Developer.find(john.id), Project.find(ap.id).developers.find(john.id)
assert_equal Project.find(ap.id), Developer.find(john.id).projects.find(ap.id)
ap.developers.delete john
assert_nil Project.find(ap.id).developers.find { |d| d.id == john.id }
assert_nil Developer.find(john.id).projects.find { |p| p.id == ap.id }
assert_raises(ActiveRecord::RecordNotFound) {
Project.find(ap.id).developers.find(john.id)
}
assert_raises(ActiveRecord::RecordNotFound) {
Developer.find(john.id).projects.find(ap.id)
}
end
def test_storing_in_pstore

View file

@ -77,6 +77,11 @@ class FinderTest < Test::Unit::TestCase
end
def test_find_on_conditions
assert Topic.find(1, :conditions => "approved = 0")
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => "approved = 1") }
end
def test_deprecated_find_on_conditions
assert Topic.find_on_conditions(1, "approved = 0")
assert_raises(ActiveRecord::RecordNotFound) { Topic.find_on_conditions(1, "approved = 1") }
end
@ -111,7 +116,19 @@ class FinderTest < Test::Unit::TestCase
assert Company.find_first(["name = :name", {:name => "37signals' go'es agains"}])
end
def test_bind_arity
assert_nothing_raised { bind '' }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_nothing_raised { bind '?', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
assert_equal '1', bind(':a', :a => 1) # ' ruby-mode
assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode
assert_kind_of Firm, Company.find_first(["name = :name", { :name => "37signals" }])
assert_nil Company.find_first(["name = :name", { :name => "37signals!" }])
assert_nil Company.find_first(["name = :name", { :name => "37signals!' OR 1=1" }])
@ -124,7 +141,13 @@ class FinderTest < Test::Unit::TestCase
}
end
def test_named_bind_arity
assert_nothing_raised { bind '', {} }
assert_nothing_raised { bind '', :a => 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind ':a', {} } # ' ruby-mode
assert_nothing_raised { bind ':a', :a => 1 } # ' ruby-mode
assert_nothing_raised { bind ':a', :a => 1, :b => 2 } # ' ruby-mode
end
def test_string_sanitation
assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
@ -142,4 +165,13 @@ class FinderTest < Test::Unit::TestCase
assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)
ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
else
ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
end
end
end