Rails中同一个model被两次引用

呃,感觉标题有点不够精确,不过暂时没想到合适的词语去描述,先将就着。

场景:

现有一个model:group群组,一个model: user用户,每一个群组被一个user创建,每一个user可以创建多个group,很明显,两者之间是一对多关系。现在每个group可以添加一个管理员,管理员与group也是一对多的关系,而群组group的管理员和创建人都来自user这个model。

也就是说,user这个model需要两次被引用,group与user之间建立了两个一对多的关联。

解决方法

分两种情况:

  • 如果是新建一个项目,不涉及到数据迁移部分,可以参考Referencing the same model twice in Rails? 完成。
  • model已经建立,group与user之间建立了一对多的关系,同时group有一个外键是user_id, 这个外键的约束来自user。

针对后者说一下具体的解决方法。

分两步:

  • 给group添加两个字段creator_id,admin_id,作为reference,同时与user建立一对多的关联

  • 迁移数据,将user_id的数据迁移给到creator_id,同时删除user_id

好,一步步来。

目前的group表结构如下:

groups:
  string   "title"
  text     "description"
  datetime "created_at",  null: false
  datetime "updated_at",  null: false
  bigint  "user_id"
  index ["user_id"], name: "index_groups_on_user_id"

用户部分使用devise生成了user model。

第一步:给group添加字段,重新生成一对多关联

终端执行:

rails g migration add_creator_and_admin_to_group

在新生成的migration表中,添加如下内容:

class AddCreatorAndAdminToGroup < ActiveRecord::Migration[5.0]
  def change
    add_reference :groups, :creator
    add_reference :groups, :admin
    add_foreign_key :groups, :users, column: :creator_id, primary_key: :id
    add_foreign_key :groups, :users, column: :admin_id, primary_key: :id
  end
end

终端执行:

rake db:mgirate

这里为group添加了两个reference, creator_id, admin_id,同时通过add_foreign_key,将creator_id,admin_id指定成外键。

app/models/group.rb中,添加:

class Group < ApplicationRecord
  ......
  belongs_to :creator, class_name: 'User'
  belongs_to :admin, class_name: 'User', optional: true
end

这里对于admin添加了optional: true,这样创建group的时候,不必指定admin。

app/models/user.rb中,添加:

class User < ApplicationRecord
  ......
  has_many :created_groups, class_name: 'Group', foreign_key: 'creator_id'
  has_many :admined_groups, class_name: 'Group', foreign_key: 'admin_id'
end

此时rails c 进入rails控制台, 可以检查下刚刚创建的关系是否成功。

User.create(email: "[email protected]", password: "123456", password_confirmation: "123456")
=>  #<User id: 2, email: "[email protected]", created_at: "2018-06-09 05:43:58", updated_at: "2018-06-09 05:43:58">

User.create(email: "[email protected]", password: "123456", password_confirmation: "123456")

=> #<User id: 3, email: "[email protected]", created_at: "2018-06-09 05:44:17", updated_at: "2018-06-09 05:44:17">

a = Group.new(title: "rubyist", description: "for search")
=> #<Group:0x007ff3d0a04030
 id: nil,
 title: "rubyist",
 description: "for search",
 created_at: nil,
 updated_at: nil,
 user_id: nil,
 creator_id: nil,
 admin_id: nil>

 a.user = User.last
=> #<User id: 3, email: "[email protected]", created_at: "2018-06-09 05:44:17", updated_at: "2018-06-09 05:44:17">

 a.creator = User.last
=> #<User id: 3, email: "[email protected]", created_at: "2018-06-09 05:44:17", updated_at: "2018-06-09 05:44:17">

a.admin = User.find(2)
=> #<User id: 2, email: "[email protected]", created_at: "2018-06-09 05:43:58", updated_at: "2018-06-09 05:43:58">

a.save
=> true

User.last.created_groups
=> [#<Group:0x007ff3ce0a3ee8
  id: 5,
  title: "rubyist",
  description: "for search",
  created_at: Sat, 09 Jun 2018 05:47:49 UTC +00:00,
  updated_at: Sat, 09 Jun 2018 05:47:49 UTC +00:00,
  user_id: 3,
  creator_id: 3,
  admin_id: 2>]

User.second.admined_groups
=> [#<Group:0x007ff3cd61ff80
  id: 5,
  title: "rubyist",
  description: "for search",
  created_at: Sat, 09 Jun 2018 05:47:49 UTC +00:00,
  updated_at: Sat, 09 Jun 2018 05:47:49 UTC +00:00,
  user_id: 3,
  creator_id: 3,
  admin_id: 2>]

OK! 基本功能实现。下面我们将user_id的数据赋给creator_id,同时删除掉user_id这个外键。

第二步:数据迁移

没有迁移前,进rails c , 查看Group的第一个record:

Group.first
> #<Group:0x007ff3d093d9d0
 id: 3,
 title: "group1",
 description: "groups",
 created_at: Sat, 09 Jun 2018 05:46:29 UTC +00:00,
 updated_at: Sat, 09 Jun 2018 05:46:29 UTC +00:00,
 user_id: 1,
 creator_id: nil,
 admin_id: nil>

可以看到creator_id此时为nil, user_id 为1。我们需要把user_id给到creator_id。

终端执行:

rails g migration remove_user_id_on_group

在新生成的migration表中,添加如下内容:

class RemoveUserIdOnGroup < ActiveRecord::Migration[5.0]
  class Group < ActiveRecord::Base
  end
  def change
    Group.where.not(user_id: nil).find_each do |g|
      g.update(creator_id: g.user_id)
    end
    remove_foreign_key :groups, column: :user_id
  end
end

终端执行:

rake db:mgirate

这样数据就迁移过来了,同时group的外键user_id也被删除了。

rails console 看看:

Group.first
> #<Group:0x007ff3d093d9d0
 id: 3,
 title: "group1",
 description: "groups",
 created_at: Sat, 09 Jun 2018 05:46:29 UTC +00:00,
 updated_at: Sat, 09 Jun 2018 06:05:24 UTC +00:00,
 creator_id: 1,
 admin_id: nil>

可以看到数据已经成功迁移了。

但是,还没有完,需要清理下代码。

app/models/group.rb中,删除belongs_to :user

app/models/user.rb中,删除has_many :groups

app/controllers/groups_controller.rb 与group对应的views中,将user更换成creator。

OK! 完成 :)

参考

Ruby on rails - Reference the same model twice?

Referencing the same model twice in Rails?