Вложенные атрибуты Rails правильно назначают parent_id в качестве индекса, но не назначают дополнительные атрибуты

Я работаю над реализацией новой модели, которая принадлежит одной из существующих моделей наших веб-приложений. В конце концов, я хочу создать вложенную форму. Я понимаю, что форма и сильные параметры могут иметь свой собственный набор проблем, но в настоящее время я изо всех сил пытаюсь заставить модели вести себя так, как я ожидаю, в консоли рельсов.

Рельсы 4.2.7, БД Postgres

ОБНОВЛЕНИЕ — 03.10.16 — Все еще пытаюсь найти правильное решение, но внес некоторые изменения.

Мы работаем со школами и округами, и в данном конкретном случае речь идет об опросах и о том, как опрос назначается школе и округу. До сих пор опрос назначался округу с помощью модели SurveyAssignment, и некоторые последующие логические схемы предполагали, что все школы округа также были «приписаны» к опросу. Теперь мы хотим иметь возможность добавить больше детализации в SurveyAssignment и разрешить некоторую специфику на уровне школы.

Поэтому я создал модель SchoolSurveyAssignment и начал расставлять все по местам.

Вот соответствующая информация о модели:

class District < ActiveRecord::Base
  ...
  has_many :schools, dependent: :destroy
  has_many :survey_assignments, dependent: :destroy
  ...
end

class School
  ...
  belongs_to :district
  has_many :school_survey_assignments
  has_many :survey_assignments, :through => :school_survey_assignments

  ...
end

class SurveyAssignment
  belongs_to :district
  belongs_to :survey
  has_one :survey_version, through: :survey
  has_many :school_survey_assignments, inverse_of: survey_assignment
  has_many :schools, :through => :school_survey_assignments

  accepts_nested_attributes_for :school_survey_assignments

  attr_accessor :survey_group, :survey_version_type, :survey_version_id, :school_survey_assignments_attributes
  validates :survey_id, presence: true
end 

class SchoolSurveyAssignment
  belongs_to :survey_assignment, inverse_of: :school_survey_assignments
  belongs_to :school

  attr_accessor :school_id, :survey_assignment_id, :grades_affected, :ulc_affected
  validates_presence_of :survey_assignment
  validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end

Соответствующий код контроллера:

class SurveyAssignmentsController < ApplicationController
  before_action :set_district
  before_action :set_survey_assignment, only: [:show, :edit, :update, :destroy]

  respond_to :html, :json, :js

  def new
    @new_survey_assignment = SurveyAssignment.new()
    @district.schools.each do |school|
      @new_survey_assignment.school_survey_assignments.build(school_id: school.id)
    end
  end

  def create
    @survey_assignment = SurveyAssignment.new(survey_assignment_params)
    if @survey_assignment.save
      flash[:notice] = "Survey successfully assigned to #{@district.name}"
    else
      flash[:alert] = "There was a problem assigning this survey to #{@district.name}"
    end
    redirect_to district_survey_assignments_path(@district)
  end

  def survey_assignment_params
    params.require(:survey_assignment).permit(:survey_id, :status, :survey_version_id, school_survey_assignments_attributes: [:id, :survey_assignment_id, :school_id, grades_affected: [], ulc_affected: []]).tap do |p|
      p[:district_id] = @district.id
      p[:school_year] = session[:selected_year]
    end
  end

  def set_district
    @district = District.find(params[:district_id])
  end

Вот соответствующая информация о схеме:

create_table "school_survey_assignments", force: :cascade do |t|
  t.integer "survey_assignment_id"
  t.integer "school_id"
  t.integer "grades_affected",      default: [], array: true
  t.string  "ulc_affected",         default: [], array: true
 end

add_index "school_survey_assignments", ["school_id"], name: "index_school_survey_assignments_on_school_id", using: :btree
add_index "school_survey_assignments", ["survey_assignment_id"], name: "index_school_survey_assignments_on_survey_assignment_id", using: :btree

create_table "survey_assignments", force: :cascade do |t|
  t.integer  "district_id"
  t.integer  "survey_id"
  t.integer  "status"
  t.datetime "created_at"
  t.datetime "updated_at"
  t.integer  "school_year"
  t.integer  "last_response_status_id"
