Selecting from joined tables in ActiveRecord - ruby-on-rails

I have the following models
class Forum < ActiveRecord::Base
has_many :topics
end
class Topic < ActiveRecord::Base
belongs_to :forum
has_many :posts
end
class Posts < ActiveRecord::Base
belongs_to :topic
belongs_to :user
end
I would like to join these associations (with some conditions on both topics and posts), so I did Topic.joins(:forum, :posts).where(some condition), but I would like to access the attributes in all three tables. How can I achieve this?

You can do a multiple join like that. All three tables are available, you just have to specify in the wheres.
Topic.joins(:forum, :posts).where('topics.foo' => 'bar').where('posts.whizz' => 'bang)
The two caveats are that that is a multiple inner join so the topic will need to have both forum and post to show up at all and that the where's are anded together. For outer joins or 'or' logic in the where's you will have to get fancier. But you can write sql for both .joins and .where to make that happen. Using .includes instead of .joins is another way to get outer join behavior--but of course you are then maybe loading a lot of records.
To clarify the comment below, and show the improved (more rails-y) syntax where syntax suggested by another user:
topics = Topic.joins(:forum, :posts).where(topic: {foo: 'bar'}).where(posts: {whizz: 'bang'})
topics.each do |t|
puts "Topic #{t.name} has these posts:"
t.posts.each do |p|
puts p.text
end
end

Related

Rails, active record. Joining four tables together

I am learning ruby on rails and I've run into an active record problem I have been unable to solve. I am trying to inner join four tables together and display data from them.
I have confirmed that my database schema is correct by running the following SQL style query. It returns the data I expect it to.
select * from sailings
INNER JOIN travelling_parties on sailings.id = travelling_parties.sailing_id
INNER JOIN party_registers on travelling_parties.id = party_registers.travelling_party_id
inner JOIN users on party_registers.user_id = users.id
where users.id = 8
After reading through the great docs on guides.rubyonrails.org, I am still unable to find the solution. The below query is what I've come up with in my controller, but it doesn't seem to be return what I need it to.
#sailing = Sailing.joins(:travelling_parties => [{:party_registers => :user}])
I would appreciate anyone who could steer me in the right direction.
Thanks
EDIT 1: Here are my models. I am attempting to check user.id = current_user.id and display data based on that.
class Sailing
has_many :travelling_parties
end
class TravelingParty
has_many :party_registers
has_many :users, through: :party_registers
end
class PartyRegister
belongs_to :user
belongs_to :travelling_party
end
class User
has_many :party_registers
has_many :travelling_parties, through: :party_registers
end
EDIT 2: The following query gave me my intended results
#sailing = Sailing.joins(:travelling_parties => {:party_registers => :user}).where("users.id" => current_user.id)
Thanks for the responses.
Your answer is close to correct assuming that you have defined the associations.
class Sailing
belongs_to :traveling_party
end
class TravelingParty
belongs_to :party_register
end
class PartyRegister
belongs_to :user
end
The
#sailing = Sailing.joins(:travelling_parties => {:party_registers => :user})

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!
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
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)
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 :).

Rails - Eager Load Association on an Association

