Wednesday, August 31, 2011

Alphabetical sort method in Rails

So the other day, frustrated with the complete lack of progress I'd made on trying to figure out how to link to a Background model from a Character's view, I decided the best thing to do is to work on some smaller problems until I either ran across a method that seemed like it would work, or INSPIRATO struck me, or I felt like battling that wily Background again.

One of the things I wanted to do is make it so when a user creates a list of Skills, they're displayed in alphabetical order. Sounds simple, right? And it is! Absolutely. But I had only the vaguest idea of how to write such a method (probably finding all the skills, then ordering them by name) and absolutely no idea how to call that method in the view.

So I headed over to Stack Overflow since this seemed like an easy enough problem that I could describe and get answered quickly enough. Who came riding over the hill like Gandalf, ready to save the day? normalocity! I have 0 idea who this guy is, but his method was clear, easy to understand and exactly what I was looking for. Have I mentioned how awesome the Rails community is, both locally and online? One of my favorite things about my learning process has been getting to know more people, and talking to more experienced programmers about Rails, coding and how things work in general.

So here's what I ended up doing. I first had to define the method in my skills_controller (which I had already done, and correctly too!) and then call that method in the view.

So here's the definition I came up with:

def index
  @character = Character.find(params[:character_id])
  @skill = @character.skills.build
  @sorted_skills = @character.skills.find(:all, :order => :name)
end

Not bad! But then I got stuck. How do I call it in the view? Is it a separate thing? I've already got code that iterates over each skill and then spits it back out to the view. So was it a second call? That doesn't seem logical. So what about editing the code I already have, and calling the new method I wrote instead of the previous method?

Just one little change here. We started off with:

<% @character.skills.reject {|skill| skill.new_record? }.each do |skill| %>

And changed it to:

<% @sorted_skills.reject {|skill| skill.new_record? }.each do |skill| %>

So this code is doing the same thing it did before, but instead of simply bringing up @character.skills, it's bringing up @sorted_skills, which has already been defined as @character.skills.find(:all, :order => :name.

And this is just one example. I've got a crazy idea for a way to sort by two variables. For example, a Character will have class and cross-class skills. What about a way to sort these skills both alphabetically and by class or cross class skills? Nutty, I know! I'm letting that one brew for a while, though. Or what about spells? It might make sense to sort spells both by spell level and alphabetically. But you see what I mean? It's kind of getting impossible for me to learn something new in Rails without a) wondering how else I can apply it and b) wondering how I can tweak it, change it, expand it, pose it, scroll it, click it, or zoom it.

It felt really good to figure this out tonight, and even though this one instance is just a tiny fix that literally took two seconds to code, the logic behind it and understanding that logic reaches quite a bit deeper. After all, I'm not learning Rails to build Dungeons and Dragons character databases. I'm learning Rails to understand Rails.

5 comments:

  1. That will work, but an easier way is to handle the sorting in the model. The main benefit of placing it in the model is that if your ordering changes in the future, you can change it in a single place rather than throughout your controllers/views. If you had a class like Skill you could do this in two ways...

    Default Scope
    ===============

    class Skill
    default_scope order("name")
    end

    The skills will then just automatically be sorted without the find in the controller (i.e. @character.skills will return your skills already sorted by name).

    The default_scope method has made for some tricky situations in the past with more complex scoping, but it works in most cases.

    In the future, if you want to add an additional sorting method by something like "color", you could add a class method in your model to override this default_scope...

    class Skill
    def self.by_color
    reorder("color")
    end
    end

    Note that the method "reorder" is called because it needs to override the default_scope. To call this in the controller you would use...
    @character.skills.by_color


    Class methods / Scopes
    ===========================

    Instead of using default_scope, you also could run all of your calls in the controller through a class method that performs your preferred ordering. Here is an example...

    class Skill
    def self.ordered
    order("name")
    end
    end

    If you wanted to call that in the controller you would just use...
    @character.skills.ordered

    This makes for a little more work than the default_scope method as you need to explicitly call "ordered" to sort the records. You can still add the same type of "by_color" sort method like above. The only difference is that now you don't need "reorder" as it doesn't need to override a default_scope...

    class Skill
    def self.by_color
    order("color")
    end
    end

    This is again called in the controller with...
    @character.skills.by_color

    The cool thing with these model-based methods is that you can chain the methods together in the controller and the model.

    ReplyDelete
  2. Also, I should note that I am using the Arel querying method in these examples. This is the method introduced in Rails 3 and you will be best off using it going forward. The old way will be deprecated at some point (originally in the 3.1 release, but I think it is still in there yet). Here is some great documentation for Rails in general...http://guides.rubyonrails.org/

    Take a look at the "Active Record Query Interface" link as it has everything you need to know about Arel. I think you will find it to be quite a bit cleaner and more powerful.

    ReplyDelete
  3. @David so does this go along with the fat models / skinny controllers idea? As in, if you define a method in the model as opposed to the controller, it'll be more flexible and you can use it in multiple views?

    I'll look into this method more tomorrow, but I definitely see the benefit of doing it this way. Thanks for the input!

    ReplyDelete
  4. Yes. If you were to place some special ordering query in 10 different locations within controllers and views and you wanted to update that query, you would then need to update all of those places. If it is centralized in your model, that change only needs to be made in one place.

    ReplyDelete
  5. @David I switched the code I posted earlier with the default_scope order("name") method you showed me. Much nicer! I plan to spend some time this weekend working with the rest of the code you posted. Thanks for the heads-up, this was really good advice.

    ReplyDelete