Polymorphic has_and_belongs_to_many relationship in ActiveRecord

While I was checking this awesome Rails plugin, I was wondering how it can use only one model for different has_and_belongs_to_many relations.

Check readme on github to see what it can do and let’s start to see how it can.

Here is the only migration file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CreateActions < ActiveRecord::Migration[5.0]
def change
create_table :actions do |t|
t.string :action_type, null: false
t.string :action_option
t.string :target_type
t.integer :target_id
t.string :user_type
t.integer :user_id

t.timestamps
end

add_index :actions, [:user_type, :user_id, :action_type]
add_index :actions, [:target_type, :target_id, :action_type]
end
end

and its model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Auto generate with action-store gem.
class Action < ActiveRecord::Base
include ActionStore::Model
end

module ActionStore
module Model
extend ActiveSupport::Concern

included do
# puts "Initialize ActionStore::Model"
belongs_to :target, polymorphic: true
belongs_to :user, polymorphic: true
end
end
end

we need to define the belongs_to relationship using polymorphic: true

to see what has many looks like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# in action-store/lib/action_store/mixin.rb

user_klass.send :has_many, has_many_name, has_many_scope, class_name: 'Action'

user_klass.send :has_many, has_many_through_name,
through: has_many_name,
source: :target,
source_type: target_klass.name


target_klass.send :has_many, has_many_name_for_target, has_many_scope,
foreign_key: :target_id,
class_name: 'Action'
target_klass.send :has_many, has_many_through_name_for_target,
through: has_many_name_for_target,
source_type: user_klass.name,

its ok to ignore those metaprogramming tricks, so to make it more readable
it’s something like

1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
has_many :like_topic_actions, -> {where(action_type: "like", target_type: "Topic", user_type: "User")}, class_name: "Action"
has_many :like_topics, through: "like_topic_actions", source: :target, source_type: "Topic"
end

class Topic < ActiveRecord::Base
has_many :like_by_user_actions, -> {where(action_type: "like", target_type: "Topic", user_type: "User")}, class_name: "Action"
has_many :like_by_users, through: "like_by_user_actions", source: :user, source_type: "User"
end

When we call user.like_topics, ActiveRecord knows we first find the relation through like_topic_actions whose class indeed is Action, and when we get a collection of like_topic_actions, we know we are going to find a collection of the target, so we will find the collection by looking for the target_id in the collections, and the source_type tell us the target_id is the id of Topic
so we should query Topic table for the results.

Since we can define different relation type, it’s easy for us to define different relations between those models.

Here provide a better example.
https://stackoverflow.com/questions/9500922/need-help-to-understand-source-type-option-of-has-one-has-many-through-of-rails

如果你觉得本文对你有帮助,请给我点赞助。