Query deeply nested relations in rails - ruby-on-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.

Related

Rails: How to find same model association records with no associated records

Given:
class Account < ApplicationRecord
belongs_to :super_account, class_name: 'Account', optional: true, foreign_key: 'account_id'
has_many :sub_accounts, class_name: 'Account'
end
What would be the rails way to find all accounts with no sub_accounts?
Account.left_joins(:sub_accounts)
.where(sub_accounts_accounts: { id: nil })
sub_accounts_accounts is what the joined table is aliased as in the query:
SELECT "accounts".* FROM "accounts"
LEFT OUTER JOIN "accounts" "sub_accounts_accounts"
ON "sub_accounts_accounts"."account_id" = "accounts"."id"
WHERE "sub_accounts_accounts"."id" IS NULL LIMIT $1
.left_joins (aka left outer joins) was introduced in Rails 5. In Rails 4 you need to use .joins with a sql string.
Account.joins(%q{
LEFT OUTER JOIN accounts sub_accounts
ON sub_accounts.account_id = accounts.id
}).where(sub_accounts: { id: nil })
The Rails way is to add :counter_cache to this associations. How to
So you need to add column sub_accounts_count to Account
And add counter_cache: true in model SubAccount
belongs_to :account, counter_cache: true
After this you can just call Account.where(sub_accounts_count: 0) as example.

Why am I getting an ActiveRecord::StatementInvalid error when using the `joins` method on my model class?

I have a simple rails app with the following models and associations:
# app/models/vendor.rb
class Vendor < ActiveRecord::Base
has_many :products
end
# app/models/product.rb
class Product < ActiveRecord::Base
belongs_to :vendor, foreign_key: :account_id
has_many :taxes
end
# app/models/tax.rb
class Tax
belongs_to :product, foreign_key: :item_id
end
Why, when I run Vendor.joins(:products) in the console do I get the following error?
irb(main):039:0> Vendor.joins(:products)
Vendor Load (0.9ms) SELECT "vendors".* FROM "vendors" INNER JOIN "products" ON "products"."vendor_id" = "vendors"."id" LIMIT ? [["LIMIT", 11]]
Traceback (most recent call last):
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: products.vendor_id: SELECT "vendors".* FROM "vendors" INNER JOIN "products" ON "products"."vendor_id" = "vendors"."id" LIMIT ?)
I also get a similar error when I try Product.joins(:taxes). Does it have something to do with the foreign keys not being the default ..._id maybe ?
Because the foreign_key in the products table "pointing" to the vendors table is expected to be vendor_id, as Rails by convention prefixes the table name for foreign key constraints through tables and joins makes use of that column.
From belongs_to Rails documentation:
... By default this is
guessed to be the name of the association with an “_id” suffix. So a
class that defines a belongs_to :person association will use
“person_id” as the default :foreign_key. Similarly, belongs_to
:favorite_person, class_name: "Person" will use a foreign key of
“favorite_person_id”.
What you can do is to "tell" Rails that the foreign_key to be used in the vendors table (Vendor model) is account_id instead of vendor_id.
Try with:
class Vendor < ApplicationRecord
has_many :products, foreign_key: :account_id
...
end
Other way is just to create your own join:
Vendor.joins('INNER JOIN products ON products.account_id = vendors.id')

RoR lookup HABTM join by multiple ids

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] })

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

Mutual friends as an ActiveRecord Arel Relation

I have the following models for my user:
class User < ActiveRecord::Base
has_many :facebook_friendships
has_many :facebook_friends, :through => :facebook_friendships, :source => :friend
def mutual_facebook_friends_with(user)
User.find_by_sql ["SELECT users.* FROM facebook_friendships AS a
INNER JOIN facebook_friendships AS b
ON a.user_id = ? AND b.user_id = ? AND a.friend_id = b.friend_id
INNER JOIN users ON users.id = a.friend_id", self.id, user.id]
end
end
class FacebookFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User'
end
If user with id 53 and user with id 97 are friends with each other, then you would have rows [53, 97] and [97, 53] in the facebook_friendships table in the database. Here is the raw sql query that I have come up with to calculate mutual friends:
SELECT users.* FROM facebook_friendships AS a
INNER JOIN facebook_friendships AS b
ON a.user_id = :user_a AND b.user_id = :user_b AND a.friend_id = b.friend_id
INNER JOIN users ON users.id = a.friend_id
I would mutual_friends_with to return a relation instead of an Array. This way, I could chain the result with other conditions such as where(college: 'NYU') and get all of that ActiveRecord goodness. Is there a good way to do this?
Have you tried #find_by_sql?
http://guides.rubyonrails.org/active_record_querying.html#finding-by-sql
If you’d like to use your own SQL to find records in a table you can
use find_by_sql. The find_by_sql method will return an array of
objects even if the underlying query returns just a single record. For
example you could run this query:
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc")
find_by_sql provides you with a simple way of making custom calls to
the database and retrieving instantiated objects.
This is what I use to get mutual friends.
has_many :company_friendships, autosave: true
has_many :company_friends, through: :company_friendships, autosave: true
has_many :inverse_company_friendships, class_name: "CompanyFriendship", foreign_key: "company_friend_id", autosave: true
has_many :inverse_company_friends, through: :inverse_company_friendships, source: :company, autosave: true
def mutual_company_friends
Company.where(id: (company_friends | inverse_company_friends).map(&:id))
end

Resources