Reputation: 1207
I have inherited some legacy rails code with workarounds around standard polymorphic associations. These used to work up until rails 5. Now I am trying to upgrade to rails6.1 and they break and I have no clue as to how to fix them.
Problem: We have following 2 tables:
posts
comments
A post has 3 types of comments -
We have modeled this as polymorphic relationship as follows:
# comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
# post.rb
class Post < ActiveRecord::Base
has_many :comments, as :commentable, dependent: :destroy, inverse_of: :post
has_many :admin_comments, -> { where commentable_type: "AdminComment" },
class_name: 'Comment', foreign_key: :commentable_id,
foreign_type: :commentable_type, dependent: :destroy, inverse_of: :post
has_many :user_comments, -> { where commentable_type: "UserComment" },
class_name: 'Comment', foreign_key: :commentable_id,
foreign_type: :commentable_type, dependent: :destroy, inverse_of: :post
has_many :anonymous_comments, -> { where commentable_type: "AnonymousComment" },
class_name: 'Comment', foreign_key: :commentable_id,
foreign_type: :commentable_type, dependent: :destroy, inverse_of: :post
end
With this setup, I could fetch either all comments by doing -
p = Post.find(123)
p.comments
or fetch comments by specific type of user -
p = Post.find(123)
p.user_comments
p.anonymous_comments
With rails 6.1 this setup does not work.
When trying to run rails console
It fails with error:
ruby2.7.x/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/hash/keys.rb:52:in `block in assert_valid_keys': Unknown key: :foreign_type. Valid keys are: :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :autosave, :before_add, :after_add, :before_remove, :after_remove, :extend, :counter_cache, :join_table, :index_errors, :ensuring_owner_was (ArgumentError)
After looking for potential answers online, the closest I could get was by adding as: :commentable
to each association like -
has_many :user_comments, -> { where commentable_type: "UserComment" },
class_name: 'Comment', foreign_key: :commentable_id,
foreign_type: :commentable_type, dependent: :destroy, inverse_of: :post, as: :commentable
But this does not get me correct result set. It generates sql as follows -
SELECT comments.* FROM comments WHERE comments.commentable_id = 123 AND comments.commentable_type = 'Post' AND comments.commentable_type = 'UserComment'
without as: :commentable
it would generate sql as -
SELECT comments.* FROM comments WHERE comments.commentable_id = 123 AND comments.commentable_type = 'UserComment'
I am kinda lost and would appreciate any help!
Thanks!
Upvotes: 3
Views: 1222
Reputation: 1173
EDIT
After reading the documentation carefully, this should work for has_one
association types and not has_many
Rails 6.1 added Delegated Types that can be used to achieve what you want.
Try the following
# comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
# post.rb
class Post < ActiveRecord::Base
has_many :comments, as :commentable, dependent: :destroy, inverse_of: :post
delegated_type :commentable, types: %w[AdminComment UserComment AnonymousComment], dependent: :destroy
end
Upvotes: 0
Reputation: 102222
This looks like an extremely confused and misguided attempt at a combination of Single Table Inheritance and a polymorphic assocations with no understanding of how either works and I doubt the code actually worked as intended in earlier versions.
In a STI setup you use the type inheritance column (type by default) to specify what class ActiveRecord will load by when retrieving the rows from the database. That means your comments
table should have a string column named type
containing values such as UserComment
and AdminComment
.
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
class UserComment < Comment
end
class AdminComment < Comment
end
# ...
When you setup a has many assocation pointing to a STI model you should set the condition on the type
column of the table - not commentable_type
:
class Post < ActiveRecord::Base
has_many :comments
has_many :user_comments, -> { where(type: 'UserComment') }
has_many :admin_comments, -> { where(type: 'AdminComment') }
# ...
end
irb(main):002:0> puts Post.joins(:user_comments).to_sql
SELECT "posts".* FROM "posts"
INNER JOIN "comments"
ON "comments"."type" = 'UserComment'
AND "comments"."post_id" = "posts"."id"
Polymorphic assocations are really just a dirty cheat to get around the Object Relational Impedience Missmatch problem. By using a integer column together with a varchar column you can create a single assocation that points to any table - this would be for example attaching comments to posts, videos, images or even other comments. Thats what the commentable_type
column should actually be containing.
To change the assocations from the previous example to be polymorphic you just have to add the as:
option to tell Rails what belongs_to
assocation to look for:
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
has_many :user_comments,
-> { where(type: 'UserComment') },
as: :commentable
has_many :admin_comments,
-> { where(type: 'AdminComment') },
as: :commentable
end
Otherwise it will just assume that you're looking for an assocation named post on the other end - and as:
tells ActiveRecord that its a polymorphic assocation:
irb(main):002:0> puts Post.joins(:user_comments).to_sql
SELECT "posts".* FROM "posts"
INNER JOIN "comments" ON "comments"."commentable_type" = 'Post'
AND "comments"."type" = 'UserComment'
AND "comments"."commentable_id" = "posts"."id"
You don't have to configure the foreign_key
, foreign_type
or class_name
option (this one was dead wrong) as it all can be derived from the name.
Upvotes: 1