The README says it all:


You’re knee deep in a debugger session, and you can’t understand why something’s wrong. You wish you could fire up your application against the test database, but sadly, the process which is running the tests is within a transaction, and thus the actual data is opaque. What can you do?


1 # Somewhere deep in your tests
2 test "the frobble touches the widget" do
3 assert_equal 42, frobble.widget_id
4 end

You’ve been on this assert_equal call for the past hour wondering. Frustration’s been mounting, because you don’t understand why the frobble doesn’t touch the widget. Clearly, there’s something wrong with the fixtures, but you can’t understand what it is. Time to fire up the debugger and dump the data:


1 [814, 823] in test/unit/widget_test.rb
2 814 frobble.save!
3 815 end
4 816
5 817 test "the frobble touches the widget" do
6 818 debugger
7 => 819 assert_equal 42, frobble.widget_id
8 820 end
9 821
10 822 test "the widget touched the frobble in turn" do
11 823 assert widget.touched_by_frobble?
12 test/unit/widget_test.rb:819
13 => 819 assert_equal 42, frobble.widget_id
14 (rdb:112)

Since the data_dumper gem is already declared in your Gemfile (if not, declare it, bundle install, then run your tests again), type:


1 (rdb:112) File.mkdir(Rails.root + "dump")
2 (rdb:113) DataDumper.dump(Rails.root + "dump")

Then, quit your failing tests, and from the trusty command line:


1 $ rails console
2 > DataDumper.load(Rails.root + "dump")
3 > exit
4
5 $ rails server

Any and all data from your test database will be loaded in your development environment. You can now explore your model with your trusty application, to find out what’s really going on.

What happens when you mix foreign key constraints, PostgreSQL, ActiveRecord and cached counters (search for :counter_cache)? Deadlocks.

My colleague, Mina Naguib, spent some time while away from the office to dig and diagnose some problems. The underlying issue is probably not related to PostgreSQL itself, but cached counters, which Rails makes very easy to do, sometimes result in deadlocks. We’ve seen those backtraces in Hoptoad, but had never really investigated them: they were rare, meaning nobody complained about them. They were just a thorn in our side.

In a nutshell, this flow is problematic:

  1. Adding or modifying a record in a table, when:
  2. That table is a child (using a foreign key contraint) of another table
  3. and then modifying the parent record in the parent table
  4. Using transactions
  5. Concurrently
  6. Sometimes (race condition)

From Mina’s PostgreSQL transactions wrapping child+parent modifications, deadlocks, and ActiveRecord

Read the full text: it’s well worth it. Unfortunately, Mina doesn’t have a resolution, but he wants more people to be aware of the issue, in the hopes that somebody will find a clean solution.

To try and ease my pain, I am refactoring a Rails 2.3.5 application using Hobo from multiple tables to a single table. I’m talking about models which are essentially the same: companies, people and employees. I went from this:


1 class Company < ActiveRecord::Base
2 has_many :employees
3 has_many :people, :through => :employees
4 end
5
6 class Person < ActiveRecord::Base
7 has_many :employees
8 has_many :companies, :through => :employees
9 end
10
11 class Employee < ActiveRecord::Base
12 belongs_to :company
13 belongs_to :person
14 end

To an STI-enabled class hierarchy:


1 class Addressee < ActiveRecord::Base
2 self.abstract_class = true
3 end
4
5 class Company < Addressee
6 has_many :employees
7 has_many :people, :through => :employees
8 end
9
10 class Person < Addressee
11 has_many :employees
12 has_many :companies, :through => :employees
13 end
14
15 class Employee < Addressee
16 belongs_to :company
17 belongs_to :person
18 end

Can you spot the mistake? Here’s where it breaks down:


1 $ script/console
2 > PersonAddressee.count
3 SQL (9.9ms) SELECT count(*) AS count_all FROM "addressees"
4 => 10231

When the classes are defined with abstract_class? set to true, calling Company#count executes the following:

  1. ActiveRecord Company#superclass, which answers Addressee
  2. ActiveRecord calls Addressee#abstract_class?, which answers true
  3. Because Company#base_class == Company#superclass, no STI is involved, and ActiveRecord queries the addressees table with no STI hints (type column).

