Rails 6.1 API POST创建三级嵌套记录 内层Day关联Meal未生效问题
核心问题
Rails的has_and_belongs_to_many(简称HABTM)关联默认不支持通过accepts_nested_attributes_for创建新的关联记录,仅支持关联已存在的记录,这是你提交的meals_attributes完全不被处理的根本原因。
方案1:替换HABTM为has_many :through关联(推荐)
该方案符合Rails最佳实践,后续扩展关联字段也更灵活。
步骤1:创建中间模型及迁移
中间模型 PlanDay
# app/models/plan_day.rb class PlanDay < ApplicationRecord belongs_to :plan belongs_to :day end
对应迁移:
create_table :plan_days do |t| t.bigint :plan_id, null: false t.bigint :day_id, null: false end add_index :plan_days, [:plan_id, :day_id], unique: true
中间模型 DayMeal
# app/models/day_meal.rb class DayMeal < ApplicationRecord belongs_to :day belongs_to :meal end
对应迁移:
create_table :day_meals do |t| t.bigint :day_id, null: false t.bigint :meal_id, null: false end add_index :day_meals, [:day_id, :meal_id], unique: true
中间模型 PlanMeal
# app/models/plan_meal.rb class PlanMeal < ApplicationRecord belongs_to :plan belongs_to :meal end
对应迁移:
create_table :plan_meals do |t| t.bigint :plan_id, null: false t.bigint :meal_id, null: false end add_index :plan_meals, [:plan_id, :meal_id], unique: true
步骤2:修改模型关联
Plan.rb
class Plan < ApplicationRecord has_many :plan_days, dependent: :destroy has_many :days, through: :plan_days has_many :plan_meals, dependent: :destroy has_many :meals, through: :plan_meals has_one_attached :image, dependent: :destroy # 允许嵌套创建Day accepts_nested_attributes_for :days, reject_if: ->(obj) { obj[:number].blank? }, allow_destroy: true end
Day.rb
class Day < ApplicationRecord has_many :plan_days, dependent: :destroy has_many :plans, through: :plan_days has_many :day_meals, dependent: :destroy has_many :meals, through: :day_meals validates :number, presence: { message: "can't be blank" }, uniqueness: { message: "You can't use same day number twice" } # 允许嵌套创建Meal accepts_nested_attributes_for :meals, reject_if: ->(obj) { obj[:name].blank? }, allow_destroy: true end
Meal.rb
class Meal < ApplicationRecord has_many :day_meals, dependent: :destroy has_many :days, through: :day_meals has_many :plan_meals, dependent: :destroy has_many :plans, through: :plan_meals end
步骤3:修改控制器逻辑
补充Meal和Plan的关联逻辑,避免Meal仅关联Day、不关联Plan:
# POST /create_custon_plan def create_custon_plan ActiveRecord::Base.transaction do # 加事务保证数据一致性 @plan = Plan.new(plan_params) if @plan.save # 将Day下的所有Meal关联到当前Plan @plan.days.each do |day| @plan.meals << day.meals end render json: { messages: "Plan was successfully created.", is_success: true, status: :created, # 预加载关联避免N+1查询 data: { plan: @plan, days_attributes: @plan.days.includes(:meals), meals_attributes: @plan.meals }, } else render json: @plan.errors, status: :unprocessable_entity end end end
参数白名单不需要修改,原有配置即可正常工作。
方案2:不改动现有HABTM结构,手动处理嵌套参数
如果不想调整表结构,可以直接手动解析参数创建关联记录:
# POST /create_custon_plan def create_custon_plan ActiveRecord::Base.transaction do # 提取嵌套参数 plan_attrs = plan_params.to_h days_attrs = plan_attrs.delete(:days_attributes) || [] @plan = Plan.create!(plan_attrs) days_attrs.each do |day_attr| meal_attrs = day_attr.delete(:meals_attributes) || [] # 创建Day并关联Plan day = @plan.days.create!(day_attr) meal_attrs.each do |m_attr| # 创建Meal并关联Day和Plan meal = Meal.create!(m_attr) day.meals << meal @plan.meals << meal end end render json: { messages: "Plan was successfully created.", is_success: true, status: :created, data: { plan: @plan, days_attributes: @plan.days.includes(:meals), meals_attributes: @plan.meals }, } rescue ActiveRecord::RecordInvalid => e render json: e.record.errors, status: :unprocessable_entity end end
额外优化建议
- 若Day的编号仅需要在同一个Plan下唯一,可将Day的唯一性验证修改为
validates :number, uniqueness: { scope: :plan_id, message: "You can't use same day number twice in one plan" } - 接口路径
create_custon_plan存在拼写错误,建议修正为create_custom_plan
内容的提问来源于stack exchange,提问作者Mohamed Amine Azaiez
相关产品推荐
相关产品推荐

