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
class Action < ActiveRecord::Base
include ActionStore::Model
end
module ActionStore
module Model
extend ActiveSupport::Concern
included do
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