AssetsGraphed: Part 2

Posted by koz Wednesday, January 10, 2007 22:14:00 GMT

This is part two in our series reviewing, John Topley’s AssetsGraphed.

One of the common anti-patterns I saw was the overuse of, and over-reliance on, the identifiers of objects. In general, unless it’s going into a url or the session, there’s very little reason for you to be passing around the id of an active record object, you should be using the object itself.

There are a few places in Assets Graphed which use IDs, but the most common is the get_account_id method in ApplicationController.

1
2
3
  def get_account_id
    session[:account_id]
  end

This is used all over the application, and it’s almost always passed straight to Account.find. A simpler, and probably more efficient, alternative is to make the application helper return the current_account instead of its ID, not only is this ‘more object oriented’, you can also use memoization to ensure you only issue one database query per request.

1
2
3
  def current_account
    @account ||= Account.find(session[:account_id])
  end

Now you can call current_account whenever you need the current account, instead of Account.find(get_account_id).

One extremely common (ab)use of IDs is using them for finding associated records. The FeedController contains the following code for finding the first 10 Incoming’s for a given Asset.

1
2
3
4
5
6
  def asset
    @asset = Asset.find_by_feed_url(params[:id])
    @incomings = Incoming.find(:all, :conditions => { :asset_id => @asset.id },
      :limit => 10)
    #...
  end

However, Active Record’s associations let you avoid this use of the id. To find the first 10 Incomings, simply ask the asset for them.


  @asset.incomings.find(:all, :limit=>10)

Once you start thinking about your methods in terms of the objects they relate to, instead of the opaque integers which identify them, several other refactorings become apparent.

Jamis says:

Always be on the lookout for duplicated code. If you find yourself doing asset.incomings.find(:all, :limit => 10) in multiple places, then perhaps you need to pull that into a method of its own. Given the use of the magic number “10”, it might not be a bad idea to do so anyway:

1
2
3
4
5
6
7
class Asset < ActiveRecord::Base
  has_many :incomings do
    def recent(count=10)
      find(:all, :limit => count)
    end
  end
end

Then, you just have to do asset.incomings.recent to get the first 10 items.

Another common use of IDs, is when manipulating belongs_to associations, for example the create method of the AssetController contains the following code.

1
2
3
4
5
6
7
8
9
10
  def create
    @asset = Asset.create(params[:asset])
    @asset.starting_balance = params[:asset][:starting_balance]
    @asset.account_id = get_account_id
    if @asset.save
      flash[:notice] = "Successfully added #{@asset.name}."
      redirect_to assets_path
    else
      render :action => :index
    end

If we transform it to use the account object instead of the ID, it will look like this:


  @asset.account = current_account

But once you’re dealing with the object itself, it should become apparent that we can use the association proxies to tidy up that code.


  @asset = current_account.assets.build params[:asset]

This lets you halve the code, and also ensures that the Asset appears in current_account.assets, guaranteeing a consistent in memory object graph.

For a final example we can take a look at the total_incomings method on the Asset class. The current implementation takes the ID of an account, and returns the ‘number of incomings’ across all its assets.

1
2
3
4
5
6
7
8
9
  def self.incomings_count(account_id)
    incomings_count = 0
    assets = Asset.find_all_by_account_id(account_id) or
      raise ActiveRecord::RecordNotFound
    assets.each do |asset|
      incomings_count += asset.incomings.count
    end
    incomings_count
  end

The first step is to refactor this to a method on an object instance, instead of a static method taking an identifier. The purpose of the method is to determine the “number of incomings” for an Account, so ideally we can call it with @account.number_of_incomings. If we move the implementation to the Account class, and throw in some Enumerable#inject magic we’re left with:

1
2
3
4
5
  class Account < ActiveRecord::Base
    def number_of_incomings
      assets.inject(0) {|total, asset| total += asset.incomings.size }
    end
  end

While this is functionally correct, it’s going to issue one database query for every Asset belonging to an account. This will put excessive load on the database, and it’s just plain wasteful. Thanks to has_many :through we can issue one database query to figure it out. First define the associations.

1
2
3
4
  class Account < ActiveRecord
    has_many :assets
    has_many :incomings, :through=>:assets
  end

Now you can get the number of incomings for an account the same way you would any other association. @account.incomings.size

So every time you find yourself accessing an object’s id, try to think if it’s really necessary, chances are there’s a nice opportunity to improve the design, and performance, of your application.

Comments

