У меня странный рабочий процесс, при котором сначала создается дочерняя запись, а затем я хочу, чтобы она была связана с родителем через вложенную форму. Вот пример использования rspec:
Вот модели:
class Parent < ApplicationRecord
has_many :children, class_name: 'Child', dependent: :destroy
accepts_nested_attributes_for :children, allow_destroy: true, reject_if: :all_blank
end
class Child < ApplicationRecord
belongs_to :parent, foreign_key: 'child_id', optional: true
end
Мой контроллер стандартный:
def create
@prent = Parent.new(parent_params)
respond_to do |format|
if @parent.save
format.html { redirect_to @parent }
else
p @recipe.errors.full_messages
end
end
end
def parent_params
params.require(:parent).permit(
:name,
children_attributes: [
:id, :_destroy
]
)
end
Поскольку дочерний элемент уже существует, я пытаюсь связать его только с родителем, а не создавать совершенно новую запись. Вот мой тест, который не работает с ошибкой:
child = Child.create
expect {
post "/parent", params: {
parent: {
name: "test",
children_attributes: [{
id: child.id
}]
}
}.to change(Parent.all, :size).by(+1)
.and change(Child.all, :size).by(0)
end
И ошибка, которую я получаю,
ActiveRecord::RecordNotFound:
Couldn't find Child with ID=1 for Parent with ID=
Очевидно, это потому, что Child
существует до Parent
, но я все еще чувствую, что это должно быть возможно. Есть ли способ заставить это работать так, чтобы существующий дочерний элемент был связан с родителем при сохранении родителя?
@JanVítek Я бы ожидал обновить его, если ребенок уже прикреплен
@JanVítek проблема в том, что иногда дочерний элемент не существует, и его необходимо создать при отправке родителя. Такое ощущение, что я пытаюсь сделать слишком много вещей
Под «обновить, если ребенок уже подключен» вы имеете в виду украсть его у другого родителя? Я не думаю, что для этого есть универсально правильное поведение, и поэтому единственный способ - это ручное вмешательство. Часть поведения вложенных атрибутов может быть сохранена, но, по крайней мере, для связывания существующих записей вам придется отклонить их из parent_params и связать их вручную.
Ааа, теперь я понимаю. Нет, дочерний элемент на самом деле является таблицей соединения с другими дочерними элементами, и родитель никогда не будет заменен.
accepts_nested_attributes_for
не имеет функции, которая вам нужна, вы можете только создавать новые дочерние элементы или обновлять атрибуты уже связанных дочерних элементов, вы не можете добавлять существующие дочерние элементы.
Вы можете переопределить метод children_attributes=
, чтобы он обеспечивал искомую функциональность, если бы id
существовал, но еще не был частью parent.children
(а затем передать super
для нормального поведения).
Если вам нужно только связать существующие записи при создании другой, используйте метод child_ids=
.
collection_singular_ids=ids
Замените коллекцию объектами, идентифицированными первичными ключами вids
. Этот метод загружает модели и вызываетcollection=
.
https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
# db/migrate/20230601025957_create_parents.rb
class CreateParents < ActiveRecord::Migration[7.0]
def change
create_table :parents
create_table :children do |t|
t.references :parent
end
end
end
# app/models/parent.rb
class Parent < ApplicationRecord
has_many :children
end
# app/models/child.rb
class Child < ApplicationRecord
belongs_to :parent, optional: true
end
# spec/requests/parent_spec.rb
require "rails_helper"
RSpec.describe "Parents", type: :request do
it "POST /parents" do
child = Child.create!
expect do
post "/parents", params: {parent: {child_ids: [child.id]}}
end.to(
change(Parent, :count).by(1).and(
change(Child, :count).by(0)
)
)
end
end
# 1 example, 0 failures
В этой настройке таблица Child
не является таблицей соединений, но если у вас настроены сквозные ассоциации, метод *_ids
также будет работать. Что-то вроде этого: https://stackoverflow.com/a/75972917/207090
Это сделало это! У меня было ощущение, что это возможно. Спасибо!
Каково ожидаемое поведение, если ребенок уже привязан? Я бы либо сделал это вручную (без использования вложенных атрибутов), либо создал ассоциацию «многие ко многим» и манипулировал записями в таблице соединений, используя вложенные атрибуты.