end

add_index "survey_assignments", ["district_id"], name: "index_survey_assignments_on_district_id", using: :btree

Как только они были на месте, я вошел в свою консоль rails и попытался сделать следующее:

2.3.1 :002 > sa1 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
  (0.2ms)  BEGIN
 SQL (0.7ms)  INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:20.205144"], ["updated_at", "2016-09-30 21:30:20.205144"]]
  (7.2ms)  COMMIT
=> #<SurveyAssignment id: 369, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:20", updated_at: "2016-09-30 21:30:20", school_year: 2017, last_response_status_id: nil>
2.3.1 :003 > sa2 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
  (0.3ms)  BEGIN
 SQL (0.4ms)  INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:30.701197"], ["updated_at", "2016-09-30 21:30:30.701197"]]
  (0.5ms)  COMMIT
=> #<SurveyAssignment id: 370, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:30", updated_at: "2016-09-30 21:30:30", school_year: 2017, last_response_status_id: nil>

Итак, я успешно создал два опросных задания. Теперь я собираюсь создать два школьных опросных задания на основе sa1:

2.3.1 :004 > [{school_id: 5}, {school_id: 6}].each do |ssa|
2.3.1 :005 >     sa1.school_survey_assignments.create(ssa)
2.3.1 :006?>   end
  (0.2ms)  BEGIN
 SchoolSurveyAssignment Exists (2.4ms)  SELECT  1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 5 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
 SQL (0.4ms)  INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id"  [["survey_assignment_id", 369]]
  (6.4ms)  COMMIT
  (0.6ms)  BEGIN
 SchoolSurveyAssignment Exists (0.4ms)  SELECT  1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 6 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
  SQL (0.3ms)  INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id"  [["survey_assignment_id", 369]]
  (0.4ms)  COMMIT
=> [{:school_id=>5}, {:school_id=>6}]
2.3.1 :007 > sa1.save
  (0.3ms)  BEGIN
  (0.4ms)  COMMIT
=> true

Теперь похоже, что я успешно создал два SchoolSurveyAssignments с Survey_assignment_id = 369 и school_ids = 5 и 6.

2.3.1 :008 > sa1.school_survey_assignments
  SchoolSurveyAssignment Load (0.3ms)  SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."survey_assignment_id" = $1  [["survey_assignment_id", 369]]
=> #<ActiveRecord::Associations::CollectionProxy [#<SchoolSurveyAssignment id: 5, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>, #<SchoolSurveyAssignment id: 6, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>]>

Как видно из ActivRecord::Associations::CollectionProxy, были созданы оба объекта SchoolSurveyAssignment с Survey_assignment_id: 369, но с нулевым school_id. Это беспокоит, как кажется

  1. Игнорирование параметров, передаваемых в функцию создания, и
  2. игнорирование проверки school_id

Другой пункт, который я не понимаю, заключается в следующем:

2.3.1 :009 > SchoolSurveyAssignment.find(5).survey_assignment_id
  SchoolSurveyAssignment Load (0.6ms)  SELECT  "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1  [["id", 5]]
=> nil
2.3.1 :011 > SchoolSurveyAssignment.find(5).survey_assignment.id
  SchoolSurveyAssignment Load (0.3ms)  SELECT  "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1  [["id", 5]]
  SurveyAssignment Load (0.4ms)  SELECT  "survey_assignments".* FROM "survey_assignments" WHERE "survey_assignments"."id" = $1 LIMIT 1  [["id", 369]]
 => 369

Вызов .survey_assignment_id должен вернуть атрибут SchoolSurveyAssignment и дать 369. .survey_assignment.id просто получает идентификатор родительского объекта. Я бы ожидал, что оба вернут одно и то же значение, но один возвращает ноль.

Конечным вариантом использования является создание формы SurveyAssignment, которая позволяет пользователю задавать атрибуты для нового SurveyAssignment, а также задавать атрибуты для количества X SchoolSurveyAssignments (в зависимости от количества школ в округе; варьируется от 2 до 15). Как только я лучше понимаю, как взаимодействуют эти модели, я чувствую уверенность в достижении этой цели, но поведение, которое я вижу, не имеет для меня смысла, и я надеялся найти некоторую ясность в реализации этих связанных моделей. Я чувствую, что прыгаю вокруг ответа, но упускаю ключевую деталь.

