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
For example
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:
This will create a
order_details_attributes=
setter which takes an array of hashes as input. And which will initialize/create the nested records.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 withPATCH /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.
find
and notfind_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 returnjson: { 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.order_detail.update(order_details_params)
is successful. Always code for invalid user input.