Helping ActiveRecord finders help you

Posted by Jamis on October 06, 2006 @ 10:52 PM

For an application I helped write a few months ago, I found myself needing a way to randomly display a single record from the database. I considered creating a new action that would do this, but I disliked that approach for two reasons:

  • it was almost exactly the same (code and view) as displaying a specific record by ID, and
  • adding the specialized action wasn’t very RESTful, which is something I’ve been trying hard to internalize

My action for displaying a single record looked something like this:

1
2
3
4
5
6
7
8
9
def show
  @thing = Thing.find(params[:id])

  respond_to do |wants|
    wants.html
    wants.js
    wants.xml { render :xml => @thing.to_xml }
  end
end

This allowed my (RESTfully routed) application to happily respond to URL’s of the following format:

  • /things/15
  • /things/15.xml
  • /things/15.js

I realized that all I was wanting was a special case of the ID, the case where the ID is randomly selected. In other words, I wanted to be able to say:

  • /things/random
  • /things/random.xml
  • /things/random.js

But if I did that, the string “random” would be passed to the Thing.find method, and that would be bad, right?

Well…

1
2
3
4
5
6
7
8
9
10
class Thing < ActiveRecord::Base
  def self.find(*args)
    if args.first.to_s == "random"
      ids = connection.select_all("SELECT id FROM things")
      super(ids[rand(ids.length)]["id"].to_i)
    else
      super
    end
  end
end

And voila! It all Just Works.

Posted in Tips & Tricks

Comments

Have something to add? Click here to leave a comment.

07 Oct 2006

1. Thomas said...

maybe… SELECT id FROM things ORDER BY RAND LIMIT 1

2. mdaines said...

Or perhaps this? (or imagine it as it would be inside the find method, with “super”)

Thing.find(:first, :order => 'RANDOM()')

I assume that doesn’t work for all the ActiveRecord adapters? Or is RANDOM actually really standard? And maybe the following would be useful?

Thing.find(:random)
Thing.find(:random, :limit => 5)

Has someone made that plugin already?

3. Jamis said...

Alas, neither “rand” nor “random” exist for MySQL 4.1, which is the database we’re using. Same with sqlite, which is the DB I’ve been using for my test and development environments.

The point of the article, though, was not to demonstrate a technique for grabbing a random record from the database, it was to demonstrate how you can override find to make your controller actions polymorphic. Sorry I wasn’t clearer about that thesis.

4. Kevin Marsh said...

Nice tip… you could even use this for latest, newest, oldest, etc. It really does expose more of your app to the world as a “resource”. I like it!

5. Jesper Rønn-Jensen said...

I really like the idea to let the model pick the random thing. Also heads up for the elegant way you avoid things that are database specific.

Recently I ran into this exact thing with an :order => “rand()” that works with MySQL and MSSQL but not on Sqlite.

Your example could be taken further to see if order by rand() could be emulated.

6. Jamis said...

Kevin, your idea of using this technique for returning things like ‘latest’, ‘newest’, ‘oldest’, etc. really made something click for me. What this technique is all about is allowing find to return records for pseudo-ids. To be really useful, you’d need to make sure your implementation takes into account things like the :conditions option and so forth, as well as considering the current query scope, but none of that is particularly hard. Definitely a fun plugin idea, though!

7. Mislav said...

Jamis, RAND function exists even before MySQL 4… In SQLite, however, it does not.

As Jesper said, we should probably take advantage of the SQL functions in MySQL, Postgres and MSSQL and fall back to emulation (your solution) for SQLite (and maybe some other db).

8. Jamis said...

Ah, so it is! I must have mistyped something when I first experimented with that, because I got errors, but I can see it works now.

Very well. But again, the point was not “how to select a random record from the DB” (which you don’t often need to do) but rather “how to override find to make your actions polymorphic”.

08 Oct 2006

9. jeroen@supercool.nl said...

I must say I don’t really like this. I’m all for the pragmatic approach and I would mind using this in my code, but it seems ugly to have to go through an if statement for all your finders.

Couldn’t you just do something like this inside your controller:

def random @things = Thing.find(:first, :limit => 1, :offset => rand(Thing.count-1)) render :action => ‘show’ end

I’m not sure that render :action will work, I’m not into the RESTful routing etc yet.. So maybe this:

def random redirect_to :action => ‘show’, :id => rand(Thing.count) end

10. jeroen@supercool.nl said...

Whoops! Typo: .. wouldn’t mind using this..

11. Jamis said...

Jeroen, it’s definitely an esthetics issue. Myself, I find adding a specialized “random” action (and, potentially, “latest”, “first”, etc. actions) unappealing (especially in the face of RESTful conventions). The if statement in the find method is a fair tradeoff. Note that you can always refactor the logic into a separate AR method, and have the find simply case it away—much as the base implementation of find does.

12. jeroen@supercool.nl said...

Jamis, I admit haven’t explored REST all that much, but are you saying a method named “random” in your ThingControler isn’t very REST compliant?

Isn’t REST all about how the outside world interacts with your application and not how the internals (controllers etc) are setup?

13. Jamis said...

Jeroen, REST is about exposing resources via unique URL’s, rather than about exposing remote procedure calls. Having a specific action named ‘random’ is not a violation of REST—the whole RESTful thing is pretty laid back, and doesn’t take offense very easily. :) That said, the implementation of the RESTful helpers in Rails are more opinionated, and although you CAN have specific URL’s for specific actions, that’s not the default.

Also, it’s an esthetics thing. Me, I like having the show method “do the right thing” with pseudo-ids. It “feels” right, whereas multiplying the actions in your controller “feels” wrong. One of these days I’ll analyze my feelings and write something about why I think that, but for you now you’ll just have to take my word for it. :)

14. mdaines said...

Sqlite seems to have ‘random’ but not ‘rand’, at least sqlite3 does. That find statement I quoted above works on my sqlite3 development database. (Using sqlite for test and development databases is hella convenient, I must tell you!)

10 Oct 2006

15. jeroen@supercool.nl said...

Jamis,

But won’t this mean that over time my controller methods will end up with if statements all over the place. Like:

def show if ID is really an ID do this if ID is actually a random things, do this if ID is “give me latest entry” do this end

Maybe that’s not all that bad though..

p.s. when I post this form without a name, my message is lost

16. Jamis said...

Jeroen, something I’m coming to feel very strongly about, and which has changed greatly the way I write Rails apps, is that the controllers should be absolutely minimal, and as much (non-view) logic as possible should be delegated to the models. How that is actually accomplished is up to you; I find that overloading find for lookup-based logic is quite handy, but you may prefer to add a specialized finder method that does the branching logic. My ideal controller action is one or two lines related to finding, creating, updating, or deleting the record (or records) in question, and then a three or four line respond_to block indicating what to render and how. There are, of course, exceptions to every rule, but I find that 80% or more of my actions can be done this way if I refactor stuff into the models themselves (appropriately).

(Regarding posting without a name, yah, names are required, but the validation logic is buggy right now.)

12 Oct 2006

17. Rob Pitt said...

Pretty much all the databases have some sort of ORDER BY RANDOM but they are all different, you have to do things like:

case connection
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
...
end
</pre

	

The benefit of Jamis method is it’s cross-compatible with only a small speed penalty on small-medium datasets.