Tomaž Zaman
Tomaž Zaman

Reputation: 318

Getting last related record(s) in Rails

I've been looking for the best method to do this quite some time with mediocre results so I decided to ask here.

The scenario is as follows: I have three models, Task, User and Comment, that essentially look like this:

class Task < ActiveRecord::Base
  belongs_to :user
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :task
end

class User < ActiveRecord::Base
  has_many :tasks
  has_many :comments
end

I'm trying to output a list of tasks (let's say last 10 for the purpose of this question), and associated last comment for each task and it's author (user model) with the least queries possible.

Thank you

UPDATE: I've combined solutions by Blue Smith and harigopal so the solution looks like this:

class Task < ActiveRecord::Base
  belongs_to :user
  has_many :comments
  has_one :last_comment, -> { order 'created_at' }, class_name: "Comment"
end

and then fetch comments like this:

tasks = Task.joins(last_comment: :user)
  .includes(last_comment: :user)
  .order('tasks.created_at DESC').limit(10).load

which produces only one query, which was exactly what I was looking for! Thank you!

Upvotes: 2

Views: 2969

Answers (3)

Blue Smith
Blue Smith

Reputation: 8820

You can try this:

class Task < ActiveRecord::Base
  belongs_to :user
  has_many :comments
  has_one :last_comment, :class_name => 'Comment', :order => 'comments.created_at DESC'
end

@tasks = Task.limit(10).includes(:last_comment => :user)

last_comment is a "virtual" association and just load only one Comment record. The advantage of this approach is that it will use less memory (because just load one Comment and one related User). If you use includes(comments: user) it may consume a lot of memory (if there a lot of comments :).

Upvotes: 1

Hari Gopal
Hari Gopal

Reputation: 718

You'll have to join the tables, and eager-load the data to minimize number of queries.

For example:

tasks = Task.joins(comments: :user)
  .includes(comments: :user)
  .order('tasks.created_at DESC')
  .limit(10).load

comments = tasks.first.comments   # No query, eager-loaded
user = comments.first.user        # No query, eager-loaded

This should reduce the number of queries to just one (very complex one), so you'll have to make sure your indexing is up to snuff! :-D

Official documentation about combining joins and includes is vague, but should help: http://guides.rubyonrails.org/active_record_querying.html#joining-multiple-associations

EDIT:

Here's a demo application with the same models as yours performing eager-loading using the above method. It uses two queries to load the data. Fire up the app to see it in action.

https://github.com/harigopal/activerecord-join-eager-loading

Upvotes: 2

Dani&#235;l Zwijnenburg
Dani&#235;l Zwijnenburg

Reputation: 2560

You could try something along the lines of this:

class Task
  scope :recent, order('created_at desc')
  scope :last, lambda{|n| limit: n }
end

Now you have reusable scopes:

Task.recent.last(10)

And i'm guessing you want to output the last 10 tasks for a given user (let's say the current logged in user).

current_user.tasks.recent.last(10).includes(comments: user)

Upvotes: 1

Related Questions