ruby-on-railsnestedmany-to-manynested-attributesaccepts-nested-attributes

Is there a suggested way to create nested many-to-many entities in RoR that avoids *_attributes in the JSON field for the nested resource?


I've managed to get nested many-to-many entities created through the use of accepts_nested_attributes_for for the JSON and the project below.

My question is - is there a suggested way to achieve the same thing where I don't have to add _attributes to be able to create the array of emotion objects on the RoR server? What I mean by suggested is either something the framework itself suggests or otherwise something as clean as possible.

JSON getting posted to http://localhost:3000/periods:

{
    "period": {
        "date": "2022-03-17T03:00:52.820Z",
        "period": "morning",
        "emotions_attributes": [
            {
                "name": "ok"
            },
            {
                "name": "fine"
            }
        ]
    }
}

rails generate commands used to create subsequent files:

rails g model Emotion name:string
rails g scaffold Period date:date period:string --skip-template-engine
rails g scaffold Entry emotion:references period:references --skip-template-engine

periods.rb:

class Period < ApplicationRecord
  has_many :entries
  has_many :emotions, through: :entries

  accepts_nested_attributes_for :emotions
end

emotion.rb

class Emotion < ApplicationRecord
  has_many :entries
  has_many :periods, through: :entries
end

entry.rb

class Entry < ApplicationRecord
  belongs_to :emotion
  belongs_to :period
end

periods_controller.rb

class PeriodsController < ApplicationController
  before_action :set_period, only: %i[ show edit update destroy ]
  skip_before_action :verify_authenticity_token

  ...

  def create
    @period = Period.new(period_params)

    respond_to do |format|
      if @period.save
        format.html { redirect_to period_url(@period), notice: "Period was successfully created." }
        format.json { render :show, status: :created, location: @period }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @period.errors, status: :unprocessable_entity }
      end
    end
  end
  
  ...

    # FYI turns out the id is key here to be able to even perform create 
    def period_params
      params.require(:period).permit(
        :date, 
        :period,
        emotions_attributes: [:id, :name]
      )
    end

routes.rb

Rails.application.routes.draw do
  resources :entries
  resources :periods

What I'd like to be able to post is this:

{
    "period": {
        "date": "2022-03-17T03:00:52.820Z",
        "period": "morning",
        "emotions": [
            {
                "name": "ok"
            },
            {
                "name": "fine"
            }
        ]
    }
}

Solution

  • you won't find the cleaner way to achieve that cause "emotions_attributes" are handled "out of the box" by the framework.

    alternatively you can create an service object for more custom period creation (fe app/services/periods/create.rb) and call it from controller. This way you can customize default convention and keep the logic in one place.

    But imho you should stay with the default convention as long as you are just starting your journey with rails and the main concern is that "emotions looks better than emotions_attributes".