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
CodePudding user response:
For example
order = Order.create(order_attribs)
order_items.each do |item_attribs|
order.order_items.create(item_attribs)
end
CodePudding user response:
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 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.- You're not actually checking if
order_detail.update(order_details_params)
is successful. Always code for invalid user input.
CodePudding user response:
Your code is just fine. There is just one thing that you need to correct. You need to define the order
model accepts_nested_attributes_for for the order_details
inside your Order model:
class Order < ApplicationRecord
belongs_to :customer
has_many :order_details
has_many :products, through: :order_details
accepts_nested_attributes_for :order_details
end
One this is done, you will be able to create/update order_details along with order with a single command- order.update(order_params)