May 3 2009

On the Rails Maturity Model

tobyhede

So the Rails Maturity Model is reality.

I hate it.

I hate the actual concept of the Rails Maturity Model.

I hate it so much it’s making me think it’s time to start moving on from Rails and Ruby. Which is probably a slight over-reaction on my part, but anyway.

At best I think RMM is a marketing exercise for certain Rails consultancies.

At worst it fosters a myopic vision of Best Practice founded in the worst kind of Group Think.

As Yehuda Katz posted:

It makes perfect sense to create a forum for sharing and aggregating the practices that people are finding useful at the moment. What makes less sense is creating a ranked list of “popular” practices, with no obvious mechanism for mediating differences except pure popularity. And even worse is ranking firms by their aggregate level of conformance.
(see Incentivizing Innovation for more).

This is really the at the heart of the  problem. It’s not that we’re against a forum for sharing practice and process, but the moment we start rating and ranking organisations we are engaging in the worst kind of group think that many of us are trying desperately to avoid.

If I wanted this sort of Best Practice, I would have stayed in the Java world …

What happens when you disagree with a number of the current  practices that are make up the RMM?

What happens in the case where you have a different development philosophy?

There is no capacity for dissent in the current vision of RMM.

Full disclosure – I really saw red when I saw one of the RMM practices is “Haml for templates”. Genshi is beautiful. ERB is adequate. Haml is clumsy, brutal, ugly, wrong and demonstrably fucked.


Mar 1 2009

Filtering and Named Scopes

tobyhede

I have been delighting in the power of Active Record’s Named Scopes and recently discovered is a technique for cleanly adding user-driven filtering to Active Record models using Named Scopes and a little bit of Ruby magic.

Named Scopes provide a clean way of adding finders to your Active Record Models – collecting complex finder logic into granular methods that can then be chained together to perform complex combinations of queries. Named scopes are eminently testable as each defined scope can be tested individually, as well as the actual combinations of scope-chains.

Take the following example from some recent production code:

named_scope :by_author, lambda { |*args| {:conditions =>; ["author_id = ?", args.first] }}

named_scope :by_state, lambda { |*args| { :conditions => ["state = ?", args.first || "published"]
}}

named_scope :by_date, lambda { |*args| {:conditions => ["published_at BETWEEN ? AND ? OR updated_at BETWEEN ? AND ?", args.first[0], args.first[1] || Time.now, args.first[0], args.first[1] || Time.now]
}}

As you can see, some quite complex logic can be wrapped into the named scopes, including parameters.

The scopes can be chained, which wraps everything into a single query:


Model.by_author(author_id).by_state("published").by_date(10.days.ago)

Once you have your named scopes set up, you can add some magic to dynamically chain them for user-based filtering.

What follows is from Caboose’s: The awesomest filter and sort ever

In the controller I set up some code to grab incoming parameters and pass them to the Model (in this case called BlogPost).


filter_opts = {}
filter_opts[:page] = params[:page] || 1
filter_opts[:author] = params[:user]
filter_opts[:state] = params[:state]
filter_opts[:date] = [start_date, end_date]

@blog_posts = BlogPost.find_by_filter(filter_opts)

I have removed some of the processing logic here, but the idea is that the user selects from a range of filtering options in the User Interface (selecting a date range, for example) and the controller grabs, cleans and validates these as appropriate and passes it through to the model.

The find_by_filter method is where all of the magic happens. We add the valid scopes to an array, and then chain them all together using inject.


def self.find_by_filter(opts = {})
scopes = []

scopes =>; [:by_author, opts[:author] ] if opts[:author]
scopes => [:by_state, opts[:state]] if opts[:state]
scopes => [:by_date, opts[:date]] if opts[:date]

order = opts[:order] || "published_at DESC"
page = opts[:page] || 1

scopes.inject(BlogPost) {|model,scope|
model.scopes[scope[0]].call(model, scope[1])
}.paginate(:all, :o rder => order, :page => page)
end

The final line is the magic. Using inject with the model as the accumulator, basically emulates the chained call we saw earlier (by_author.by_state.by_date), but with the added advantage in this instance that only the scopes with the relevant options (defined in opts) are called by the find_by_filter.

As you can see, not only are the named scopes chained together, but I am adding paginate for good measure. Records are cleanly paginated

For more information on named scopes see
Ruby on Rails Active Record Guide
.