在 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
抽象地想这个有点棘手,所以这里有一个例子:
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