When removing the abstract class declaration, here’s how it works:

  1. ActiveRecord Company#superclass, which answers Addressee
  2. ActiveRecord calls Addressee#abstract_class?, which answers false
  3. ActiveRecord Addressee#superclass, which answers ActiveRecord::Base
  4. Because Company#base_class != Company#superclass, STI is involved and ActiveRecord adds the type hint to the addressees table query.

The documentation is quite clear though:

abstract_class?()

Returns whether this class is a base AR class. If A is a base class and B descends from A, then B.base_class will return B.

The documentation on #base_class is much better though:

base_class()

Returns the base AR subclass that this class descends from. If A extends AR::Base, A.base_class will return A. If B descends from A through some arbitrarily deep hierarchy, B.base_class will return A.

And current HEAD documentation is the best of them all:

base_class()

Returns the base AR subclass that this class descends from. If A extends AR::Base, A.base_class will return A. If B descends from A through some arbitrarily deep hierarchy, B.base_class will return A.

If B < A and C < B and if A is an abstract_class then both B.base_class and C.base_class would return B as the answer since A is an abstract_class.

Luckily, I found this before users got a chance to report bugs. And no, I would never have thought to add a test for this: I’m not in the habit of testing base ActiveRecord functionality.

We received a nasty Hoptoad notification today at AdGear :


1 ActiveRecord::StatementInvalid: PGError: ERROR:
2 duplicate key value violates unique constraint "index_placement_rules_on_type_and_placement_id_and_bookable_typ" :
3 INSERT INTO "placement_rules" ("kind", "created_at", "placement_id", "bookable_type", "bookable_id", "updated_at") VALUES RETURNING "id"

This manifested itself when a customer called and told us he’d found our 500 error page. Ooops.

Our problem turned out to be a misunderstanding of autosave association validations.

Autosave associations are a wonderful beast: with a single #save call, the nested entities are saved within the same transaction, without any intervention on the developer’s part. Unfortunately, validations behave differently than what we expected.

For the sake of argument, let’s use these models:


1 class Order < ActiveRecord::Base
2 has_many :items
3 end
4
5 class OrderItem < ActiveRecord::Base
6 belongs_to :order
7
8 validates_presence_of :product_number
9 validates_uniqueness_of :product_number, :scope => :order
10 end

If you build an Order and add two items, you’ll have troubles, unless you wrap your save in a transaction block:


1 > o = Order.new
2 > 2.times { o.items << Item.new(:product_number => "A-113") }
3 > o.save
4 # Validates entities
5 Item Load (0.2ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id IS NULL) LIMIT 1
6 Item Load (0.1ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id IS NULL) LIMIT 1
7 Order Create (0.7ms) INSERT INTO "orders" ("customer_number", "created_at", "updated_at") VALUES
8 # Validate 1st entity again
9 Item Load (0.1ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id = 1) LIMIT 1
10 Item Create (0.1ms) INSERT INTO "items" ("created_at", "order_id", "updated_at", "product_number") VALUES
11 # Validate 2nd entity again, but fails this time around
12 Item Load (0.1ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id = 1) LIMIT 1
13 => [false, true]
14 > o.items.map(&:errors).flatten.map(&:full_messages)
15 => [[], ["Product number has already been taken"]]

A partially saved order is rarely what you’re looking for. Adding the :autosave =&gt; true option…


1 class Order < ActiveRecord::Base
2 has_many :items, :autosave => true
3 end

Generates a very different SQL trace:


1 > o = Order.new
2 > 2.times { o.items << Item.new(:product_number => "A-113") }
3 > o.save
4 # Validate associated entities…
5 Item Load (0.2ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id IS NULL) LIMIT 1
6 Item Load (0.1ms) SELECT "items".id FROM "items" WHERE ("items"."product_number" = A-113 AND "items".order_id IS NULL) LIMIT 1
7 Order Create (0.4ms) INSERT INTO "orders" ("customer_number", "created_at", "updated_at") VALUES
8 # Then save them, even though they’re invalid.
9 Item Create (0.1ms) INSERT INTO "items" ("created_at", "order_id", "updated_at", "product_number") VALUES
10 Item Create (0.1ms) INSERT INTO "items" ("created_at", "order_id", "updated_at", "product_number") VALUES
11 => true

