how does has_many :through work with only two models? - ruby-on-rails

Just trying to work out how has_many :through works when there are only two models. I know there are a whole bunch of answers, but none seem to give an example with only using two models, all other examples are using three+ example models.
The question I would like answered is why is it that whilst in the rails console I get two completely separate results with the commands a.friendships vs a.friends, e.g. why does a.friends know to return the users object back to me? but a.friendships does not.
#User.rb
class User < ApplicationRecord
has_many :friendships
has_many :friends, through: :friendships
end
#Friendship.rb
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, class_name: "User"
end
irb(main):020:0> a = User.first
irb(main):016:0> a.friendships
Friendship Load (0.1ms) SELECT "friendships".* FROM "friendships" WHERE "friendships"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Friendship id: 1, user_id: 1, friend_id: 2, created_at: "2019-06-10 20:27:16", updated_at: "2019-06-10 20:31:41">]>
irb(main):020:0> a = User.first
irb(main):019:0> a.friends
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id" WHERE "friendships"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "myemail#gmail.com", created_at: "2019-06-10 20:28:25", updated_at: "2019-06-10 20:28:25">]>

In designing a data model, you will sometimes find a model that should have a relation to itself. For example, you may want to store all employees in a single database model, but be able to trace relationships such as between manager and subordinates. This situation can be modeled with self-joining associations:
class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee"
end
With this setup, you can retrieve #employee.subordinates and #employee.manager.
Source: https://guides.rubyonrails.org/association_basics.html#self-joins

users table
-----------
-id
-...
friendships table
-----------------
- id
- user_id
- friend_id
When you call #user.friendships, the target is to find friendships of #user. it returns objects from friendships table, the middle table, which hold the relationships between users and users' friends
When you call #user.friends, the target is to find friends of the #user. It is to join users table and friendships table, where friendships.user_id = #user.id, then, it gets all friend_id of from those friendships records, and find users who has id included in that friend_ids array.

Related

Why does a polymorphic association in Rails with source_type set result in the wrong SQL statement?

I have three models: User, Organisation and Role. A user has access to many organizations through a role, and an organization can have many users through their roles. In addition, users can have access to other models through roles, so the Role model has a polymorphic belongs_to association named "access_to", and the roles table have the fields "user_id", "access_to_id" and "access_to_type" to track the associations. This all works great, I can use the organisation.users and user.organisations collections and get the expected records back.
However, it has been decided to rename the Organisation model to "Organization" with US English spelling instead of UK English (this is a made up example, the real problem is similar, but with additional complexity irrelevant to this issue). The application has been running for years, so there are thousands of records in the roles table with "access_to_type" set to "Organisation" (with an "s"). Also, the "Organisation" model has to be kept around for legacy code purposes, while the "Organization" model is used by new code.
To achieve this, "source_type: 'Organisation'" is added to the has_many through: associations on User and the new Organization model, so the complete code looks like this:
class Role < ApplicationRecord
belongs_to :user
belongs_to :access_to, polymorphic: true
end
class User < ApplicationRecord
has_many :roles, autosave: true, foreign_key: "user_id"
has_many(
:organizations,
through: :roles,
source: :access_to,
source_type: "Organisation"
)
end
class Organization < ApplicationRecord
self.table_name = 'organisations'
has_many :roles, as: :access_to
has_many :users, through: :roles, source: :access_to, source_type: "Organisation"
end
class Organisation < ApplicationRecord
has_many :roles, as: :access_to
has_many :users, through: :roles
end
Calling "User.first.organizations" still works as expected and returns the expected records with this SQL statement:
SELECT "organisations".* FROM "organisations"
INNER JOIN "roles" ON "organisations"."id" = "roles"."access_to_id"
WHERE "roles"."user_id" = ? AND "roles"."access_to_type" = ?
LIMIT ? [["user_id", 1], ["access_to_type", "Organisation"], ["LIMIT", 11]]
And calling "Organisation.first.users" on the legacy model spelled with an "s" work fine, generating the expected SQL:
SELECT "users".* FROM "users" INNER JOIN "roles"
ON "users"."id" = "roles"."user_id"
WHERE "roles"."access_to_id" = ?
AND "roles"."access_to_type" = ?
LIMIT ?
[["access_to_id", 1],
["access_to_type", "Organisation"],
["LIMIT", 11]]
However, calling "Organization.first.users" does not return any records, and the reason is obvious when looking at the SQL statement Rails generates:
SELECT "organisations".* FROM "organisations"
INNER JOIN "roles" ON "organisations"."id" = "roles"."access_to_id"
WHERE "roles"."access_to_id" = ?
AND "roles"."access_to_type" = ?
AND "roles"."access_to_type" = ?
LIMIT ?
[["access_to_id", 1],
["access_to_type", "Organization"],
["access_to_type", "Organisation"],
["LIMIT", 11]]
The SQL statement looks for Role records where access_to_type is both "Organization" (with a "z") and "Organisation" (with an "s"). It seems that setting source_type: "Organisation" adds an additional condition on access_to_type, rather than replacing the default condition where "Organization" is spelled with a "z".
Also it changes the association to look in the "organisations" table instead of the "users" table. I would it expect it to simply change the "access_to_type" condition.
Why does this work in one direction (finding organisations for a user), but not in the other direction (finding users for an organisation)? Is this a bug in Rails (the double condition could indicate that), or is there something I can fix in the association configuration to make it work? How can the source_type mess up so much in one place, and work fine in another?
(Changing the access_to_type values in the database is unfortunately not an option, as there is other code expecting the data to remain unchanged.)
Here is the problem reproduced in a minimal Rails 6.0 app: https://github.com/RSpace/polymorphic-issue
I found a solution that works around the suspected bug: There is an undocumented method called polymorphic_name that ActiveRecord uses to determine what model name to use when doing polymorphic lookups.
When I change the Organization model to:
class Organization < ApplicationRecord
self.table_name = 'organisations'
has_many :roles, as: :access_to
has_many :users, through: :roles
def self.polymorphic_name
"Organisation"
end
end
then Organization.first.users generates the SQL I want:
SELECT "users".* FROM "users" INNER JOIN "roles"
ON "users"."id" = "roles"."user_id"
WHERE "roles"."access_to_id" = ?
AND "roles"."access_to_type" = ?
LIMIT ? [
["access_to_id", 1],
["access_to_type", "Organisation"],
["LIMIT", 11]]
Commit that fixed my example: https://github.com/RSpace/polymorphic-issue/commit/648de2c4afe54a1e1dff767c7b980bb905e50bad
I'd still love to hear why the other approach doesn't work though. This workaround seems risky, as I simply discovered this method by digging through the Rails code base, and it's only used internally: https://github.com/rails/rails/search?q=polymorphic_name&unscoped_q=polymorphic_name
EDIT: I now understand why setting source_type: "Organisation" results in a lookup in the organisations table rather than the users table, as the source_type option controls both model, table and polymorphic name as per the documentation. There is still a bug around getting "access_to_type" set twice, but fixing that won't get my use case working, as source_type is first and foremost for controlling, well, the source type of the association. I will instead pursue to get the polymorphic_name method documented and thus be part of the official ActiveRecord API.

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

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

