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
en.yml
could do the trick without having to rewrite the code. – dbugger