Good notes posted to Ruby on Rails

RSS feed
November 12, 2009
3 thanks

Use hash form of updates argument

The examples are unfortunate, because passing a string as the updates argument is an invitation to SQL injection attacks. Don’t do this!

 Billing.update_all("author='#{author}'")

Use the hash form of updates instead:

 Billing.update_all(:author => author)

Then the SQL adapter will quote everything safely. Even if [you think] you’re sure there’s no quoting issue, it’s better to cultivate the habit of using the hash form just in case you missed something.

Same with conditions—use the hash or array form rather than a string if there are variables involved.

BTW, to do this and give options, of course you’ll need to put the braces back in:

  Billing.update_all({:author => author},
                     ['title like ?', "#{prefix}%"])
November 5, 2009 - (>= v2.1.0)
6 thanks

Named scope better than conditions

In modern versions of Rails, in most cases a named_scope is a better alternative to using :conditions on your has_many relations. Compare:

  class User
    has_many :published_posts, :conditions => {:published => true}
  end

  user.published_posts

with:

  class Post
    named_scope :published, :conditions => {:published => true}
  end

  class User
    has_many :posts
  end

  user.posts.published

It’s better because the Post's logic ("am I published?") should not be coupled within User class. This makes it easier to refactor: e.g. if you wanted to refactor the boolean :published field into a :status field with more available values, you would not have to modify User class. Having to modify User when you refactor some implementation detail of Post class is clearly a code smell.

This also applies to :order, :group, :having and similar options.

October 22, 2009 - (>= v2.1.0)
3 thanks

Update statement won't include all attributes with ActiveRecord::Dirty

With the addition of ActiveRecord::Dirty, the update statement will only feature changed columns, as opposed to the comment of railsmonk below.

October 7, 2009 - (>= v2.3.2)
3 thanks

Streaming XML with Builder

To generate larger XMLs, it’s a good idea to a) stream the XML and b) use Active Record batch finders.

Here’s one way of doing it:

  def my_action
    @items = Enumerable::Enumerator.new(
      Item.some_named_scope,
      :find_each,
      :batch_size => 500)

    respond_to do |format|
      format.xml do
        render :text => lambda { |response, output|
          extend ApplicationHelper

          xml = Builder::XmlMarkup.new(
            :target => StreamingOutputWrapper.new(output),
            :indent => 2)
          eval(default_template.source, binding, default_template.path)
        }
      end
    end
  end

The Builder template does not need to be modified.

September 14, 2009
3 thanks

Pluralize Without Count

Helper method that returns the word without the count.

application_helper.rb

  def pluralize_without_count(count, noun, text = nil)
    if count != 0
      count == 1 ? "#{noun}#{text}" : "#{noun.pluralize}#{text}"
    end
  end

Example usage:

_form.html.erb

  <%= pluralize_without_count(item.categories.count, 'Category', ':') %>
August 13, 2009 - (>= v2.3.2)
3 thanks

with_exclusive_scope example by Ramon broken in latest Rails

The example Ramon gave works within the model itself, i.e.

  class Article
    def closed
      with_exclusive_scope { find(:all) }
    end
  end

However, from what I can see, this approach does not work within a controller. You may be wanting to use

  Article.with_exclusive_scope { find(:all) }  #=> "SELECT * FROM 'articles'

But it will error out about find(:all) not existing on ArticlesController. To get around this, you must now do

  Article.with_exclusive_scope { Article.find(:all) }  #=> "SELECT * FROM 'articles'

In otherwards, find(:all) isn’t being executed in the scope of the model, but in the controller in which its called.

Took me a minute or two to find out, so I thought I’d let others know.

August 7, 2009
4 thanks

Join multiple tables

It’s easy to join multiple tables too. In this case we have:

  class Article
    belongs_to :feed
  end

  class Feed
    has_many :articles
    belongs_to :source
  end

  class Source
    has_many :feeds
    # t.bool :visible
  end

