Logo

新手篇-浅谈ActiveRecord自联结

avatar jane 23 Aug 2018

在网上看到个有趣的自联结例子:Person对象自联结 ,它考虑了Person对象的孩子和孙子关系,本文要考虑的是另一种情况:每个用户和销售商有一个邀请码,当邀请新用户进来报名时,新用户可填写该邀请码以标志邀请者。这里销售商与用户、用户与用户之间存在着邀请与被邀请的关系,其中第二种关系是下面会讲到的自联结。

图片加载失败……

1、构建模型

模型:User 用户
属性:invited_code 邀请码
   name 姓名
关联:belongs_to :inviter, polymorphic: true
   has_many :users, as: :inviter

模型:Seller 销售商
属性:invited_code 邀请码
   name 姓名
关联:has_many :users, as: :inviter

迁移代码如下:

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :invited_code
      t.belongs_to :inviter, polymorphic: true # 自动创建 inviter_id 和 inviter_type 字段
      t.timestamps
    end
  end
end

class CreateSellers < ActiveRecord::Migration[5.2]
  def change
    create_table :sellers do |t|
	  t.string :name
      t.string :invited_code
      t.timestamps
    end
  end
end

polymophic 是多态关联,在同一个关联中,一个模型可以属于多个模型,具体解析可参见Rails Guides的多态关联
执行 rails db:migrate命令后,创建了users表和sellers表。

2、Seller与User建立一对多关系

# app/models/sellers.rb
class Seller < ApplicationRecord
  has_many :users, as: :inviter, dependent: :nullify
end

# app/models/users.rb
class User < ApplicationRecord
  belongs_to :inviter, polymorphic: true, optional: true
end

这样就可以在rails console中执行以下语句:

# 本文会用到同一些例子,所以这里用变量保存结果以备后用
sensitive_seller = Seller.create(name: "感性的销售商", invited_code: "销售商_code")

# 一山与二爷在注册页面的邀请码一栏输入了“销售商_code”,那么后台会这样给他们创建用户
seller = Seller.find_by(invited_code: "销售商_code")
yi_shan = User.create(name: "一山", invited_code: "一山_code", inviter: seller)
er_ye = User.create(name: "二爷", invited_code: "二爷_code", inviter: seller)

# 接下来通过users方法获取自己邀请了哪些人,通过inviter方法知道自己的邀请者
sensitive_seller.users.pluck(:name) # => ["二爷", "一山"]
yi_shan.inviter.name # => "感性的销售商"

optional 选项的值为 true 时表示 User 中的外键 invited_id 不一定要填写 Seller 中已经存在了的 id ,它也可以留空或填写其他的内容。

在这个关联关系中,User 模型的 belongs_toSeller 模型的 has_many 没有用 foreign_keyprimary_key选项 ,所以默认将 Seller 中的 id 当做主键,将User中的 inviter_id 当做外键。如果想让其他字段作为主键或外键,必须用 foreign_keyprimary_key来进行设置。

3、User自联结

User 自联结跟 UserInvitation 的关联情况类似,只是这里变成了 User 与自己相关联。如下在 User model里面添加代码:

# app/models/users.rb
class User < ApplicationRecord
  has_many :users, as: :inviter, dependent: :nullify # 添加了这一行
  belongs_to :inviter, polymorphic: true, optional: true
end

这样,User 对象也可以调用 usersinviter 方法来查看自己 邀请了哪些人 以及 邀请自己的人是谁 了。
在rails console中:

mr_3 = User.create(name: "Mr.3", invited_code: "Mr.3_code", inviter: yi_shan)
yi_shan.users.pluck(:name) # => ["Mr.3"]
mr_3.inviter.name # => "一山"

4、Has_many :through

还有一个更复杂的例子:Has_many | Has_many — Self Join Table:每个 user 有多个追随者,同时自己也是多个 user 的追随者。本来代码在文章中都有给出,但为了方便加上自己的一些发现,这里也把代码贴出来。
首先创建一个连接表 follows

create_table :follows do |t|
  t.references :follower, foreign_key: { to_table: :users }
  t.references :followee, foreign_key: { to_table: :users }
  t.timestamps
end

在原文中是这样创建表的:

create_table :follows do |t|
  t.integer :follower_id, index: true
  t.integer :followee_id, index: true
end

这两种写法都能得到自己想要的结果,但第一种写法比较能反应表与表之间的联系,所以推荐第一种写法。
这里有个需要注意的地方,如果你采用 references 写法,foreign_key 别忘了添加 to_table 选项。以下这种写法会报错:

t.references :follower, foreign_key: true
t.references :followee, foreign_key: true

执行 rails db:migrate 时报了以下错误:

PG::UndefinedTable: ERROR: relation “followers” does not exist

接下来分别在 User model和 follow model中添加以下代码。

# app/models/users.rb
class User < ApplicationRecord
  ……
  # follower_follows "names" the Follow join table for accessing through the follower association
  has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" #标注1
  # source: :follower matches with the belong_to :follower identification in the Follow model 
  has_many :followers, through: :follower_follows, source: :follower #标注2

  # followee_follows "names" the Follow join table for accessing through the followee association
  has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow"
  # source: :followee matches with the belong_to :followee identification in the Follow model   
  has_many :followees, through: :followee_follows, source: :followee
end

#app/models/follows.rb
class Follow < ActiveRecord::Base
  belongs_to :follower, foreign_key: "follower_id", class_name: "User"
  belongs_to :followee, foreign_key: "followee_id", class_name: "User"
end

在控制台中执行下面语句:

yi_shan.followees << er_ye
yi_shan.followees << mr_3
yi_shan.followees.pluck(:name) # => ["二爷", "Mr.3"]
er_ye.followers.pluck(:name) # => ["一山"]

在原来的文章中,models/users.rb文件中由 标注1标注2 标志出来的两行代码顺序是倒过来的,也就是先 has_many :followers ,再 has_many :follower_follows,但按照这种顺序来写的话,执行 u2.follewers 时会出现以下错误:

ActiveRecord::HasManyThroughOrderError (Cannot have a has_many :through association ‘User#followers’ which goes through ‘User#follower_follows’ before the through association is defined.)

所以 has_many :follower_follows 一定得写在 has_many :followers 前面。

至此,自联结关系的讲解已告一段落,如果文章存在什么问题,请多指教~~

Tags
ruby
Active Record
self joins