#ruby-on-rails #ruby #database #activerecord #reference
#ruby-on-rails #ruby #База данных #activerecord #ссылка
Вопрос:
Немного сложно думать об этом абстрактно, поэтому вот пример:
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 равен нулю для первого комментария и предыдущего для двух других).
Что я получаю: второй комментарий сохраняется в базе данных, но без родительского идентификатора. Третий комментарий сохранен правильно. Первый полностью потерян.
Я даже не вижу, как получить то, что мне нужно, даже если я вручную проверю цепочку ссылок и обязательно сохраню все в правильном порядке; этот пример должен был сработать, потому что комментарий без родителей сохраняется первым. Я также не хочу углубляться в это: это будет действительно сложно, если базовая структура будет более сложной, с несколькими цепочками комментариев и несколькими дочерними элементами для каждого комментария. Кроме того, разве подобные вещи не должны абстрагироваться от Ruby и ActiveRecord?
Итак, что я делаю не так, и как мне правильно сохранить данные, когда создается несколько новых объектов ActiveRecord одного и того же класса, и некоторые из них ссылаются друг на друга?
Версия Rails: 5.1.7
ОС: macOS 10.15.7 (Catalina)
DB: PostgreSQL 12.4
Комментарии:
1. Вы изучали api.rubyonrails.org/classes/ActiveRecord/NestedAttributes /… ? или railscasts.com/episodes/196-nested-model-form-revised
Ответ №1:
То, что вы описываете, называется ассоциацией с самоссылкой или самосоединением.
Чтобы настроить самоссылочную ассоциацию, вы просто создаете столбец внешнего ключа с возможностью обнуления, который указывает на ту же таблицу:
class CreateObservations < ActiveRecord::Migration[6.0]
def change
add_reference :comments, :parent,
null: true,
foreign_key: { to_table: :comments }
end
end
И ассоциацию, которая указывает на тот же класс:
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
ассоциацию необязательной, вы окажетесь в сценарии курица против яйца, где вы фактически не можете вставить первую запись в таблицу, поскольку ей не на что ссылаться.
Если вы хотите создать запись и дочерние элементы одновременно, вы используете 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
}
]
}
]
])
Поскольку он автоматически обрабатывает рекурсию. Смотрите руководства о том, как создавать формы для вложенных атрибутов и как вносить их в белый список в вашем контроллере. Если вложенные атрибуты содержат идентификатор, вложенные записи будут обновляться вместо создания новых записей.
Решение всей проблемы post_id может быть выполнено по-разному:
- Используйте обратный вызов, чтобы установить сообщение из родительского комментария. Это мой наименее любимый.
- Сделайте столбец обнуляемым и получите родительский элемент, пройдя вверх по дереву.
- Вместо этого используйте полиморфную ассоциацию, чтобы комментарий мог принадлежать либо комментарию, либо сообщению. Получите исходное сообщение, пройдя вверх по дереву.
Комментарии:
1. Хотя, честно говоря, я не совсем понимаю, зачем вам нужно обновлять / создавать кучу комментариев в одном запросе. Это действительно можно просто сделать с помощью нескольких отдельных вложенных маршрутов —
POST /posts/1/comments
(для создания одного комментария) иPOST /comments/1/comments
для создания вложенного комментария. Комментарии могут быть отредактированы / уничтоженыPATCH|DELETE /comments/1
.2. Это значительно снижает сложность и обрабатывает любой уровень рекурсии. Используйте AJAX, если вы хотите, чтобы он выглядел бесшовным, а не чудовищно вложенной формой.
3. На самом деле это не то, о чем мое приложение, я просто использую комментарии в качестве иллюстрации. Основная идея заключается в том, что у меня есть форма, которая отправляет дерево элементов, которые необходимо сохранить в базе данных со ссылками. Мне действительно не нужен
children
аксессор, хотя я понимаю, что, возможно, захочу добавить его позже. Но ключ, по-видимомуoptional: true
, является флагом; как только он был добавлен, состояние БД было тем, что мне нужно. Наверное, я предположил, что значение optional будет по умолчанию, а required должно быть указано, а не наоборот.
Ответ №2:
Проблема, похоже, в том, что вы сохраняете комментарии только после этого. Обычно Rails генерирует идентификатор только после сохранения записи, поэтому, когда вы назначили parent_id перед сохранением, он все равно был равен нулю. Вы могли бы добавить все это в транзакцию, просто для уверенности. Что-то вроде этого:
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
Комментарии:
1. Нет, нет, нет. Rails автоматически сохранит ассоциацию по умолчанию. Это означает, что если у вас есть модель со связанными экземплярами, которые не сохранены, rails сначала сохранит родительский, а затем дочерний и предоставит им идентификатор. Хотя ваши намерения, возможно, были хорошими, вы переосмыслили колесо и сделали его хуже.