You can search articles and specify a condition on the sources table.

  Article.find(:all,
    :conditions => { :feeds => { :sources => { :visible => true }}},
    :joins => [:feed => :source],
July 28, 2009
4 thanks

Return True

As is the case with the before_validation and before_save callbacks, returning false will break the callback chain. For example, the expire_cache_id method will not run if Rails.cache.expire returns false (as it will if the key is not cached with memcache).

Returning False Example (Bad)

  after_save :expire_cache_by_name
  after_save :expire_cache_by_id

  def expire_cache_by_name
    Rails.cache.expire("my_object:name:#{self.name}")
  end

  def expire_cache_by_id
    Rails.cache.expire("my_object:#{self.id}")
  end

Returning True Example (Good)

  def expire_cache_by_name
    Rails.cache.expire("my_object:name:#{self.name}")
    return true
  end

  def expire_cache_by_id
    Rails.cache.expire("my_object:#{self.id}")
    return true
  end
July 27, 2009 - (>= v2.2.1)
3 thanks

Overriding default validation messages

Before Rails 2.2 you could globally customize the default validation error messages by changing AR::Base.default_error_messages. The messages have now been moved to i18n, so to customize them in 2.2 and up, just create a locales/ folder in your config/ folder, copy activerecord/lib/active_record/locale/en.yml (in Rails source) to config/locales/en.yml, and then change the strings inside. As szeryf indicated below, the strings of interest are activerecord.errors.messages.

July 23, 2009 - (v1.0.0 - v2.3.2)
4 thanks

Format meaning

%a - The abbreviated weekday name (``Sun’‘)

%A - The full weekday name (``Sunday’‘)

%b - The abbreviated month name (``Jan’‘)

%B - The full month name (``January’‘)

%c - The preferred local date and time representation

%d - Day of the month (01..31)

%H - Hour of the day, 24-hour clock (00..23)

%I - Hour of the day, 12-hour clock (01..12)

%j - Day of the year (001..366)

%m - Month of the year (01..12)

%M - Minute of the hour (00..59)

%p - Meridian indicator (``AM’‘ or ``PM’‘)

%S - Second of the minute (00..60)

%U - Week number of the current year, starting with the first Sunday as the first day of the first week (00..53)

%W - Week number of the current year, starting with the first Monday as the firstday of the first week (00..53)

%w - Day of the week (Sunday is 0, 0..6)

%x - Preferred representation for the date alone, no time

%X - Preferred representation for the time alone, no date

%y - Year without a century (00..99)

%Y - Year with century

%Z - Time zone name

%% - Literal ``%’’ character

June 17, 2009
3 thanks

Skipping validation

Unlike the save method, you can’t pass false to update_attributes to tell it to skip validation. Should you wish to do this (consider carefully if this is wise) update the attributes explicitly then call save and pass false:

  @model_name.attributes = params[:model_name]
  @model_name.save false
June 6, 2009
4 thanks

add index with :quiet=>true option for indices that are possibly already added

# Allows you to specify indices to add in a migration that will only be created if they do not # already exist, or to remove indices only if they already exist with :quiet=>true module ActiveRecord::ConnectionAdapters::SchemaStatements

  def add_index_with_quiet(table_name, column_names, options = {})
    quiet = options.delete(:quiet)
    add_index_without_quiet table_name, column_names, options
  rescue
    raise unless quiet and $!.message =~ /^Mysql::Error: Duplicate key name/i
    puts "Failed to create index #{table_name} #{column_names.inspect} #{options.inspect}"
  end
  alias_method_chain :add_index, :quiet

  def remove_index_with_quiet(table_name, column_names, options = {})
    quiet = options.delete(:quiet)
    raise "no options allowed for remove_index, except quiet with this hack #{__FILE__}:#{__LINE__}" unless options.empty?
    remove_index_without_quiet table_name, column_names
  rescue
    raise unless quiet and $!.message =~ /^Mysql::Error: Can't DROP/i
    puts "Failed to drop index #{table_name} #{column_names.inspect}"
  end
  alias_method_chain :remove_index, :quiet

end

June 4, 2009
3 thanks

A catch-all format

If you’d like to specify a respond_to only for 1 or a few formats and render something else for all other formats, eg: (action.rss returns a feed but action.html or action.js should just render 404), use format.all:

   respond_to do |format|
     format.rss { render_rss }
     format.all { render_404 }
   end

Rails will render an empty string for all formats that don’t specify a response explicitly.

April 28, 2009 - (>= v2.3.2)
3 thanks

A very thorough explanation of use

Ryan Daigle has a great article about 2.3’s new nest forms which does a really good job of explaining how to use this and some of the potential gotchas. Highly recommended:

http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes

April 21, 2009
3 thanks

Format not coming out properly?

Date, Time and DateTime may have different formats defined.

If you do:

    @user.created_at.to_formatted_s(:long_ordinal)

You will get (or something):

    April 16th, 2009 22:03

But if you do:

    @user.created_at.to_date.to_formatted_s(:long_ordinal)

You will get:

    April 16th, 2009

So, be sure you know which one you are working with.

April 9, 2009
3 thanks

Define handlers in order of most generic to most specific

The later the definition of the rescue handler, the higher the priority:

  rescue_from Exception, :with => :error_generic
  rescue_from Exception::ComputerOnFire, :with => :panic

Declaring the Exception catch-all handler last would have the side-effect of precluding any other handlers from running.

This is what is meant by being "searched…from bottom to top".

April 9, 2009
4 thanks

Method has moved to ActionController::Rescue::ClassMethods module

This method has simply moved, still works the same way in 2.3+

New location: ActiveSupport::Rescuable::ClassMethods#rescue_from

April 8, 2009 - (>= v2.3.2)
9 thanks

Setting child_index while using nested attributes mass assignment

When using nested attributes mass assignment sometimes you will want to add new records with javascript. You can do it with pure javascript, but if HTML is long your javascript will be long and messy and it will not be DRY as probably you already have a partial for it.

So to add a partial dynamically you can do something like that (notice string "index_to_replace_with_js"):

link_to_function

  def add_object_link(name, form, object, partial, where)
    options = {:parent => true}.merge(options)
    html = render(:partial => partial, :locals => { :form => form}, :object => object)
    link_to_function name, %{
      var new_object_id = new Date().getTime() ;
      var html = jQuery(#{js html}.replace(/index_to_replace_with_js/g, new_object_id)).hide();
      html.appendTo(jQuery("#{where}")).slideDown('slow');
    }
  end

js method in one of helpers (from minus mor plugin)

  def js(data)
    if data.respond_to? :to_json
      data.to_json
    else
      data.inspect.to_json
    end
  end

This method will generate link adding generated partial to html.

The thing that is not mentioned in docs is how to set child_index. You must add it as an argument in hash.

Example of partial

  <% form.fields_for :tasks, task, :child_index =>
         (task.new_record? ? "index_to_replace_with_js" : nil) do |tasks_form| %>
    <% tasks_form.text_field :name %>
  <% end %>

Using add_object_link

  <% form_for :project do |form| %>
    <div id="tasks">
      <%# displaying existing tasks %>
    </div>

    <%= add_object_link("New task, form, Task.new, "task", "#tasks") %>

  <% end %>

Thanks to child_index after insertion it will change indexes to current time in miliseconds so added tasks will have different names and ids.

April 8, 2009 - (v2.3.2)
4 thanks

Superclass of OrderedHash

Note that in Rails 2.3, OrderedHash changed from being a subclass of Array to a subclass of Hash. This is contrary to what the documentation says above.

April 6, 2009
5 thanks

HTML entities in options

Unfortunately everything is escaped with ERB::Util#html_escape. Your only option is either manually construct options or compeletely overwrite this method.

April 6, 2009
3 thanks

Array clustering

Sometimes you don’t want to mangle sequence of an array and just want to group adjacent values. Here’s a nice method to do so (drop it in your initializers directory or something):

  module Enumerable
    # clumps adjacent elements together
    # >> [2,2,2,3,3,4,2,2,1].cluster{|x| x}
    # => [[2, 2, 2], [3, 3], [4], [2, 2], [1]]
    def cluster
      cluster = []
      each do |element|
        if cluster.last && yield(cluster.last.last) == yield(element)
          cluster.last << element
        else
          cluster << [element]
        end
      end
      cluster
    end
  end

Similarly you can do the clustering on more complex items. For instance you want to cluster Documents on creation date and their type:

  Document.all.cluster{|document| [document.created_on, document.type]}
April 1, 2009
3 thanks

Ordering of format blocks is important

The order in which your format blocks appear, like:

format.html { } format.js { }

are used to infer priority in cases where the appropriate format is ambiguous.

March 23, 2009 - (v2.3.2)
4 thanks

So, how do you enable db sessions?

First, run:

  rake db:sessions:create

Then, run your pending migrations. This will create the migration you need to run in order to create the sessions table.

Second, go into config/environment.rb and uncomment or put in:

  config.action_controller.session_store = :active_record_store
  config.action_controller.session = {
     :session_key => '_your_session_name_here',
     :secret      => 'SOME_CRYPTOGRAPHICALLY_SECURE_KEY'
   }

Third, get yourself a secure key with:

  rake secret

And finally, paste your new key into the :secret above.

March 21, 2009
4 thanks

Use helpers in your ActionMailer views

It’s very easy to give your mailer access to helpers:

  # Let your mailer user the ApplicationHelper methods
  class MyMailer < ActionMailer::Base
    helper :application
  end
March 12, 2009
3 thanks
March 12, 2009
8 thanks

User a block to extend your associations

You can use blocks to extend your associations with extra methods.

code sample

  has_many :children, :dependent => :destroy do
    def at(time)
      proxy_owner.children.find_with_deleted :all, :conditions => [
        "created_at <= :time AND (deleted_at > :time OR deleted_at IS NULL)", { :time => time }
      ]
    end
  end

  Model.children.each # do stuff
  Model.children.at( 1.week.ago ).each # do old stuff

you must use ‘proxy_owner’ to link back to your model.

March 10, 2009 - (>= v2.1.0)
5 thanks

Use lambda to avoid caching of generated query

If you’re using a named_scope that includes a changing variable you need to wrap it in a lambda to avoid the query being cached and thus becoming unaffected by future changes to the variable, example:

  named_scope :translated, :conditions => { :locale => I18n.locale }

Will always return the same locale after the first hit even though I18n.locale might change. So do this instead:

  named_scope :translated, lambda { { :conditions => { :locale => I18n.locale } } }

Ugly, but at least it’s working as we expect it…

March 4, 2009
7 thanks

Differences between normal or-assign operator

Differences between this method and normal memoization with ||=:

  • memoize works with false/nil values
  • Potential arguments are memoized

Take the following example:

  def allowed?
    @allowed ||= begin
      # Big calculation
      puts "Worked"
      false
    end
  end

  allowed? # Outputs "Worked"
  allowed? # Outputs "Worked" again

Since @allowed is set to false (this is also applicable with nil), the ||= operator will move on the the next statement and will not be short-circuited.

When you use memoize you will not have this problem.

  def allowed?
    # Big calculation
    puts "Worked"
    false
  end
  memoize :allowed?

  allowed? # Outputs "Worked"
  allowed? # No output

Now, look at the case where we have parameters:

  def random(max=10)
    @random ||= rand(max)
  end

  random     # => 4
  random     # => 4 -- Yay!
  random(20) # => 4 -- Oops!

Better use memoize again!

  def random(max=10)
    rand(max)
  end
  memoize :random

  random     # => 6
  random     # => 6 -- Yay!
  random(20) # => 12 -- Double-Yay!
  random     # => 6 -- Head a'splode
March 4, 2009
3 thanks

Usage

This defines attr_accessors at a class level instead of instance level.

  class Foo
    cattr_accessor :greeting
  end

  Foo.greeting = "Hello"

This could be compared to, but is not the same as doing this:

  class Bar
    class << self
      attr_accessor :greeting
    end
  end

  Bar.greeting = "Hello"

The difference might not be apparent at first, but cattr_accessor will make the accessor inherited to the instances:

  Foo.new.greeting #=> "Hello"
  Bar.new.greeting # NoMethodError: undefined method `greeting' for #<Bar:0x18e4d78>

This inheritance is also not copy-on-write in case you assumed that:

  Foo.greeting  #=> "Hello"
  foo1, foo2 = Foo.new, Foo.new

  foo1.greeting = "Hi!"

  Foo.greeting  #=> "Hi!"
  foo2.greeting #=> "Hi!"

This makes it possible to share common state (queues, semaphores, etc.), configuration (max value, etc.) or temporary values through this.

February 27, 2009
4 thanks

Extend with an anonymous module

You can extend with an anonymous module for one-off cases that won’t be repeated:

  belongs_to :container, :polymorphic => true, :extend => ( Module.new do
      def find_target
        ...
      end
    end )

The parentheses are important, will fail silently without them.