Спасибо,

Алекс


person Alex Lee    schedule 30.09.2016    source источник
comment
можешь выложить код контроллера? Я предполагаю, что вы используете сильные параметры?   -  person Ren    schedule 01.10.2016
comment
Я использую сильные параметры. Я обновлю код контроллера. Это может разоблачить мое невежество, но у меня сложилось впечатление, что методы контроллера попадают только в истинные HTTP-запросы/маршрутизацию, и что запуск команд из консоли не затрагивает эти методы.   -  person Alex Lee    schedule 03.10.2016


Ответы (2)


Попробуйте удалить attr_accessor строк кода. attr_accessor не следует использовать для атрибутов, которые сохраняются в базе данных, и это, вероятно, искажает методы, которые ActiveRecord уже предоставляет по умолчанию, в результате чего эти атрибуты не сохраняются должным образом.

class SurveyAssignment
  belongs_to :district
  belongs_to :survey
  has_one :survey_version, through: :survey
  has_many :school_survey_assignments, inverse_of: survey_assignment
  has_many :schools, :through => :school_survey_assignments

  accepts_nested_attributes_for :school_survey_assignments

  validates :survey_id, presence: true
end 

class SchoolSurveyAssignment
  belongs_to :survey_assignment, inverse_of: :school_survey_assignments
  belongs_to :school

  validates_presence_of :survey_assignment
  validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
person Ren    schedule 04.10.2016
comment
Привет, спасибо за помощь! Я попытался создать в соответствии с вашим предложением, и он создает SurveyAssignment, но не school_survey_assignments. Я также попробовал то, что говорят документы api.rubyonrails.org для примера «один ко многим» api.rubyonrails.org/v4.2/classes/ActiveRecord/NestedAttributes/ Это также привело к созданию Назначение опроса, но не создание SchoolSurveyAssignments. - person Alex Lee; 04.10.2016
comment
Основываясь на совете коллеги, я перешел к использованию пользовательского класса объектов формы для обработки создания задания на опрос и заданий школьного опроса. Он правильно обрабатывает параметры, и я знаю, что получаю все необходимые параметры через SurveyAssignmentController и в этот объект. Однако атрибуты дочерней модели по-прежнему не сохраняются (school_survey_assignment). Похоже, у меня есть какая-то другая проблема, а не проблема с вложенными формами/объектами форм. - person Alex Lee; 04.10.2016
comment
внес правку в исходный пост. попробуйте закомментировать/удалить строки attr_accessor - person Ren; 04.10.2016
comment
Да, это было так. Я удалил строки attr_accessor в своей модели SchoolSurveyAssignment, и теперь мой объект формы работает отлично. Я полагаю, что мог бы вернуться и заставить работать подход nested_attributes, но этот объект формы кажется достаточно элегантным. - person Alex Lee; 05.10.2016

Для первого вопроса School и SurveyAssignment не знают друг друга, school_id становится равным нулю. В вашем приложении эти модели имеют косвенную ассоциацию m-to-n, так как насчет использования has_many через ассоциацию между моделями?

В школьной модели добавьте ниже:

has_many :survey_assignments, :through => :school_survey_assignments

В модели SurveyAssignments добавьте ниже:

has_many :schools, :through => :school_survey_assignments

Что касается последнего вопроса, оба кода кажутся одинаковыми.

person YTorii    schedule 01.10.2016
comment
спасибо за указание на необходимую ассоциацию. Я добавил это, и я все еще сталкиваюсь с некоторыми проблемами. Я обновил код, чтобы отразить изменения. Что касается последнего вопроса, разница заключается в .id и _id - person Alex Lee; 03.10.2016
comment
Извините, я упустил это из виду, но на моем ПК работают и .id, и _id. Ваша проблема все еще .id против _id? Кстати, в классе SurveyAssignment inverse_of: Survey_assignment отсутствует ':' перед 'survey_assignment', я думаю. - person YTorii; 04.10.2016
comment
Спасибо за продолжение. Это отсутствующее : было опечаткой при переходе от моего редактора к Stackoverflow, но хороший улов. - person Alex Lee; 04.10.2016