Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH] Added find_or_create_by_X as a second type of dynamic finder that'll create the record if it doesn't already exist [DHH] Added constrain scoping for creates using a hash of attributes bound to the :creation key [DHH]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2872 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
4506a463e4
commit
a5a82d978b
@ -1,5 +1,44 @@
|
||||
*SVN*
|
||||
|
||||
* Added constrain scoping for creates using a hash of attributes bound to the :creation key [DHH]. Example:
|
||||
|
||||
Comment.constrain(:creation => { :post_id => 5 }) do
|
||||
# Associated with :post_id
|
||||
Comment.create :body => "Hello world"
|
||||
end
|
||||
|
||||
This is rarely used directly, but allows for find_or_create on associations. So you can do:
|
||||
|
||||
# If the tag doesn't exist, a new one is created that's associated with the person
|
||||
person.tags.find_or_create_by_name("Summer")
|
||||
|
||||
* Added find_or_create_by_X as a second type of dynamic finder that'll create the record if it doesn't already exist [DHH]. Example:
|
||||
|
||||
# No 'Summer' tag exists
|
||||
Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
|
||||
|
||||
# Now the 'Summer' tag does exist
|
||||
Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
|
||||
|
||||
* Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH]. Example:
|
||||
|
||||
class Account < ActiveRecord::Base
|
||||
has_many :people, :extend => Module.new {
|
||||
def find_or_create_by_name(name)
|
||||
first_name, *last_name = name.split
|
||||
last_name = last_name.join " "
|
||||
|
||||
find_or_create_by_first_name_and_last_name(first_name, last_name)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
|
||||
person.first_name # => "David"
|
||||
person.last_name # => "Heinemeier Hansson"
|
||||
|
||||
Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation).
|
||||
|
||||
* Omit internal dtproperties table from SQLServer table list. #2729 [rtomayko@gmail.com]
|
||||
|
||||
* Quote column names in generated SQL. #2728 [rtomayko@gmail.com]
|
||||
@ -10,27 +49,6 @@
|
||||
|
||||
* Correct fixture behavior when table name pluralization is off. #2719 [Rick Bradley <rick@rickbradley.com>]
|
||||
|
||||
* Added extension capabilities to has_many and has_and_belongs_to_many proxies [DHH]. Example:
|
||||
|
||||
class Account < ActiveRecord::Base
|
||||
has_many :people, :extend => Module.new {
|
||||
def find_or_create_by_name(name)
|
||||
first_name, *last_name = name.split
|
||||
last_name = last_name.join " "
|
||||
|
||||
find_by_first_name_and_last_name(first_name, last_name) ||
|
||||
create({ :first_name => first_name, :last_name => last_name })
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
|
||||
person.first_name # => "David"
|
||||
person.last_name # => "Heinemeier Hansson"
|
||||
|
||||
Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation).
|
||||
|
||||
|
||||
* Changed :dbfile to :database for SQLite adapter for consistency (old key still works as an alias) #2644 [Dan Peterson]
|
||||
|
||||
* Added migration support for Oracle #2647 [Michael Schoen]
|
||||
|
@ -124,14 +124,6 @@ def replace(other_array)
|
||||
end
|
||||
|
||||
private
|
||||
def method_missing(method, *args, &block)
|
||||
if @target.respond_to?(method) or (not @association_class.respond_to?(method) and Class.respond_to?(method))
|
||||
super
|
||||
else
|
||||
@association_class.constrain(:conditions => @finder_sql, :joins => @join_sql, :readonly => false) { @association_class.send(method, *args, &block) }
|
||||
end
|
||||
end
|
||||
|
||||
def raise_on_type_mismatch(record)
|
||||
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
|
||||
end
|
||||
|
@ -76,6 +76,16 @@ def size
|
||||
end
|
||||
|
||||
protected
|
||||
def method_missing(method, *args, &block)
|
||||
if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
|
||||
super
|
||||
else
|
||||
@association_class.constrain(:conditions => @finder_sql, :joins => @join_sql, :readonly => false) do
|
||||
@association_class.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
if @options[:finder_sql]
|
||||
records = @association_class.find_by_sql(@finder_sql)
|
||||
|
@ -85,6 +85,20 @@ def find(*args)
|
||||
end
|
||||
|
||||
protected
|
||||
def method_missing(method, *args, &block)
|
||||
if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
|
||||
super
|
||||
else
|
||||
@association_class.constrain(
|
||||
:conditions => @finder_sql,
|
||||
:joins => @join_sql,
|
||||
:readonly => false,
|
||||
:creation => { @association_class_primary_key_name => @owner.id }) do
|
||||
@association_class.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
find_all
|
||||
end
|
||||
|
@ -140,7 +140,7 @@ def initialize(errors)
|
||||
#
|
||||
# == Dynamic attribute-based finders
|
||||
#
|
||||
# Dynamic attribute-based finders are a cleaner way of getting objects by simple queries without turning to SQL. They work by
|
||||
# Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by
|
||||
# appending the name of an attribute to <tt>find_by_</tt> or <tt>find_all_by_</tt>, so you get finders like Person.find_by_user_name,
|
||||
# Person.find_all_by_last_name, Payment.find_by_transaction_id. So instead of writing
|
||||
# <tt>Person.find(:first, ["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
|
||||
@ -155,6 +155,15 @@ def initialize(errors)
|
||||
# is actually Payment.find_all_by_amount(amount, options). And the full interface to Person.find_by_user_name is
|
||||
# actually Person.find_by_user_name(user_name, options). So you could call <tt>Payment.find_all_by_amount(50, :order => "created_on")</tt>.
|
||||
#
|
||||
# The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with
|
||||
# <tt>find_or_create_by_</tt> and will return the object if it already exists and otherwise creates it, then returns it. Example:
|
||||
#
|
||||
# # No 'Summer' tag exists
|
||||
# Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
|
||||
#
|
||||
# # Now the 'Summer' tag does exist
|
||||
# Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
|
||||
#
|
||||
# == Saving arrays, hashes, and other non-mappable objects in text columns
|
||||
#
|
||||
# Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
|
||||
@ -451,6 +460,8 @@ def create(attributes = nil)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| create(attr) }
|
||||
else
|
||||
attributes.reverse_merge!(scope_constraints[:creation]) if scope_constraints[:creation]
|
||||
|
||||
object = new(attributes)
|
||||
object.save
|
||||
object
|
||||
@ -838,10 +849,10 @@ def silence
|
||||
# end
|
||||
def constrain(options = {})
|
||||
options = options.dup
|
||||
if !options[:joins].blank? and !options.has_key?(:readonly)
|
||||
options[:readonly] = true
|
||||
end
|
||||
options[:readonly] = true if !options[:joins].blank? && !options.has_key?(:readonly)
|
||||
|
||||
self.scope_constraints = options
|
||||
|
||||
yield if block_given?
|
||||
ensure
|
||||
self.scope_constraints = nil
|
||||
@ -948,27 +959,54 @@ def undecorated_table_name(class_name = class_name_of_active_record_descendant(s
|
||||
# It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
|
||||
# is actually find_all_by_amount(amount, options).
|
||||
def method_missing(method_id, *arguments)
|
||||
method_name = method_id.id2name
|
||||
if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
||||
finder = determine_finder(match)
|
||||
|
||||
if md = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
||||
finder = md.captures.first == 'all_by' ? :all : :first
|
||||
attributes = md.captures.last.split('_and_')
|
||||
attributes.each { |attr_name| super unless column_methods_hash.include?(attr_name.to_sym) }
|
||||
attribute_names = extract_attribute_names_from_match(match)
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
|
||||
attr_index = -1
|
||||
conditions = attributes.collect { |attr_name| attr_index += 1; "#{table_name}.#{attr_name} #{attribute_condition(arguments[attr_index])} " }.join(" AND ")
|
||||
conditions = construct_conditions_from_arguments(attribute_names, arguments)
|
||||
|
||||
if arguments[attributes.length].is_a?(Hash)
|
||||
find(finder, { :conditions => [conditions, *arguments[0...attributes.length]] }.update(arguments[attributes.length]))
|
||||
if arguments[attribute_names.length].is_a?(Hash)
|
||||
find(finder, { :conditions => conditions }.update(arguments[attribute_names.length]))
|
||||
else
|
||||
# deprecated API
|
||||
send("find_#{finder}", [conditions, *arguments[0...attributes.length]], *arguments[attributes.length..-1])
|
||||
send("find_#{finder}", conditions, *arguments[attribute_names.length..-1]) # deprecated API
|
||||
end
|
||||
elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
||||
attribute_names = extract_attribute_names_from_match(match)
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
|
||||
find(:first, :conditions => construct_conditions_from_arguments(attribute_names, arguments)) ||
|
||||
create(construct_attributes_from_arguments(attribute_names, arguments))
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def determine_finder(match)
|
||||
match.captures.first == 'all_by' ? :all : :first
|
||||
end
|
||||
|
||||
def extract_attribute_names_from_match(match)
|
||||
match.captures.last.split('_and_')
|
||||
end
|
||||
|
||||
def construct_conditions_from_arguments(attribute_names, arguments)
|
||||
conditions = []
|
||||
attribute_names.each_with_index { |name, idx| conditions << "#{table_name}.#{name} #{attribute_condition(arguments[idx])} " }
|
||||
[ conditions.join(" AND "), *arguments[0...attribute_names.length] ]
|
||||
end
|
||||
|
||||
def construct_attributes_from_arguments(attribute_names, arguments)
|
||||
attributes = {}
|
||||
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
||||
attributes
|
||||
end
|
||||
|
||||
def all_attributes_exists?(attribute_names)
|
||||
attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
|
||||
end
|
||||
|
||||
def attribute_condition(argument)
|
||||
case argument
|
||||
when nil then "IS ?"
|
||||
@ -1691,4 +1729,4 @@ def clone_attribute_value(reader_method, attribute_name)
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,11 @@
|
||||
require 'abstract_unit'
|
||||
require 'fixtures/post'
|
||||
require 'fixtures/comment'
|
||||
require 'fixtures/project'
|
||||
require 'fixtures/developer'
|
||||
|
||||
class AssociationsExtensionsTest < Test::Unit::TestCase
|
||||
fixtures :projects, :developers
|
||||
fixtures :projects, :developers, :comments, :posts
|
||||
|
||||
def test_extension_on_habtm
|
||||
assert_equal projects(:action_controller), developers(:david).projects.find_most_recent
|
||||
|
@ -490,6 +490,14 @@ def test_create_many
|
||||
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_find_or_create
|
||||
number_of_clients = companies(:first_firm).clients.size
|
||||
the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client")
|
||||
assert_equal number_of_clients + 1, companies(:first_firm, :refresh).clients.size
|
||||
assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client")
|
||||
assert_equal number_of_clients + 1, companies(:first_firm, :refresh).clients.size
|
||||
end
|
||||
|
||||
def test_deleting
|
||||
force_signal37_to_load_all_clients_of_firm
|
||||
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
|
||||
|
@ -5,7 +5,7 @@
|
||||
require 'fixtures/category'
|
||||
|
||||
class ConditionsScopingTest < Test::Unit::TestCase
|
||||
fixtures :developers
|
||||
fixtures :developers, :comments, :posts
|
||||
|
||||
def test_set_conditions
|
||||
Developer.constrain(:conditions => 'just a test...') do
|
||||
@ -42,6 +42,17 @@ def test_scoped_count
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_create
|
||||
new_comment = nil
|
||||
|
||||
VerySpecialComment.constrain(:creation => { :post_id => 1 }) do
|
||||
assert_equal({ :post_id => 1 }, Thread.current[:constraints][VerySpecialComment][:creation])
|
||||
new_comment = VerySpecialComment.create :body => "Wonderful world"
|
||||
end
|
||||
|
||||
assert Post.find(1).comments.include?(new_comment)
|
||||
end
|
||||
|
||||
def test_immutable_constraint
|
||||
options = { :conditions => "name = 'David'" }
|
||||
Developer.constrain(options) do
|
||||
|
@ -278,6 +278,20 @@ def test_find_all_by_nil_and_not_nil_attributes
|
||||
assert_equal "Mary", topics[0].author_name
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_attribute
|
||||
number_of_companies = Company.count
|
||||
sig38 = Company.find_or_create_by_name("38signals")
|
||||
assert_equal number_of_companies + 1, Company.count
|
||||
assert_equal sig38, Company.find_or_create_by_name("38signals")
|
||||
end
|
||||
|
||||
def test_find_or_create_from_two_attributes
|
||||
number_of_companies = Company.count
|
||||
sig38 = Company.find_or_create_by_name("38signals")
|
||||
assert_equal number_of_companies + 1, Company.count
|
||||
assert_equal sig38, Company.find_or_create_by_name("38signals")
|
||||
end
|
||||
|
||||
def test_find_with_bad_sql
|
||||
assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
|
||||
end
|
||||
|
12
activerecord/test/fixtures/comment.rb
vendored
12
activerecord/test/fixtures/comment.rb
vendored
@ -10,18 +10,14 @@ def self.search_by_type(q)
|
||||
end
|
||||
end
|
||||
|
||||
class SpecialComment < Comment;
|
||||
|
||||
class SpecialComment < Comment
|
||||
def self.what_are_you
|
||||
'a special comment...'
|
||||
end
|
||||
|
||||
end;
|
||||
end
|
||||
|
||||
class VerySpecialComment < Comment;
|
||||
|
||||
class VerySpecialComment < Comment
|
||||
def self.what_are_you
|
||||
'a very special comment...'
|
||||
end
|
||||
|
||||
end;
|
||||
end
|
3
activerecord/test/fixtures/post.rb
vendored
3
activerecord/test/fixtures/post.rb
vendored
@ -11,8 +11,9 @@ def find_most_recent
|
||||
end
|
||||
}
|
||||
|
||||
has_one :very_special_comment
|
||||
has_many :special_comments
|
||||
|
||||
has_many :special_comments, :class_name => "SpecialComment"
|
||||
has_and_belongs_to_many :categories
|
||||
has_and_belongs_to_many :special_categories, :join_table => "categories_posts"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user