RoR lookup HABTM join by multiple ids - ruby-on-rails

I've got 3 tables -- a model QuizResult, a model QuizAnswers and then a jointable for them (quiz_answer_quiz_results):
class QuizResult < ActiveRecord::Base
has_and_belongs_to_many :quiz_answers, :join_table => :quiz_answer_quiz_results
belongs_to :quiz
has_many :quiz_questions, through: :quiz_answers
end
class QuizAnswer < ActiveRecord::Base
belongs_to :quiz_question
has_and_belongs_to_many :quiz_results, :join_table => :quiz_answer_quiz_results
end
I want to be able to find a QuizResult by searching for attributed quiz_answer_ids and yet I can't figure out the relation, even via sql syntax.
Going in the OPPOSITE direction, if I ask QuizResult.first.answer_ids I get [5,9):
QuizResult.first.quiz_answer_ids
QuizResult Load (2.0ms) SELECT "quiz_results".* FROM "quiz_results" ORDER BY "quiz_results"."id" ASC LIMIT 1
(4.2ms) SELECT "quiz_answers".id FROM "quiz_answers" INNER JOIN "quiz_answer_quiz_results" ON "quiz_answers"."id" = "quiz_answer_quiz_results"."quiz_answer_id" WHERE "quiz_answer_quiz_results"."quiz_result_id" = $1 [["quiz_result_id", 1]]
=> [5, 9]
What I'm trying to do is given quiz_answer_ids 5,9 how can I get back a QuizResult object? I've been attempting all sorts of strange QuizResult.joins(:quiz_answers), or sql queries, but to no avail.

Try:
QuizResult.includes(:quiz_answers).where(quiz_answers: { id: [5,9] })

Related

Query deeply nested relations in rails

We have a lot of through relations in a model. Rails correctly joins the relations, however I am struggling in figuring out how to apply a where search to the joined table using active record.
For instance:
class Model
has_one :relation1
has_one :relation2, through: :relation1
has_one :relation3, through: :relation2
end
If all the relations are different models, we easily query using where. The issue arise rails starts aliasing the models.
For instance, Model.joins(:relation3).where(relation3: {name: "Hello"}) wont work, as no table is aliased relation3.
Is it possible using active record, or would I have to achieve it using arel or sql?
I am using rails 6.0.4.
In a simple query where a table is only referenced once there is no alias and the table name is just used:
irb(main):023:0> puts City.joins(:country).where(countries: { name: 'Portugal'})
City Load (0.7ms) SELECT "cities".* FROM "cities" INNER JOIN "regions" ON "regions"."id" = "cities"."region_id" INNER JOIN "countries" ON "countries"."id" = "regions"."country_id" WHERE "countries"."name" = $1 [["name", "Portugal"]]
In a more complex scenario where a table is referenced more then once the scheme seems to be association_name_table_name and association_name_table_name_join.
class Pet < ApplicationRecord
has_many :parenthoods_as_parent,
class_name: 'Parenthood',
foreign_key: :parent_id
has_many :parenthoods_as_child,
class_name: 'Parenthood',
foreign_key: :child_id
has_many :parents, through: :parenthoods_as_child
has_many :children, through: :parenthoods_as_child
end
class Parenthood < ApplicationRecord
belongs_to :parent, class_name: 'Pet'
belongs_to :child, class_name: 'Pet'
end
irb(main):014:0> puts Pet.joins(:parents, :children).to_sql
# auto-formatted edited for readibility
SELECT "pets".*
FROM "pets"
INNER JOIN "parenthoods"
ON "parenthoods"."child_id" = "pets"."id"
INNER JOIN "pets" "parents_pets"
ON "parents_pets"."id" = "parenthoods"."parent_id"
INNER JOIN "parenthoods" "parenthoods_as_children_pets_join"
ON "parenthoods_as_children_pets_join"."child_id" = "pets"."id"
INNER JOIN "pets" "children_pets"
ON "children_pets"."id" =
"parenthoods_as_children_pets_join"."child_id"
For more advanced queries you often need to write your own joins with Arel or strings if you need to reliably know the aliases used.

Ruby on Rails Active Record: chaining together 3 methods does not work

I'm a student working on a Rails project. I set up the table relationships as shown in the diagram (I think!).
Basically, a Group has many Projects, and a Project has many tasks.
using generic variable names, I was able to get group.projects and projects.tasks to work, but not group.tasks. Is this possible? What am I missing?
Any help would be so much appreciated. Just in case, here are my models (I removed all 'User' info as it is not relevant):
class Group < ApplicationRecord
has_many :projects
has_many :tasks, through: :projects
end
class Project < ApplicationRecord
belongs_to :group
has_many :tasks
end
class Task < ApplicationRecord
belongs_to :project
end
Is group.tasks possible?
Yes this should work out of the box.
One of the better ways to find this out is to look at the sql that is generated from a query like that and see if it jives with what you are wanting. I cleaned this up a little bit but looks exactly like what you are wanting.
irb(main):> Group.first.tasks.to_sql
Group Load (0.1ms) SELECT "groups".* FROM "groups" ORDER BY "groups"."id" ASC LIMIT ? [["LIMIT", 1]]
=> SELECT "tasks".* FROM "tasks"
INNER JOIN "projects"
ON "tasks"."project_id" = "projects"."id"
WHERE "projects"."group_id" = 1"

What is causing this error with my Active Record associations when I use model.collection.build?