EDIT - Using 'includes' generates a SQL 'IN' clause. When using Oracle this has a 1,000 item limit. It will not work for my company. Are there any other solutions out there?
Is it possible to eager load an association on an association?
For example, let's say I have an Academy class, and an academy has many students. Each student belongs_to student_level
class Academy < ActiveRecord::Base
has_many :students
end
class Student < ActiveRecord::Base
belongs_to :academy
belongs_to :student_level
end
class StudentLevel < ActiveRecord::Base
has_many :students
end
Is it possible to tailor the association in Academy so that when I load the students, I ALWAYS load the student_level with the student?
In other words, I would like the following section of code to produce one or two queries total, not one query for every student:
#academy.students.each do |student|
puts "#{student.name} - #{student.student_level.level_name}"
end
I know I can do this if I change students from an association to a method, but I don't want to do that as I won't be able to reference students as an association in my other queries. I also know that I can do this in SQL in the following manner, but I want to know if there's a way to do this without finder_sql on my association, because now I need to update my finder_sql anytime my default scope changes, and this won't preload the association:
SELECT students.*, student_levels.* FROM students
LEFT JOIN student_levels ON students.student_level_id = student_levels.id
WHERE students.academy_id = ACADEMY_ID_HERE
Have you tried using includes to eager load the data?
class Academy < ActiveRecord::Base
has_many :students
# you can probably come up with better method name
def students_with_levels
# not sure if includes works off associations, see alternate below if it does not
self.students.includes(:student_level)
end
def alternate
Student.where("academy_id = ?", self.id).includes(:student_level)
end
end
see also: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
should result in 3 queries
the initial find on Academy
the query for a collection of Student objects
the query for all of those Students StudentLevel objects
Additions:
# the same can be done outside of methods
#academy.students.includes(:student_level).each do |student|
puts "#{student.name} - #{student.student_level.level_name}"
end
Student.where("academy_id = ?", #academy.id).includes(:student_level).each do |student|
puts "#{student.name} - #{student.student_level.level_name}"
end
ActiveRelation queries are also chainable
#academy.students_with_levels.where("name ILIKE ?", "Smi%").each do # ...
Sort of related a nice article on encapsulation of ActiveRecord queries (methods) - http://ablogaboutcode.com/2012/03/22/respect-the-active-record/

How to write Rails finder with several subqueries

This is a library system, people can borrow books here. And each book belongs to a category. We'd like to give people some suggestions according to what kind of books they borrowed most.
Here are four models:
class Person < AR
has_many :borrows
end
class Borrow < AR
belongs_to :person
belongs_to :book
end
class Category < AR
has_many :books
end
class Book < AR
has_many :borrows
belongs_to :category
end
And I wrote SQL to find the books
SELECT * FROM books WHERE category_id =
(SELECT category_id FROM books WHERE id IN
(SELECT book_id FROM borrows WHERE person_id =10000)
GROUP BY category_id ORDER BY count(*) DESC LIMIT 1)
AND id NOT IN
(SELECT book_id FROM borrows WHERE person_id =10000)
This seems to be working, but I wonder how could I write the finder in the Rails way...
You can do following things, write following in person.rb
has_many :books, :through => :borrows
has_many :categories_of_books, :through => :books, :source => :category
&
def suggested_books
Book.where("category_id IN (?) AND id NOT IN (?)", self.categories_of_books, self.books)
end
Though it results in more than 1 query, but its clean, you just have to do:
#user.suggested_books
With active record, you can eliminate two of the three subqueries in favor of joins:
Book.where(
category_id: Category.limit(1)
.joins(:books => :borrows)
.where("borrows.person_id = ?", 10000)
.group("categories.id")
.order("COUNT(*) DESC")
.pluck("categories.id")
).joins(:borrows).where("borrows.person_id != ?", 10000)
Still not the best solution because it generates two separate queries (one for the inner query on Category). Depending on your needs, this may not be so bad, if, say, you decide to use the result of the inner query (the most borrowed category of the user in question) for something else.
May be something like that :
#person = Person.find(10000)
#categories = #person.books.map{|b| b.category}.uniq!
#suggestions = #categories.map{|c| c.books} - #person.books
In order to have '#person.books' working, you have to add in your Person model :
has_many :books, :through => :borrows

Find all associated objects by specific condition

class QuestionGroup < ActiveRecord::Base
has_many :questions
end
class Question < ActiveRecord::Base
belongs_to :question_group
has_many :question_answers
has_many :question_users_answers, :through => :question_answers, :source => :user_question_answers
def self.questions_without_answers(user_id)
select {|q| q.question_users_answers.where(:user_id=>user_id).empty?}
end
end
class QuestionAnswer < ActiveRecord::Base
belongs_to :question
has_many :user_question_answers
end
I need find all Questions if they have no user answers I did it by class method self.questions_without_answers(user_id)
But how can I find all QuestionGroups where present questions_without_answers and for particular user?
P.S: I need to find all unanswered questions and all groups that own these questions, can I do it by find or named-scope?
UPDATED:
def self.groups_without_answers(user_id)
questions_ids = Question.questions_without_answers(user_id).map {|q| q.id}
all(:conditions => "id in (select distinct question_group_id from questions where id in (#{questions_ids.join(',')}))")
end
But I think it is not good or maybe I wrong?
class QuestionGroup < ActiveRecord::Base
has_many :questions
def self.without_answers(user_id)
joins(%"inner join questions on question_groups.id = questions.question_group_id
inner join question_answers
on question_answers.question_id = questions.id
inner join question_groups
on question_answers.question_users_answers_id = question_users_answers.id").where("user_question_answers.user_id" => user_id).select { |qq| ... }
end
end
end
You can change some of the inner joins to left out join to pick up records where the table you are joining to doesn't have a match, for instance where there is no answer. The fields of the table you are joining to will have NULL values for all the fields. Adding a where id is null will even filter to just the questions with no answers.
Keep in mind that this is just an alternative technique. You could programmatically solve the problem simple with:
class QuestionGroup
def self.question_groups_without_answers(user_id)
select {|qq| qq.question_users_answers.where(:user_id=>user_id).empty?}.map{ |qq| qq.question_group }
end
end
An advantage of doing the joins is that the database does all the work, and you don't send several SQL queries to the database, so it can be much faster.

Resources