This is even worse: instead of a partial save, ActiveRecord told us that everything was good. This is how ActiveRecord behaves:


1 if order.save then
2 order.items.each(&:save) if order.items.all?(&:valid?)
3 end

What we need instead is to hook into the autosave callback chain:


1 class Order < ActiveRecord::Base
2 has_many :items, :autosave => true
3
4 private
5
6 # items is the has_many name. If you had posts, it would be
7 # validate_associated_records_for_posts. If you have multiple
8 # autosave associations, each can have a method such as this
9 # one to handle it’s validation needs.
10 def validate_associated_records_for_items
11 product_number_errors = false
12 items.group_by(&:product_number).reject {|, group| group.length <= 1}.each do |product_number, group|
13 product_number_errors = true
14 errors.add_to_base "Product #{product_number} used #{group.length} times on the order"
15 end
16
17 errors.add_to_base("Each product number can be used at most once per order") if product_number
errors
18 end
19 end

With this validation in place, validations will run on the full collection in a single pass, rather than piecemeal.


1 > o = Order.new
2 > 2.times { o.items << Item.new(:product_number => "A-113") }
3 > o.save # Will run in-memory validations
4 => false
5 > pp o.errors.full_messages
6 ["Product A-113 used 2 times on the order",
7 "Each product number can be used at most once per order"]

Notice I didn’t say anything about the validates_uniqueness_of validation in Item. I left it in place. It doesn’t harm anything, and if you ever create items without going through the parent model, your code is ready to take care of itself.

Yesterday, I read Brian Cardarella‘s post entitled Brian Cardarella’s post entitled A case against Mocking and Stubbing. In the article, Brian says:

SQLite3 can be an in-memory database. Problem solved, right? Not quite. SQLite3 is pretty limited. Most people are probably using MySQL and rely upon many of the SQL functions that are included.

What would be nice (and well beyond my ability) is to have a Gem that simulated the database you use, only it is in-memory. Optimized for small data sets. No need to go through a heavy hashing algorithm. Keep it light. Keep it fast.

Brian Cardarella in A case against Mocking and Stubbing

MySQL already has the MEMORY storage engine, and I wanted to see if that would help for testing purposes. Since we’re staying in MySQL-land, this should have been a simple matter.

First, the good news. I had to change only a couple of lines:


1 diff -git a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/vendor/rails/activerecor
2 index 1e452ae..c207080 100644
3 -
- a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
4 + b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
5 @ -442,7 +442,13 @ module ActiveRecord
6 end
7
8 def create_table(table_name, options = {}) #:nodoc:
9 – super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
10 + engine = case Rails.env
11 + when "test"
12 + "MEMORY"
13 + else
14 + "InnoDB"
15 + end
16 + super(table_name, options.reverse_merge(:options => "ENGINE=#{engine}"))
17 end
18
19 def rename_table(table_name, new_name)
20 diff -git a/vendor/rails/railties/lib/tasks/databases.rake b/vendor/rails/railties/lib/tasks/databases.rake
21 index 5cb27f1..c520d4a 100644
22 -
- a/vendor/rails/railties/lib/tasks/databases.rake
23 + b/vendor/rails/railties/lib/tasks/databases.rake
24 @ -368,9 +368,9 @ namespace :db do
25
26 desc ‘Check for pending migrations and load the test schema’
27 task :prepare => ‘db:abort_if_pending_migrations’ do
28 – if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
29 – Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].i
30 – end
31 + # if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
32 + # Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]]
33 + # end
34 end
35 end
36

Great, but there’s little benefit. First, a regular run (InnoDB):


1 $ time rake
2 (in /Users/francois/Documents/work/fasttest)
3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/password_reset_mailer_test.rb" "test/unit/person_test.rb"
4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
5 Started
6 ……………………….
7 Finished in 0.520087 seconds.
8
9 28 tests, 33 assertions, 0 failures, 0 errors
10 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/functional/accounts_controller_test.rb" "test/functional/password_resets_controller_test.rb" "test/functional/people_controller_test.rb" "test/functional/sessions_controller_test.rb"
11 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
12 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
13 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
14 Started
15 ……………………………………………..
16 Finished in 1.828175 seconds.
17
18 53 tests, 72 assertions, 0 failures, 0 errors
19 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb"
20
21 real 0m10.365s
22 user 0m6.582s
23 sys 0m1.925s