In my project, I have the following three classes:
class User < ApplicationRecord
has_many :portfolios, dependent: :destroy
has_many :positions, through: :portfolios
end
class Portfolio < ApplicationRecord
belongs_to :user
has_many :positions, dependent: :destroy
end
class Position < ApplicationRecord
belongs_to :portfolio
end
When I try to build a position directly off the user model (user.positions.build(attributes)) by passing in an existing portfolio_id as one of the attributes, I get the following error:
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection (Cannot modify association 'User#positions' because the source reflection class 'Position' is associated to 'Portfolio' via :has_many.
Why would this happen? I feel there's something to be learned here but I don't really get it!
Addendum: I think my associations make sense: a portfolio should only belong to one user, a position to only one portfolio, and a portfolio should have multiple positions and a user multiple portfolios.
You need to build positions like this
user = User.first
portfolio_attributes = {name: 'Portfolio_1'}
position_attributes = {name: 'Postion_1'}
user.portfolios.build(portfolio_attributes).positions.build(position_attributes)
user.save!
When i run user.positions i get the below result
user.positions
Position Load (0.6ms) SELECT "positions".* FROM "positions" INNER JOIN "portfolios" ON "positions"."portfolio_id" = "portfolios"."id" WHERE "portfolios"."user_id" = ? LIMIT ? [["user_id", nil], ["LIMIT", nil]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Position id: 1, name: "Postion_1", portfolio_id: 1>]>

What's the best way to write a "has_many :through" so that it just uses a plain WHERE query, instead of an INNER JOIN?

I have a User model. All users belongs to a company, and companies have many Records:
class User < ApplicationRecord
belongs_to :company
end
class Company < ApplicationRecord
has_many :records
end
class Record < ApplicationRecord
belongs_to :company
end
I'm using Devise to authenticate users, so I want to start all my queries with current_user, to make sure I'm only returning records that the user is allowed to see. I just want to call something like:
current_user.company_records
(I don't want to call current_user.company.records, because I don't need to load the Company. I just need the records.)
If I do:
has_many :company_records, source: :records, through: :company
Then Rails does an inner join, which is gross:
2.3.3 :030 > user.company_records
Record Load (0.5ms) SELECT "records".* FROM "records" INNER JOIN "companies" ON
"records"."company_id" = "companies"."id" WHERE "companies"."id" = $1 [["id", 1]]
I have to do this:
has_many :company_records,
class_name: 'Record',
primary_key: :company_id,
foreign_key: :company_id
Then Rails will just run a simple "where" query (which has the proper indexes):
2.3.3 :039 > user.company_records
Record Load (0.4ms) SELECT "records".* FROM "records" WHERE "records"."company_id" = $1 [["company_id", 1]]
Is there a nicer way of writing this? I could just do:
def company_records
Record.where(company_id: company_id)
end
... but I want to know if there's a more idiomatic way to do it with ActiveRecord associations.
Preferably use delegate ?
delegate :records, to: :company, prefix: true
Then you can directly use
current_user.company_records

.includes() fails on has_many :through with custom select to retrieve a join table field

Assume I have the following models:
class Article < ActiveRecord::Base
has_many :keyword_mappings, :as => :entity, :dependent => :delete_all
has_many :keywords, -> {select('keywords.*, keyword_mappings.count as count')}, :through => :keyword_mappings
end
class Keyword < ActiveRecord::Base
belongs_to :entity, polymorphic: true
has_many :keyword_mappings, :dependent => :delete_all
end
class KeywordMapping < ActiveRecord::Base
belongs_to :keyword
belongs_to :entity, polymorphic: true
validates :keyword_id, :uniqueness => {
:scope => [:entity_id, :entity_type]
}
end
The intention behind the custom select in the has_many :keywords for Article is that I can loop through an article's keywords while checking the count, individually retrieve the count, or pass it to a Javascript app with the count being part each keyword in a JSON response. count is a field of the keyword_mappings table, which servers as a join table / join model object between the keywords and the articles.
(I found this solution from these two answers.)
With this setup, if I call .includes(:keywords) on a set of articles which contains at least one article that has associated keyword mappings, I get this SQL error:
2.1.1 :001 > Article.order('id desc').limit(1).includes(:keywords)
Article Load (0.7ms) SELECT `articles`.* FROM `articles` WHERE `articles`.`deleted_at` IS NULL ORDER BY id desc LIMIT 1
KeywordMapping Load (0.7ms) SELECT `keyword_mappings`.* FROM `keyword_mappings` WHERE `keyword_mappings`.`entity_type` = 'Article' AND `keyword_mappings`.`entity_id` IN (20813)
Keyword Load (1.2ms) SELECT keywords.*, keyword_mappings.count as count FROM `keywords` WHERE `keywords`.`id` IN (2)
Mysql2::Error: Unknown column 'keyword_mappings.count' in 'field list': SELECT keywords.*, keyword_mappings.count as count FROM `keywords` WHERE `keywords`.`id` IN (2
It seems that it's attempting to access the count field in a separate query from the one in which it access the keyword mappings table, whereas with accessing a single article's keywords (which succeeds), it joins on both:
2.1.1 :002 > Article.last.keywords
Article Load (0.7ms) SELECT `articles`.* FROM `articles` WHERE `articles`.`deleted_at` IS NULL ORDER BY `articles`.`id` DESC LIMIT 1
Keyword Load (0.9ms) SELECT keywords.*, keyword_mappings.count as count FROM `keywords` INNER JOIN `keyword_mappings` ON `keywords`.`id` = `keyword_mappings`.`keyword_id` WHERE `keyword_mappings`.`entity_id` = 20813 AND `keyword_mappings`.`entity_type` = 'Article'
I'd like to use eager loading in this case, while retaining results that do not have any keywords -- is there any way to force .includes() to recognize this pattern and successfully load in the join table field? Or perhaps some way to alter the custom SQL in the has_many to ensure it always joins with the keyword_mappings table?

Resources