Doing an ActiveRecord join in RoR seems to work if I look at the generated SQL.
But what I can't figure out is why the result of that SQL isn't returned into the variable.
What I'm doing is:
class Book < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :readers, :through => :readings
accepts_nested_attributes_for :readings
end
class Reader < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :books, :through => :readings
accepts_nested_attributes_for :books
end
class Reading < ActiveRecord::Base
belongs_to :reader
belongs_to :book
end
Now, when asking:
result = Reading.where(:reader_id => rid, ).joins(:book).select(columns.collect{|c| c[:name]}.join(',')).flatten
It shows the correct generated SQL:
SELECT readings.id,books.title,books.author,readings.when FROM `readings` INNER JOIN `books` ON `books`.`id` = `readings`.`book_id` WHERE `readings`.`reader_id` = 2
BUT: the result variable only contains the values of the Reading record, NOT the fields of the joined table.
What am I missing?
I have made the association changes in question also:-
class Book < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :readers, :through => :readings
accepts_nested_attributes_for :readings
end
class Reader < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :books, :through => :readings
accepts_nested_attributes_for :books
end
class Reading < ActiveRecord::Base
belongs_to :reader
belongs_to :book
end
Query in this way:-
reader = Reader.find(rid)
result = reader.books.pluck(:name).join(',')
Ultimately, I've rewritten my helper class and fetched the various fields as I needed them. (as krishnar suggested)
Anyways: Thanx you guys for your contributions.
Related
I am facing a problem while I try to write a rails query using joins.
What I am building is a hash having candidate_answers grouped according to sections of a given question paper. (N.B. I have a pool of questions from where some are added in different sections of a questiion paper)
I have achieved my solution using map as:
exam_candidate.exam.question_paper.sections.includes(:questions).each do |section|
if section.questions.present?
section_question_hash[section] = candidate_answers.where(question_id: section.questions.map(&:id))
end
end
Since using the above creates a lot of database queries running on the background, it is not healthy to use, and thus I need to use joins. Also, I am able to write a SQL query for the same as
select b.name, group_concat(c.id) from sections b
left join question_papers_questions a on a.section_id = b.id
left join candidate_answers c on a.question_id = c.question_id
where a.question_paper_id = 3 and c.exam_candidate_id = 4
group by (b.name)
But while I try the same in rails I am having lots of issues with it.
Here's my model structure:
class ExamCandidate < ActiveRecord::Base
belongs_to :exam
belongs_to :candidate
has_many :candidate_answers, dependent: :delete_all
accepts_nested_attributes_for :candidate_answers
end
class Exam < ActiveRecord::Base
has_many :exam_candidates, dependent: :destroy
has_many :candidates, through: :exam_candidates
belongs_to :question_paper
end
class QuestionPaper < ActiveRecord::Base
has_many :exams, dependent: :nullify
has_many :exam_candidates, through: :exams
has_many :questions, through: :question_papers_questions
has_many :question_papers_questions
has_many :sections, dependent: :destroy
end
class QuestionPapersQuestion < ActiveRecord::Base
belongs_to :question
belongs_to :question_paper
belongs_to :section
end
class Question < ActiveRecord::Base
has_many :candidate_answers, through: :answers
has_many :exams, through: :question_papers
has_many :exam_candidates, through: :exams
has_many :question_papers_questions
has_many :question_papers, through: :question_papers_questions
end
class Section < ActiveRecord::Base
belongs_to :question_paper
has_many :questions, through: :question_papers_questions
has_many :question_papers_questions
end
class CandidateAnswer < ActiveRecord::Base
belongs_to :exam_candidate
belongs_to :question
end
I have given enough time on it, but being almost a newbie in rails is my disadvantage, if anyone can try it or suggest something it would be very helpful.
sections = exam.question_paper.sections.select("sections.name, group_concat(candidate_answers.id) as candidate_answer_ids")
.joins(:question_papers_questions).joins("inner join candidate_answers on question_papers_questions.question_id = candidate_answers.question_id")
.where(candidate_answers: {exam_candidate_id: id}, question_papers_questions: { question_paper_id: exam.question_paper_id })
.group("sections.name")
Rails 4.2 newbie:
2 Questions;
1) Is the first has_many redundant? Since its name is a plural of Save Class?
can I have only:
has_many :savers, through: :saves, source: :saver
Or even better;
has_many :savers, through: :saves
If the answer is yes, where can I set "dependent: :destroy"?
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, class_name: "Save", foreign_key: "saver_id", dependent: :destroy
has_many :savers, through: :saves, source: :saver
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
2) This is the typical blog model, where user can 'save' posts posted by another user to their timeline. Does this model make use best practices? Specially in db performance, doing a Join to get posts saved by a User. The 'Save' table that will have 100MM rows?
Lets first alter your example a bit to make the naming less confusing:
class User
has_many :bookmarks
has_many :posts, through: :bookmarks
end
class Post
has_many :bookmarks
has_many :users, through: :bookmarks
end
class Bookmark
belongs_to :user
belongs_to :post
end
Lets have a look at the query generated when we do #user.posts
irb(main):009:0> #user.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" INNER JOIN "bookmarks" ON "posts"."id" = "bookmarks"."post_id" WHERE "bookmarks"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Now lets comment out has_many :bookmarks and reload:
class User
# has_many :bookmarks
has_many :posts, through: :bookmarks
end
irb(main):005:0> #user.posts
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :bookmarks in model User
So no, the first has_many is not redundant - in fact its the very core of how has_many through: works. You setup a shortcut of sorts through another relation.
Note in has_many :posts, through: :bookmarks :bookmarks is the name relation we are joining through. Not the table which contains the joins.
To fix your original code you would need to do:
class Post < ActiveRecord::Base
has_many :saves, dependent: :destroy
has_many :savers, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
belongs_to :post # A join table with only one relation is pretty worthless.
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts
has_many :saves, dependent: :destroy
has_many :posts, through: :saves
end
Note that you don't need half the junk - if you have has_many :savers, through: :saves ActiveRecord will look for the relation saver by itself. Also you only want to use dependent: destroy on the join model - not on the post relation as that would remove all the posts a user has "saved" - even those written by others!
Teaching Rails myself, I want to learn the professional way to use the framework and following Rails guidelines best practices. That's not easy, because I usually find answers that 'just works'
I'll try to answer myself and maybe it could be useful for Rails Newbies:
Using has_many through, association, Rails firstly infers the association by looking at the foreign key of the form <class>_id where <class> is the lowercase of the class name, in this example; 'save_id'.
So, if we have the column name 'save_id', we will have the following simplified model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :savers, class_name: "User"
validates :save_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
Imagine 4 models in Rails 3.1
class Student < ActiveRecord::Base
has_many :memberships
has_many :courses, :through => :memberships
has_many :tests, :through => :courses
end
class Membership < ActiveRecord::Base
belongs_to :student
belongs_to :course
end
class Course < ActiveRecod::Base
has_many :tests
has_many :students, :through => :memberships
end
class Test < ActiveRecord::Base
belongs_to :course
end
How can I output a sorted list (ie by date) of a student's upcoming tests
(I'm guessing there is a fairly simple answer, but I've been trying in vain for a while)
My best guess is something like:
#upcomingTests = #currstudent.tests.sort_by &:testDateTime
but it seems to return an empty array
First of all, there is an slight error on your model "Course". It needs "belongs_to :student".
class Course < ActiveRecod::Base
has_many :tests
has_many :students, :through => :memberships
belongs_to :student
end
After you've created and populated a foreign key, you can create a simple named_scope on your test mode:
named_scope :ordered, :order => "created_at DESC"
Then it's just the matter of accessing it from wherever you want:
#ordered_tests = #student.tests.ordered
I have the following models:
class User < ActiveRecord::Base
has_many :books, :dependent => :destroy
has_many :favorites
has_many :books, :through => :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :book
belongs_to :user
validates :user_id, :book_id, :presence => true
end
class Book < ActiveRecord::Base
belongs_to :user
belongs_to :favorite
end
The idea is that a user can own a book and add a book from another user as favorite. In rails console, i tried User.find(1).favorites.books but got a NoMethodError: undefined method books'. Anduser.books` only returns the books owned by that user
Is there any way to retrieve all books that belong to a user's favorite in this case?
You are very close, but you shouldn't have two associations name books. Try something like this:
class User < ActiveRecord::Base
has_many :books, :dependent => :destroy
has_many :favorites
has_many :favorite_books, :through => :favorites, :source => :book
end
Then your query would simply be User.find(1).favorites_books
How can I achieve the following? I have two models (blogs and readers) and a JOIN table that will allow me to have an N:M relationship between them:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What I want to do now, is add readers to different blogs. The condition, though, is that I can only add a reader to a blog ONCE. So there mustn't be any duplicates (same readerID, same blogID) in the BlogsReaders table. How can I achieve this?
The second question is, how do I get a list of blog that the readers isn't subscribed to already (e.g. to fill a drop-down select list, which can then be used to add the reader to another blog)?
Simpler solution that's built into Rails:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Note adding the :uniq => true option to the has_many call.
Also you might want to consider has_and_belongs_to_many between Blog and Reader, unless you have some other attributes you'd like to have on the join model (which you don't, currently). That method also has a :uniq opiton.
Note that this doesn't prevent you from creating the entries in the table, but it does ensure that when you query the collection you get only one of each object.
Update
In Rails 4 the way to do it is via a scope block. The Above changes to.
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { uniq }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Update for Rails 5
The use of uniq in the scope block will cause an error NoMethodError: undefined method 'extensions' for []:Array. Use distinct instead :
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
This should take care of your first question:
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates_uniqueness_of :reader_id, :scope => :blog_id
end
The Rails 5.1 way
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What about:
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blog_ids])
Rails takes care of the collection of ids for us with association methods! :)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
The answer at this link shows how to override the "<<" method to achieve what you are looking for without raising exceptions or creating a separate method: Rails idiom to avoid duplicates in has_many :through
The top answer currently says to use uniq in the proc:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
This however kicks the relation into an array and can break things that are expecting to perform operations on a relation, not an array.
If you use distinct it keeps it as a relation:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
I'm thinking someone will come along with a better answer than this.
the_reader = Reader.find(:first, :include => :blogs)
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])
[edit]
Please see Josh's answer below. It's the way to go. (I knew there was a better way out there ;)
I do the following for Rails 6
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates :blog_id, uniqueness: { scope: :reader_id }
end
Don't forget to create database constraint to prevent violations of a uniqueness.
Easiest way is to serialize the relationship into an array:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
serialize :reader_ids, Array
end
Then when assigning values to readers, you apply them as
blog.reader_ids = [1,2,3,4]
When assigning relationships this way, duplicates are automatically removed.