Next, a run with the MEMORY engine:


1 $ time rake
2 (in /Users/francois/Documents/work/fasttest)
3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/password_reset_mailer_test.rb" "test/unit/person_test.rb"
4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
5 Started
6 ……………………….
7 Finished in 0.602607 seconds.
8
9 28 tests, 33 assertions, 0 failures, 0 errors
10 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/functional/accounts_controller_test.rb" "test/functional/password_resets_controller_test.rb" "test/functional/people_controller_test.rb" "test/functional/sessions_controller_test.rb"
11 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
12 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
13 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
14 Started
15 ……………………………………………..
16 Finished in 1.132142 seconds.
17
18 53 tests, 72 assertions, 0 failures, 0 errors
19 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb"
20
21 real 0m9.111s
22 user 0m6.862s
23 sys 0m1.870s

Note that in test/test_helper.rb, I had to disable transactional fixtures. This would account for a lot the lost time difference.

If you want to play with this further, the sample application’s code is available at http://github.com/francois/fasttest

I would be interested in seeing other people’s runs, to know if it’s just my machine that runs at essentially the same speed.

I stumbled on AlterEgo last week, after Avdi announced it. This library implements the state pattern for any object, not just ActiveRecord models, which acts_as_state_machine (and it’s successor, aasm) do.

But, many people, myself included, want to use AlterEgo in the context of ActiveRecord models. Fortunately, AlterEgo provides us with all the necessary plumbing to do that very easily.

I will reuse Avdi’s traffic light example from the specifications. Avdi already points us to 90% of the solution in his example:


1 class TrafficLightWithCustomStorage
2 def state
3 gyr = [
4 hardware_controller</span>.green, <span class="no"> <strong>5</strong></span> <span class="iv">hardware_controller.yellow,
6 hardware_controller</span>.red <span class="no"> 7</span> ] <span class="no"> 8</span> <span class="no"> 9</span> <span class="r">case</span> gyr <span class="no"><strong>10</strong></span> <span class="r">when</span> [<span class="pc">true</span>, <span class="pc">false</span>, <span class="pc">false</span>] <span class="r">then</span> <span class="sy">:proceed</span> <span class="no">11</span> <span class="r">when</span> [<span class="pc">false</span>, <span class="pc">true</span>, <span class="pc">false</span>] <span class="r">then</span> <span class="sy">:caution</span> <span class="no">12</span> <span class="r">when</span> [<span class="pc">false</span>, <span class="pc">false</span>, <span class="pc">true</span>] <span class="r">then</span> <span class="sy">:stop</span> <span class="no">13</span> <span class="r">else</span> raise <span class="s"><span class="dl">&quot;</span><span class="k">Invalid state!</span><span class="dl">&quot;</span></span> <span class="no">14</span> <span class="r">end</span> <span class="no"><strong>15</strong></span> <span class="r">end</span> <span class="no">16</span> <span class="no">17</span> <span class="r">def</span> <span class="fu">state=</span>(value) <span class="no">18</span> gyr = <span class="r">case</span> value <span class="no">19</span> <span class="r">when</span> <span class="sy">:proceed</span> <span class="r">then</span> [<span class="pc">true</span>, <span class="pc">false</span>, <span class="pc">false</span>] <span class="no"><strong>20</strong></span> <span class="r">when</span> <span class="sy">:caution</span> <span class="r">then</span> [<span class="pc">false</span>, <span class="pc">true</span>, <span class="pc">false</span>] <span class="no">21</span> <span class="r">when</span> <span class="sy">:stop</span> <span class="r">then</span> [<span class="pc">false</span>, <span class="pc">false</span>, <span class="pc">true</span>] <span class="no">22</span> <span class="r">end</span> <span class="no">23</span> <span class="iv">hardware_controller.green = gyr[0]
24 hardware_controller</span>.yellow = gyr[<span class="i">1</span>] <span class="no"><strong>25</strong></span> <span class="iv">hardware_controller.red = gyr[2]
26 end
27 end

