在 Rails 上的 Ruby 中,我如何保存许多 ActiveRecord objects,它们引用了相同 class 的其他实例?

In Ruby on Rails, how do I save many ActiveRecord objects that have references to other instances of the same class?

抽象地想这个有点棘手,所以这里有一个例子:

schema.rb

create_table "comments", force: :cascade do |t|
  t.string "text"
  t.bigint "post_id"
  t.bigint "author_id"
  t.bigint "parent_id"
  t.index ["parent_id"], name: "index_comments_on_parent_id"
end

comment.rb

class Comment < ApplicationRecord
  belongs_to :parent, class_name: 'Comment'
  belongs_to :author, class_name: 'User'
  belongs_to :post
end

comment_controller.rb

def update
  # params contains data for a chain of comments, each the child of the preceding one
  comment_data = params["_json"]
  comments = []
  comment_data.each do |cd|
    com = Comment.new(text: cd["text"], post_id: cd["post_id"], author_id: cd["user_id"])
    com.parent = comments.last
    comments.push(com)
  end

  comments.each { |com| com.save }

end

然后我尝试通过 HTTP post 请求发送内容:

> POST /comments HTTP/1.1
> Host: mywebsitebackend.net
> Content-Type: application/json

| [
|   {
|      "post_id" : 1,
|      "user_id" : 99,
|      "text" : "You suck!!!!!"
|   },
|   {
|      "post_id" : 1,
|      "user_id" : 1,
|      "text" : "Be respectful, or I'll block you."
|   },
|   {
|      "post_id" : 1,
|      "user_id" : 99,
|      "text" : "Typical *******, doesn't belive in free speech."
|   }
| ]

我期望的是:新评论将保存到基础数据库中,并正确引用结构(即,parent 第一条评论为 nil,另外两条评论为前一条评论)。

我得到的结果:第二条评论已保存到数据库中,但没有 parent id。第三条评论保存正确。第一个完全丢失了。

即使我手动检查引用链并确保以正确的顺序保存所有内容,我什至不知道如何获得我需要的东西;此示例应该有效,因为 no-parent 注释首先被保存。我也不想深入研究:如果底层结构更复杂,有多个评论链和每个评论多个 children,它会变得非常复杂。另外,Ruby和ActiveRecord不就是应该抽象出来的吗?

那么我做错了什么,当创建多个相同 class 的新 ActiveRecord objects 并且其中一些相互引用时,如何正确保存数据?

Rails版本:5.1.7

OS: macOS 10.15.7 (卡特琳娜)

数据库:PostgreSQL 12.4

问题似乎是你只是在事后保存评论。通常 Rails 只在记录保存后生成 id,所以当你在保存前分配 parent_id 时它仍然是 nil。您可以将所有这些添加到事务中,只是为了确定。像这样:

def update
  # params contains data for a chain of comments, each the child of the preceding one
  comment_data = params["_json"]
  comments = []
  Comment.transaction do
    comment_data.each do |cd|
      com = Comment.new(text: cd["text"], post_id: cd["post_id"], author_id: cd["user_id"])
      com.parent = comments.last if comments.present?
      com.save!
      comments.push(com)
    end
  end
end

您所描述的称为 self-referential 关联或 a self join

要设置自引用关联,您只需创建指向相同 table:

的可空外键列
class CreateObservations < ActiveRecord::Migration[6.0]
  def change
    add_reference :comments, :parent, 
      null: true,
      foreign_key: { to_table: :comments }
  end
end

以及指向同一个 class 的关联:

class Comment < ApplicationRecord
  belongs_to :parent, 
             class_name: 'Comment',
             optional: true,
             inverse_of: :children
             
  has_many :children,
             class_name: 'Comment',
             foreign_key: 'parent_id',
             inverse_of: :parent
end

如果您不使列可为空并且 belongs_to 关联可选,您最终将陷入先有鸡还是先有蛋的情况,您实际上无法在 table 中插入第一条记录它没有任何参考意义。

如果要同时创建记录和 children,请使用 accepts_nested_attributes

class Comment < ApplicationRecord
  belongs_to :parent, 
             class_name: 'Comment',
             optional: true,
             inverse_of: :children
             
  has_many :children,
             class_name: 'Comment',
             foreign_key: 'parent_id',
             inverse_of: :parent

  accepts_nested_attributes_for :children
end

这让您可以创建一个 reddit 风格的评论线程:

Comment.create!(
  text: "You suck!!!!!", 
  user_id: 99, 
  children_attributes: [
    { 
      text: "Be respectful, or I'll block you.", 
      user_id: 1,
      children_attributes: [
        {
          text: "Typical *******, doesn't belive in free speech.",
          user_id: 99
        }
      ]
    }
  ]
])

因为它自动处理递归。请参阅有关如何在控制器中 create forms for nested attributes and how to whitelist them 的指南。如果嵌套属性包含 id,则嵌套记录将被更新而不是创建新记录。

处理整个 post_id 问题可以通过不同的方式来完成:

  • 使用回调从 parent 评论设置 post。这是我最不喜欢的。
  • 使该列可为空并通过向上遍历树获得 parent。
  • 改为使用多态关联,以便评论可以属于评论或 post。向上遍历树得到原post