Permalink
Browse files

Added dynamic attribute-based finders as a cleaner way of getting obj…

…ects by simple queries without turning to SQL.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@307 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 9322168 commit ac8fd7dfb91eb0e554537e671eaf1615a1d19757 @dhh dhh committed Jan 2, 2005
Showing with 57 additions and 12 deletions.
  1. +10 −0 activerecord/CHANGELOG
  2. +25 −0 activerecord/lib/active_record/base.rb
  3. +22 −12 activerecord/test/finder_test.rb
View
@@ -1,5 +1,15 @@
*SVN*
+* Added dynamic attribute-based finders as a cleaner way of getting objects by simple queries without turning to SQL.
+ They work by appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name,
+ Payment.find_by_transaction_id</tt>. 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>.
+
+ It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
+ <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
+ <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do
+ <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
+
* 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]
@@ -97,6 +97,17 @@ class StaleObjectError < ActiveRecordError #:nodoc:
# end
# end
#
+ # == 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
+ # appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name, Payment.find_by_transaction_id</tt>.
+ # 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>.
+ #
+ # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
+ # <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
+ # <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do
+ # <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
+ #
# == Saving arrays, hashes, and other non-mappeable 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+.
@@ -636,6 +647,20 @@ def undecorated_table_name(class_name = class_name_of_active_record_descendant(s
return table_name
end
+ # Enables dynamic finders like find_by_user_name(user_name) and find_by_user_name_and_password(user_name, password) that are turned into
+ # find_first(["user_name = ?", user_name]) and find_first(["user_name = ? AND password = ?", user_name, password]) respectively.
+ def method_missing(method_id, *arguments)
+ method_name = method_id.id2name
+
+ if method_name =~ /find_by_([_a-z]+)/
+ attributes = $1.split("_and_")
+ attributes.each { |attr_name| super unless column_methods_hash[attr_name.intern] }
+ conditions = attributes.collect { |attr_name| "#{attr_name} = ? "}.join(" AND ")
+ find_first([conditions, *arguments])
+ else
+ super
+ end
+ end
protected
def subclasses
@@ -4,14 +4,10 @@
require 'fixtures/entrant'
class FinderTest < Test::Unit::TestCase
- def setup
- @company_fixtures = create_fixtures("companies")
- @topic_fixtures = create_fixtures("topics")
- @entrant_fixtures = create_fixtures("entrants")
- end
+ fixtures :companies, :topics, :entrants
def test_find
- assert_equal(@topic_fixtures["first"]["title"], Topic.find(1).title)
+ assert_equal(@topics["first"]["title"], Topic.find(1).title)
end
def test_find_by_array_of_one_id
@@ -21,7 +17,7 @@ def test_find_by_array_of_one_id
def test_find_by_ids
assert_equal(2, Topic.find(1, 2).length)
- assert_equal(@topic_fixtures["second"]["title"], Topic.find([ 2 ]).first.title)
+ assert_equal(@topics["second"]["title"], Topic.find([ 2 ]).first.title)
end
def test_find_by_ids_missing_one
@@ -34,33 +30,33 @@ def test_find_all_with_limit
entrants = Entrant.find_all nil, "id ASC", 2
assert_equal(2, entrants.size)
- assert_equal(@entrant_fixtures["first"]["name"], entrants.first.name)
+ assert_equal(@entrants["first"]["name"], entrants.first.name)
end
def test_find_all_with_prepared_limit_and_offset
entrants = Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1]
assert_equal(2, entrants.size)
- assert_equal(@entrant_fixtures["second"]["name"], entrants.first.name)
+ assert_equal(@entrants["second"]["name"], entrants.first.name)
end
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
assert_equal(1, topics.size)
- assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ assert_equal(@topics["second"]["title"], topics.first.title)
end
def test_find_with_prepared_select_statement
topics = Topic.find_by_sql ["SELECT * FROM topics WHERE author_name = ?", "Mary"]
assert_equal(1, topics.size)
- assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ assert_equal(@topics["second"]["title"], topics.first.title)
end
def test_find_first
first = Topic.find_first "title = 'The First Topic'"
- assert_equal(@topic_fixtures["first"]["title"], first.title)
+ assert_equal(@topics["first"]["title"], first.title)
end
def test_find_first_failing
@@ -168,6 +164,20 @@ def test_count_by_sql
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
+ def test_find_by_one_attribute
+ assert_equal @topics["first"].find, Topic.find_by_title("The First Topic")
+ assert_nil Topic.find_by_title("The First Topic!")
+ end
+
+ def test_find_by_one_missing_attribute
+ assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
+ end
+
+ def test_find_by_two_attributes
+ assert_equal @topics["first"].find, Topic.find_by_title_and_author_name("The First Topic", "David")
+ assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary")
+ end
+
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)

0 comments on commit ac8fd7d

Please sign in to comment.