When an object implements #state and #state=, AlterEgo will serialize it’s state using these two methods. Let’s hook this to ActiveRecord (note, I changed the code for a quick example, so it’s not exactly the same, but very similar):

app/models/traffic_light.rb

1 class TrafficLight < ActiveRecord::Base
2 def state
3 case read_attribute(:color)
4 when "green"; :proceed
5 when "yellow"; :caution
6 when "red"; :stop
7 else; raise "Invalid color: #{read_attribute(:color).inspect}"
8 end
9 end
10
11 def state=(value)
12 color_value = case value
13 when :proceed; "green"
14 when :caution; "yellow"
15 when :stop; "red"
16 else; raise "Don’t know how to convert #{value.inspect} to color value"
17 end
18 write_attribute(:color, color_value)
19 end
20 end

The most important thing you should check is that your database model has a valid state on model instantiation. Whether you do it using the :default key in your migration or some other way is irrelevant, but the value has to be provided, or the #state method will barf (or implement default processing there?) In the example app, I opted to use a default value:

db/migrate/20081209150159_create_traffic_lights.rb

1 class CreateTrafficLights < ActiveRecord::Migration
2 def self.up
3 create_table :traffic_lights do |t|
4 t.string :color, :default => "red"
5 end
6 end
7
8 def self.down
9 drop_table :traffic_lights
10 end
11 end

You may check the full code in action in the GitHub repository: alter_ego_plus_active_record

Does anyone know why ActiveRecord does not merge joins when using scopes ?

Scopes are wonderful, thanks for has_finder and named scopes, but joins aren’t supported. This code:


1 City.nearest(latitude, longitude).find_tagged_with(tags)

raises an ActiveRecord::StatementInvalid complaining that a specified column could not be found. The column can’t be found because the join was dropped on the floor, instead of being merged.

The change me and Marc-André are proposing to make is this:


1 $ git diff 36236235039b3b0cab22a24ce7b45a8ec071cb5e 45e7c94be02a586f70908694545e8484e4a85382 vendor
2 diff -git a/vendor/rails/activerecord/lib/active_record/base.rb b/vendor/rails/activerecord/lib/active_record/base.rb
3 index 261d854..427e3ef 100755
4 a/vendor/rails/activerecord/lib/active_record/base.rb
5 + b/vendor/rails/activerecord/lib/active_record/base.rb
6 @</span> -1673,6 +1673,8 <span class="chg">@ module ActiveRecord #:nodoc:
7 hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND "
8 elsif key == :include && merge
9 hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
10 elsif key == :joins && merge
11 hash[method][key] = [params[key], hash[method][key]].join(" ")
12 else
13 hash[method][key] = hash[method][key] || params[key]
14 end

This is based on Rails 2.0.2, but Rails Edge has the same problem. So, any ideas ?

I recently hit upon the Enhanced Migrations plugin by Revolution on Rails. Works great when you develop on branches. The #dump_schema_information method of ActiveRecord::ConnectionAdapters::SchemaStatements only dumps the most recently migration file. Since each migration is now a separate entry in the migrations_info table, we can’t report only the latest one.

To this end, I generated the following diff:


1 $ svn diff vendor
2 Index: vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb
3 ===============
4 - vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb (revision 7767)
5 + vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb (working copy)
6 </span><span class="er"> -58,8 +58,8 </span><span class="er">
7
8 ActiveRecord::ConnectionAdapters::SchemaStatements.send(:define_method, :dump_schema_information) do
9 begin
10if (current_schema = ActiveRecord::Migrator.current_version) > 0
11return "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} VALUES (#{current_schema}, NOW"
12 + select_all("SELECT * FROM #{ActiveRecord::Migrator.schema_info_table_name} ORDER BY created_at, id").map do |migration|
13 + "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} VALUES;\n"
14 end
15 rescue ActiveRecord::StatementInvalid
16 # No Schema Info

Hope this is useful for other people.

Well, in relation to my Useful #with_scope technique post, here’s a plugin that implements that idea. This code is used on a production system. It works perfectly for my needs at the moment.

AutoScope

