Close and Go BackBack to Viget

Tips and Tricks

Named Scope Caching

Brian Landau
Brian Landau, Web Developer, August 07, 2008 2

When working on high-traffic Rails sites, it often becomes necessary to find ways to improve performance with caching. One place we’ve found this is most convenient and easy-to-do is by caching an ActiveRecord result set for models that change rarely or not at all. An easy example of this is a Category model.

Often times, you have a categorization hierarchy that will never or rarely change over the life of an application. Ideally you would fetch the results once from the database and never have to again. So how do we go about caching this? First let’s look at our model and create a named_scope for it:

class Category < ActiveRecord::Base
  acts_as_tree
  named_scope :find_top_level, :conditions => 'categories.parent_id IS NULL',
                              :order      => 'categories.name'
end

Next, we need to create create a method that fetches the results for our new scope and caches it in a class variable. It should also only do caching if in production environment (alternatively or additionally, we could use the ActionController.perform_caching config value), as this can cause problems in tests.

def self.top_level
  unless ('production' == RAILS_ENV) && ActionController.perform_caching
    @@top_level_cache = self.find_top_level
  else
    @@top_level_cache ||= self.find_top_level
  end
end

Finally, we need to create a method to invalidate our cache when records are saved or deleted. Since we know this isn’t happening often (if at all), this should rarely be performed but is a good safeguard so we know our cache is current.

after_save :reset_cached_finder
after_destroy :reset_cached_finder

def reset_cached_finder
  @@top_level_cache = nil
end

This is something that we could easily see doing in a number of models for a number of finders. Since this involves a lot of similar code, it would be great if we could create some meta code that would allow us to define these caches with a simple one liner.

Continue reading "Named Scope Caching"

A Confusing Rubyism

Ben Scofield
Ben Scofield, Development Director, July 12, 2008 5

The following code has the potential to be terribly confusing:

if x = some_method
  monkify
else
  rhinofy
end

In Ruby, the return value of an assignment statement is the value assigned (run a = 1 in irb and you'll see it returns 1), so this code can be understood as:

  1. Evaluate some_method and assign the value to x
  2. If the new value of x evaluates to true, run monkify
  3. If the new value of x evaluates to false, run rhinofy

Unfortunately, however, most developers are much more accustomed to seeing the equality operator == in conditionals, which means that often we'll misinterpret that code as:

  1. Evaluate some_method and compare the result to x
  2. If they are the same, run monkify
  3. If they are different, run rhinofy

In the interest of clarity, then, it's probably better to avoid using an assignment statement in a conditional. Of course, other concerns may override the need for clarity - but it's something to keep in mind.

Inheritance is Unlikely

Justin Marney
Justin Marney, Web Developer, June 05, 2008 8 What do you do when you identify common sets of shared behavior across several classes?

A common solution is to move the shared code into a base class and inherit.
# Filled with api interaction behavior
class Base
  def api_specific_behavior
    puts "connect to api"
  end
end

class User < Base
  def user_specific_behavior
    puts "I am a user"
  end
end
The problem with inheritance in this situation lies primarily in a subtle violation of the Liskov Substitution Principal which essentially states that inheritance should implement an "is-a" relationship. If we have a class called User that inherits from Base, are we trying to model the part of our domain where a User is a Base? This doesn't make sense given that we never actually have any Bases (i.e. instances of Base).

Continue reading "Inheritance is Unlikely"

Testing for HTML Tags in Rails Plugins

Brian Landau
Brian Landau, Web Developer, June 04, 2008 5

When creating Rails plugins that add ActionView helpers, we often test to ensure they produce specific tags with specific attributes. When testing this type of assertion within controller tests, we have the very useful assert_tag and assert_select; but in plugin tests and elsewhere, these aren't available. Adding this functionality to your own tests turns out to be somewhat convoluted.

The first realization you come to is that assert_tag and assert_select can't be used because they test against the response body of a controller. Since we're trying to test functionality independent of other components, using ActionController when it's not necessary is not recommended. You might be tempted at this point to just test against a regular expression and forget using these methods. That is doable, but a little unmanageable in the long run. First off, attributes can't be expected to be in a specific order, and their order doesn't matter, so you don't want to test for that. This will even initially make your regexes fairly complicated. Here's an example of testing for a specific name attribute:


def test_for_name_attribute
   tag = text_field_tag('login')
   assert_match /<input\s+(([\w^(name)]+="([^"'><]+)?"\s+)+)?name="login"((\s+[\w^(name)]+="([^"'><]+)?")+)?(\s+)?\/>/, tag
end

As you can see, even matching one attribute can result in a extremely long and ugly regex. What if we want to match multiple attributes and nested tags? This quickly becomes untenable.

If you look to include the component used by assert_tag to find and match specific tags, you will find require 'html/document'. Try just requiring this by doing this at the top of your test:


require 'test/unit'
require 'rubygems'
require 'active_support'
require 'action_view'
require 'html/document'

You'll find you get a no such file to load -- html/document (MissingSourceFile) error. If you dig around in the Rails code, you'll find it lives in action_controller/vendor/html-scanner. To be able to include html/document, you need to first include action_controller.

Now that we have html/document included, let's make a test helper method to match a tag specification. This takes little change from the original assert_tag method.

def assert_tag_in(*opts)
   target = HTML::Document.new(opts.shift, false, false)
   opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
   assert !target.find(opts).nil?, 
     "expected tag, but no tag found matching #{opts.inspect} in:\n#{target.inspect}"
end

With this, you can now use assert_tag_in to do the same test as above:

def test_for_name_attribute
   tag = text_field_tag('login')
   assert_tag_in tag, :input, 
     :attributes => {:type => 'text', :name => 'login'}
end

The whole code for a sample test_helper.rb file for plugins can be found in this pastie.
Also, look at the documentation for assert_tag for all the options available.

From this, an assert_no_tag_in could easily be made. You could use a similar technique to make a assert_select_in, although assert_select is much more complex and often overkill for simple helpers where you aren't testing as large a number of tags or as complicated as nesting.

Update:

Given the interest in a hpricot version of the assert_tag_in, I've put together one:

def assert_tag_in(target, match)
   target = Hpricot(target)
   assert !target.search(match).empty?, 
   "expected tag, but no tag found matching #{match.inspect} in:\n#{target.inspect}"
end

With this you can then write a similar test like this:

def test_for_name_attribute
   tag = text_field_tag('login')
   assert_tag_in tag, 'input[@name="login"]'
end

The alternative test_helper.rb using hpricot can be found in this pastie.

Make Apple’s Addressbook Pull from Exchange

Tony Pitale
Tony Pitale, Web Developer, May 23, 2008 1

If you've ever wished you knew who you worked with but were to afraid to ask...entourage.

Continue reading "Make Apple’s Addressbook Pull from Exchange"

We're the Developers

at Viget Labs. We write about web development trends, tips, best practices, industry events, and our projects — all with an emphasis on Ruby on Rails.

Upcoming Events

Twin Tech III - 6 PM, January 22 - 22, 9 PM

Refresh the Triangle - 6:30 PM, January 22

Recent Comments

:D

thats exactly what i have been looking for, though i do not need it so badly since memoize arrived…