skip to Main Content

I have an Order Model which contains an array of objects called item_details.

When I create a new order, I want to iterate over item_details and create new instances of OrderDetail which include the Order id.

OrderDetail is a join table so I want to create instances after creating an Order so that I can include the order_id in OrderDetails.

How do I go about doing this? I have the data type for item_details as json, this way I managed to save it to my database.

Before I had it as text/string and it was saving a symbol as a string.

Order Sample

{
    "id": 5,
    "customer_id": 1,
    "order_date": "2023-01-03",
    "total_cost": 0,
    "item_details": [
        {
            "product_id": 3,
            "quantity": 3
        },
        {
            "product_id": 9,
            "quantity": 4
        }
    ],

Schema

  create_table "order_details", force: :cascade do |t|
    t.integer "product_id"
    t.integer "order_id"
    t.integer "quantity"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "orders", force: :cascade do |t|
    t.integer "customer_id"
    t.string "order_date"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.json "item_details"
  end

Models

class Order < ApplicationRecord
    belongs_to :customer
    has_many :order_details
    has_many :products, through: :order_details

end

class OrderDetail < ApplicationRecord

    validates :quantity, numericality: { only_integer: true }

    belongs_to :order
    belongs_to :product  

end

Serializers

class OrderSerializer < ActiveModel::Serializer
  attributes :id, :customer_id, :order_date, :total_cost, :item_details

  belongs_to :customer
  has_many :order_details
  has_many :products


  def total_cost
    cost = []
    self.object.order_details.each do |details|
      product = self.object.products.find {|product| product.id == details.product_id}
      cost << product.price * details.quantity
    end
    return cost.sum
  end

 class OrderDetailSerializer < ActiveModel::Serializer
  attributes :id, :product_id, :order_id, :quantity, :product

  belongs_to :order
  belongs_to :product
end

Order Controller

class OrdersController < ApplicationController
    wrap_parameters format: []
    skip_before_action :authorized, only: :create

    def index
        orders = Order.all
        if orders
        render json: orders
        else
            render json: {error: "Order Not Found" }, status: :not_found
        end
    end

    def show
        order = Order.find_by(id: params[:id])
        if order
            render json: order
        else
            render json: { error: "Order Not Found" }, status: :not_found
        end
    end

    def create
        order = Order.create(order_params)
        if order.valid?
            order.item_details.each do |i|
                OrderDetail.create(order_id: params[:id], product_id: i[:product_id], quantity: i[:quantity])
            end
            render json: order
        else
            render json: { errors: order.errors.full_messages }, status: :unprocessable_entity
        end
    end

    def update
        order = Order.find_by(id: params[:id])
        if order
            order.update(order_params)
            render json: order
        else
            render json: { error: "Order Not Found" }, status: :not_found
        end
    end

    def destroy
        order = Order.find_by(id: params[:id])
        if order
            order.destroy
            head :no_content
        else
            render json: {error: "Order Not Found"}, status: :not_found
        end
    end


    private

    def order_params
        params.permit(:customer_id, :order_date, item_details: [:product_id, :quantity] )
    end

end

OrderDetail Controller

class OrderDetailsController < ApplicationController

    skip_before_action :authorized, only: :create

    def index
        order_details = OrderDetail.all
        if order_details
        render json: order_details
        else
            render json: {error: "Not Found"}, status: :not_found
        end
    end

    def create
        order_detail = OrderDetail.create(order_details_params)
        if order_detail.valid?
            render json: order_detail
        else
            render json: { errors: order_detail.errors.full_messages }, status: :unprocessable_entity
        end
    end

    def update
        order_detail = OrderDetail.find_by(id: params[:id])
        if order_detail
            order_detail.update(order_details_params)
            render json: order_detail
        else
            render json: { error: "Not Found" }, status: :not_found
        end
    end

    private

    def order_details_params
        params.permit(:order_id, :product_id, :quantity)
    end

end

2

Answers


  1. For example

    order = Order.create(order_attribs)
    order_items.each do |item_attribs|
      order.order_items.create(item_attribs)
    end
    
    Login or Signup to reply.
  2. The way that you typically creating multiple records in a single request in Rails is to have the parent record accept nested attributes for its children:

    class Order
      has_many :order_details
      accepts_nested_attributes_for :order_details
      validates_associated :order_details
    end
    

    This will create a order_details_attributes= setter which takes an array of hashes as input. And which will initialize/create the nested records.

    class OrdersController
      # POST /orders
      def create
        @order = Order.new(order_params)
        if @order.save
          # either just respond with a location
          head :created, location: @order
          # or the entity as json
          render json: @order
        else
          render json: { errors: order_detail.errors.full_messages }, 
            status: :unprocessable_entity
        end
      end
    
      private 
    
      def order_params
        params.require(:order)
              .permit(:foo, :bar, order_details_attributes: [:product_id, :quantity]) 
      end
    end
    

    However this is really just somewhat of a kludge to manage multiple resources in one single syncronous request and doesn’t always result in the best user experience or good code.

    If your users are adding an item to a shopping cart it would be better to save the order right when they add the first item and then have the client send atomical POST /orders/:order_id/order_details requests to add items to the cart – updating the quantity of a single item would be done with PATCH /orders/:order_id/order_details/:id. See nested routes.

    There are also a lot of issues with the controller in the question and you would be better off if you just started over from a scaffold.

    • Use find and not find_by(id: ...). It will raise a NotFoundException if the record is not found and respond with a 404 status response. It will break out of the method without adding a bunch of cyclic complexity and duplication. You don’t need to return json: { error: "Not Found" }. That is just a silly anti-pattern.
    • if order_detail.valid? doesn’t actually guarentee that the record is persisted to the database. It just says that the validations passed. Check the return value of .save or .persisted? instead.
    • You’re not actually checking if order_detail.update(order_details_params) is successful. Always code for invalid user input.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search