Multiple Associations in a Model

I have a User model and an Account model. The user has many accounts and the accounts belong to one user. I have the models and associations all set up. Now I want to make one of those accounts the "primary account". What is the best way to set up the associations? I added a primary_account_id column to my user table and set up the associations like this but it didn't work. Any tips?
class User < ActiveRecord::Base
has_many :accounts
has_one :primary_account, :class_name => "Account"
end
class Account < ActiveRecord::Base
belongs_to :user
end
Edit
I see this question Rails model that has both 'has_one' and 'has_many' but with some contraints which is very similar and the second answer makes the suggestion that I tried. However when I use it rails ignores the column that I've made and just grabs the first one in the table:
>> u = User.find(1)
User Load (3.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
=> #<User id: 1, email: "XXXXXXX#gmail.com", created_at: "2012-03-15 22:34:39", updated_at: "2012-03-15 22:34:39", primary_account_id: nil>
>> u.primary_account
Account Load (0.1ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."user_id" = 1 LIMIT 1
=> #<Account id: 5, name: "XXXXXX", created_at: "2012-03-16 04:08:33", updated_at: "2012-03-16 17:57:53", user_id: 1>
>>
So I created a simple ERD and your issue is very simple, but I think I found a serious issue:
class User < ActiveRecord::Base
has_many :accounts
has_one :primary_account, :class_name => "Account", :primary_key => "account_pimary_id"
end
class Account < ActiveRecord::Base
belongs_to :user
end
To get the associations as is, just set the :primary_key on has_one :primary_account so that it uses users.account_primary_id instead of users.id.
While this works, it will proboably cause nothing but problems. If Account's user_id is used as the foreign key for id and account_primary_id, you have no idea if an Account is a normal Account or a Primary Account without explicitly joining both id and account_primary_id every time. A foreign_key should only point at 1 column, in this case, User's table id. Then it is a straight shot into the Account's table.
#Zabba solution is the smart one, but just needs the :include for the join
has_one :primary_account, :class_name => "Account", :conditions => "users.primary_account_id = accounts.id", :include => :user
This means all Accounts belong to a User and only 1 is flagged as a primary account. Nice and straight forward, avoiding the wacky where clauses.

Resources