В Ruby on Rails, как мне сохранить много объектов ActiveRecord, которые имеют ссылки на другие экземпляры того же класса?

#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 сначала сохранит родительский, а затем дочерний и предоставит им идентификатор. Хотя ваши намерения, возможно, были хорошими, вы переосмыслили колесо и сделали его хуже.