Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group Rails ActiveRecord results by a column?

I'm working on a little time tracking application and ran into a problem I don't know how to solve. I have a Task model and a Client model. Each task belongs to a client.

class Task < ActiveRecord::Base
  belongs_to :client
  attr_accessible :client_id, :description, :start, :end

  scope :yesterday, -> {
    where('start > ?', Date.yesterday.to_time).where('start < ?', Date.today.to_time)
  }
end

class Client < ActiveRecord::Base
  attr_accessible :name
  has_many :tasks
end

Right now I'm displaying a list of tasks scoped by the day the tasks were completed, and ordered by the time they were completed. I would like to display that same list, but grouped by the client, and sorted by the client name. Here's what I would like to do:

<div id="yesterday_summary">
  <% @yesterday_clients.each do |client| %>
    <h2><%= client.name %></h2>
    <ul>
      <% client.tasks.each do |task| %>
        <li><%= task.description %></li>
      <% end %>
    </ul>
  <% end %>
</div>

In my controller I currently have:

@tasks_yesterday = Task.yesterday
@yesterday_clients = group_tasks_by_client @tasks_yesterday

And in the group_tasks_by_client method, I have some pretty ugly code that isn't even working at the moment:

  def group_tasks_by_client(tasks)
    clients = []
    tasks.collect(&:client).each do |client|
      clients << {client.id => client} unless clients.has_key? client.id
    end
    clients_with_tasks = []
    clients.each do |client|
      c = Struct.new(:name, :tasks)
      cl = c.new(client.name, [])
      tasks.each do |task|
        cl.tasks << task if task.client_id = client.id
      end
      clients_with_tasks << cl
    end
    clients_with_tasks
  end

I'm sure there is a clean, simple, rails-way to do this but I'm not sure how. How can this be done?

like image 982
Andrew Avatar asked Feb 17 '23 07:02

Andrew


1 Answers

You can have the database do this for you like so:

@yesterdays_clients = Client.includes(:tasks).merge(Task.yesterday).order(:name)

Besides being cleaner, it's more efficient since it gets all your clients and tasks in one pass. The original code was subject to N+1 queries because there was no eager loading.

BTW, you can make your scope simpler as well:

scope :yesterday, -> { where(:start => (Date.yesterday.to_time...Date.today.to_time)) }
like image 119
PinnyM Avatar answered Feb 28 '23 01:02

PinnyM