Leave a response

  1. Sandro PaganottiJanuary 11, 2007 @ 09:40 AM

    Really amazing guys! Thank you very much, this website is my best resource for rails tips and suggestion.

  2. Dave HooverJanuary 11, 2007 @ 02:50 PM

    Great post guys. This information really needs to get further out into the Rails community.

  3. John TopleyJanuary 11, 2007 @ 03:42 PM

    I think with some of this stuff I was so busy trying to learn Ruby and Rails that I couldn’t see the forest for the trees when it came to good OO design. The great thing about having your code reviewed on this site is that you’re effectively getting top-notch Rails consultancy for free. The worst thing is that you can look like a bumbling amateur! ;-)

  4. Joshua WarcholJanuary 11, 2007 @ 04:50 PM

    There’s probably nothing wrong with it, but the fact that account.incoming is mentioned three times with three different uses puzzled me. Is it :through, does it have extensions, etc? Great post, this site is a terrific resource.

  5. Trevor StowJanuary 11, 2007 @ 05:33 PM

    Thanks for the post.

    Wouldn’t it be handy to include, on this page, a link back to part one of this article? Perhaps near the top of the article where I’ll first expect to find it?

    Or is my vision failing me?

  6. Neeraj KumarJanuary 11, 2007 @ 05:35 PM

    Thanks for the great tips.

    Every time I read an article here, I instantly have an urge to refactor my code. Your articles make me look dumb but that’s a good sign. The alternative is to be dumb all the time. Thanks a bunch.

  7. Jason L.January 11, 2007 @ 05:37 PM
    Great post guys, thanks very much. This is the kind of stuff that makes me remember why I love RoR – the feel-good object-oriented nature of it. One quick question – above, where you call the function called ‘build’: @asset = current_account.assets.build params[:asset] From the looks of it, I’m guessing ‘build’ is similar to ‘create’, but used in an associative way. I’ve been doing something similar, but instead I typically do:
    @asset = current_account.assets.new params[:asset]
    @asset.save
    The Rails API doesn’t have any comments for ‘build’, but if this is all it does, it’d save me some extra lines here and there.
  8. Piotr UsewiczJanuary 11, 2007 @ 07:27 PM

    @ Jason L. – Yes it does have docs. Look at docs for has_many relation. Build is pretty much similar to new but it does something more. It fills the foreign key for you, so in this case @asset.account_id is already set to the @account.id. So while doing @asset.save the model is already linked to the parent.

  9. Jason L.January 11, 2007 @ 07:45 PM

    Ah, thanks Piotr. Looking back at some actual code, I see I’ve been doing another extra step when I use new (setting the foreign key by doing something like @asset.account = current_account), or in other cases I’ve been using ‘create’ which sets the FK as well (when doing @asset = current_account.assets.new params[:asset]).

    Very good to know – I’m sure that has caused me trouble in the past, where I’ve expected ‘new’ to set the FK and I should be using ‘build’ instead.

  10. Michael KoziarskiJanuary 11, 2007 @ 07:48 PM

    Trevor,

    Good point, I’ll update our ‘series’ sidebar to include these ones.

  11. Silvio GissiJanuary 12, 2007 @ 01:00 AM

    Koz, great article! By reading your post I actually found out that I had the opposite problem on my app, I kept the object itself at @session[:client] and I realized now that, if one user updates the object, the other users would not see the change. Of course this also made me realize that I have tons of @session[:client] calls all over my controllers and views that are screaming for a helper function.

    Congratulations for all the articles, there wasn’t a single one where I didn’t learn something new that I could use on my own projects.

  12. Bart BraemJanuary 12, 2007 @ 09:58 PM

    Interesting! Just a small question: what’s the lifetime of a memoization? Is the state kept for the lifetime of 1 request? What about using it with AJAX stuff?

    Thanks for your great hints! Bart

  13. JamisJanuary 12, 2007 @ 10:02 PM

    The memoization lasts for as long as the instance variable lives, which (unless you manually reset it somehow) is as long as the object lives. Rails reinstantiates all controllers and models on every requests, but does not reload the classes (in production mode), so be careful not to memoize on class methods unless you really mean them to remain constant for the life of the process.

  14. Bart BraemJanuary 13, 2007 @ 11:30 AM

    So the way you implemented the current_account method it will not make any computation after the first call (when the computation is “cached”) and the beginning of a new request? I find that a really interesting approach as I am working on a project with a similar performance hit where checking on logged in users and their rights requires DB access. But it could be cached for just one request this way, right? (I know I’m going to end up sending you the entire code, but I want to try and improve the codebase myself first, based on your posts.)

  15. KozJanuary 14, 2007 @ 03:45 AM

    Bart,

    Yep, if you memoize in your current_account method you will only do a single database query per request.

  16. John TopleyJanuary 15, 2007 @ 03:22 PM

    The last code example has this association:

    has_many :incomings, :through=>:assets

    What should that association be if using the first refactoring which saw the Incoming and Outgoing models replaced with an Item model?

  17. JamisJanuary 15, 2007 @ 03:43 PM

    John, you would specify the name of the collection on Asset that you want to query. In other words, if you now have “has_many :items” on Asset, you’ll have “has_many :items, :through => :assets” on Account.

  18. John TopleyJanuary 15, 2007 @ 04:25 PM

    Jamis,

    That’s what I thought.

    However, that doesn’t let me get a count of Incomings or Outgoings belonging to an Account which is what the original method did. Do I have to use the number_of_incomings method (adapted for Items), or is there a way to do it for free purely using associations?

  19. JamisJanuary 15, 2007 @ 05:28 PM

    John, you should be able to do the module thing, as discussed before:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    
    has_many :items, :through => :assets do
      def incoming_count
        count(:conditions => "base_amount > 0")
      end
    
      def outgoing_count
        count(:conditions => "base_amount < 0")
      end
    end
    
    account.items.incoming_count
    account.items.outgoing_count
    
  20. John TopleyJanuary 15, 2007 @ 05:37 PM

    D’oh! Thanks Jamis, that makes sense.

  21. Chuck BergeronJanuary 19, 2007 @ 06:46 AM

    Hey guys, great articles so far! I’ve picked up so many great refactoring tips from this site.

    Just wanted to make a comment to Silvio, I believe @session is deprecated in the upcoming rails version, and you may want to switch them to just session instead. Same goes for @params to params and @request to request.

    Hope that helps!