Active Record tips and tricks 9

Posted by pratik
on Monday, September 15

Just a small collection of tips/tricks which I use a lot ( or try to ), that others might find helpful.

concerned_with

In most of the Rails applications that I work with, the primary model ( User model for example ) ends up being at least 1000 lines long. Thanks to Rick’s quick/awesome solution, we can easily split a model into different “concerns”.

RAILS_ROOT/config/initializers/concerns.rb
1
2
3
4
5
6
7
class << ActiveRecord::Base
  def concerned_with(*concerns)
    concerns.each do |concern|
      require_dependency "#{name.underscore}/#{concern}"
    end
  end
end

Using concerned_with, lets split the User model into 2 different concerns and 3 different files :

  • app/models/user.rb – Main model
  • app/models/user/validations.rb – User validations concern
  • app/models/user/authentication.rb – User authentication concern
RAILS_ROOT/app/models/user.rb
1
2
3
class User < ActiveRecord::Base
  concerned_with :validations, :authentication
end
RAILS_ROOT/app/models/user/validations.rb
1
2
3
class User < ActiveRecord::Base
  validates_presence_of :name
end
RAILS_ROOT/app/models/user/authentication.rb
1
2
3
4
5
class User < ActiveRecord::Base
  def self.authenticate(name, password)
    find_by_name_and_password(name, password)
  end
end

_Pay close attention to the directory structure and how concerns just open the existing class definition, make sure you don’t re-inherit the class from AR::Base inside concerns UPDATE : See comment by Clifford Heath

log_to

I’ve always used log_to for monitoring query output if irb ( script/console ). But with the recent connection pool changes, it stopped working. So here’s a new version :

1
2
3
4
def log_to(stream=$stdout)
  ActiveRecord::Base.logger = Logger.new(stream)
  ActiveRecord::Base.connection_pool.clear_reloadable_connections!
end

So.Many.Joins

As you might already know, you could use association names while constructing a join query with ActiveRecord::Base.find. For example :

1
2
3
class User < ActiveRecord::Base
  has_many :items
end

Now, if you want to find all the users who have a black item, you could query like :


User.all :joins => :items, :conditions => { :"items.color" => 'black' }

Now the black magic part. Not many people know that you can supply the same join key more than once too. So :


User.all :joins => [:items, :items]

will produce a query like :


SELECT `users`.* FROM `users` INNER JOIN `items` ON items.user_id = users.id INNER JOIN `items` items_users ON items_users.user_id = users.id 

This is very useful when you need to make complex sql queries. For example, if you want find all the users who have at least one “black” AND at least one “red” item :


User.all :joins => [:items, :items], :conditions => {:"items.color" => "red", :"items_users.color" => 'black'}

Of course, you’d want to be a little careful with joining too many tables if your tables are very large or if/when performance becomes a problem, etc. YMMV.

Force save

If you want to save an object even if the validations fail ( like, if your boss forces you to ) :


object.save(false)

Find By Bang!

This is new in edge. You can now use dynamic finders with a bang(!). If there is no result found, RecordNotFound exception will be raised :


User.find_by_name!('lifo')
Comments

Leave a response

  1. Piers CawleySeptember 15, 2008 @ 08:18 PM

    find_by_bang! – Hurrah! I’ve been handrolling methods like these for ages.

  2. MartinSeptember 15, 2008 @ 09:36 PM

    Great tips! Thanks for sharing

  3. Lourens NaudeSeptember 15, 2008 @ 11:39 PM

    Concerned with is great – but can trip up when preloading /app/models, which is by default enabled for Edge when cache_classes is true.

    app/models/payment/integrity.rb app/models/payment/payment.rb

    The first file would load before Payment, and not inherit Payment from AR::Base.

    Temp. solution being require_dependency to the parent in each concern to ensure a proper load order.

    Thoughts ?

  4. GeoffSeptember 16, 2008 @ 12:47 AM

    @Lourens: Do you have `app/models/payment.rb` and then the other two? It looks like Pratik’s example used a models/model_name.rb class as the one which inherited AR::Base and then the concerns were kept under models/model_name/ so surely Payment would have already inherited AR::Base. Course I could be horribly horribly wrong as I’ve just read this and not had a chance to play yet.

    @Pratik: Many thanks for your tips

  5. Clifford HeathSeptember 17, 2008 @ 12:18 AM

    Normally, to re-declare the inheritance when re-opening a class isn’t a problem. Why should it be in the case of AR::Base? Is there something I’m missing here? I think you should declare the superclass inheritance in every concern.

  6. PratikSeptember 17, 2008 @ 11:48 AM

    Clifford,

    You’re right. I wanted to make sure user.rb is loaded first. But I guess that doesn’t make much of a difference now that everything gets preloaded.

    I’ve modified the post.

    Thanks!

  7. PratikSeptember 17, 2008 @ 12:00 PM

    Hey Lourens, I think Clifford’s suggestion should solve that problem. If not, we need to change Rails initializer so that it sorts files properly before preloading them.

  8. rickSeptember 20, 2008 @ 11:43 PM

    The problem with declaring the superclass comes up if you use a different superclass. So, obviously don’t do that :)

    I feel like leaving out the superclass is more dry, but perhaps harder to understand. YMMV.

  9. Bence NagyOctober 05, 2008 @ 04:08 PM

    And now on GitHub as a plugin: http://github.com/jakehow/concerned_with

Comment