Automatically create scoped access methods on your ActiveRecord models.

Examples

Declare your scopes within your ActiveRecord::Base subclasses.


1 class Contact < ActiveRecord::Base
2 auto_scope \
3 :old => {:find => {:conditions => ["born_on < ?", 30.years.ago]}},
4 :young => {:find => {:conditions => ["born_on > ?", 1.year.ago]}}
5 end
6
7 class Testimonial < ActiveRecord::Base
8 auto_scope \
9 :approved => {
10 :find => {:conditions => ["approved_at < ?", proc {Time.now}]},
11 :create => {:approved_at => proc {Time.now}}},
12 :unapproved => {
13 :find => {:conditions => "approved_at IS NULL"},
14 :create => {:approved_at => nil}}
15 end

These declarations give you access to the following scoped methods:


1 Testimonial.approved.count
2 Testimonial.unapproved.create!(params[:testimonial])
3 young_contacts</span> = <span class="co">Contact</span>.young <span class="no">4</span> <span class="iv">contacts = Contact.old.find(:all, :conditions => ["name LIKE ?", params[:name]])

The plugin’s home page is: http://xlsuite.org/plugins/auto_scope
The plugin’s Subversion repository is: http://svn.xlsuite.org/plugins/auto_scope

I just stumbled upon something very interesting. In my application, we accept anonymous testimonials from the web, but they must not be shown until they have been reviewed and approved by an administrator.

The first API I designed was this:

test/unit/testimonial_test.rb

1 def test_can_get_approved_only
2 testimonials = Testimonial.find(:completed)
3 assert_equal [@approved_testimonial], testimonials
4 end

That worked well enough, even after I started adding code to take care of :all and :first, and modifying #count to also use the same scoping rules.

But then, I hit a snag with scoped #has_many accesses. The following failed:

test/unit/testimonial_test.rb

1 def test_can_count_approved_testimonials_for_a_single_contact
2 testimonials = @contact.testimonials.count(:completed)
3 assert_equal 1, testimonials
4 end

This generated an invalid statement:


1 test_can_count_approved_testimonials_for_a_single_contact(TestimonialTest::AbilityToCountTest):
2 ActiveRecord::StatementInvalid: Mysql::Error: Unknown column ‘approved’ in ‘where clause’: SELECT count(*) AS count_all FROM testimonials WHERE (testimonials.contact_id = 1 AND (approved))

I did a bit of debugging and found that #has_many had already modified the #count parameters even before my #count_with_extensions method was called. So I decided to change my API. I decided I wanted something like this:

test/unit/testimonial_test.rb

1 def test_can_get_approved_only
2 testimonials = Testimonial.completed.find
3 assert_equal [@approved_testimonial], testimonials
4 end

This new API first defines the scope, then calls a normal ActiveRecord::Base method. My first implementation was:

app/models/testimonial.rb

1 def self.completed
2 with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]}) do
3 yield self
4 end
5 end

When I ran my tests, my error was immediately apparent:


1 test_can_get_approved_only(TestimonialTest::AbilityToCountTest):
2 LocalJumpError: no block given

Ooops… So I thought a bit more. What I wanted to return from #completed was an object that could stand in for my class (Testimonial in this case), but for which the scope had been defined. I ended up with this implementation:

app/models/testimonial.rb

1 def completed
2 returning Object.new do |proxy|
3 class << proxy
4 def method_missing(method, *args)
5 Testimonial.with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]},
6 :create => {:approved_at => Time.now}) do
7 Testimonial.send(method, *args)
8 end
9 end
10 end
11 end
12 end

What this implementation does is:

  1. It starts by creating a proxy object;
  2. It redefines #method_missing on that object;
  3. The new #method_missing method creates the scope we originally wanted and calls the method we were originally targetting;
  4. Then the proxy object is returned.

With that implementation, I get something nice:


1 $ rake test:recent
2 (in /home/francois/src)
3 /usr/local/bin/ruby -Ilib:test "/usr/local/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader.rb" "test/unit/testimonial_test.rb"
4 Started
5 ……………
6 Finished in 3.041738 seconds.
7
8 15 tests, 31 assertions, 0 failures, 0 errors

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on