ユーザーからの入力formはないけどDBに保存したい場合
こういう場面ありますよね。
例えば今回説明で使う例としてorder
が保存されるとorder_detail
保存されるようにします。
流れで言うと、formで注文
が確定したらそのまま一緒に注文詳細
が確定される感じです。
ただ、そのまま保存するのではなく注文詳細
は買った当時の情報を保存するようにしています。
そうする事で、商品名が改名や値段もしくは商品自体削除されてしまった場合でも買った当時の注文データが保存すると言うわけです
ER図
まずはモデルから
class Order < ApplicationRecord
belongs_to :user
has_many :order_details, dependent: :destroy
has_many :products, through: :order_detail, dependent: :destroy
validates :postal_code, presence: true
validates :address, presence: true
validates :apartment_name, presence: true
scope :latest, ->(limit = 10) { order(created_at: :desc).limit(limit) }
def total_billing_price
order_details.inject(0) do |sum, order_detail|
sum + order_detail.subtotal
end
end
end
次にコントローラー
class OrdersController < ApplicationController
def create
@total_price = current_user.total_cart_price
@cart_products = current_user.cart_products
@order = Order.new(order_params)
@order.user_id = @current_user.id
# カートが空で商品注文するのはおかしいため
if @cart_products.empty?
flash[:danger] = 'カートが空です。商品を追加してから購入してください。'
render 'cart_products/index', status: :unprocessable_entity
return # ここで処理を止める
end
ActiveRecord::Base.transaction do
if @order.save
@cart_products.each do |cart_product|
@order.order_details.create!(product_id: cart_product.product_id,
amount: cart_product.amount,
price: cart_product.product.price,
product_name: cart_product.product.name)
end
@cart_products.destroy_all
else
render 'cart_products/index', status: :unprocessable_entity
return # AbstractController::DoubleRenderError防止(renderとreditect_toが同時に呼ばれてしまう)
end
end
flash[:success] = 'ご購入ありがとうございます'
redirect_to products_path
end
private
def order_params
params.require(:order).permit(:postal_code, :address, :apartment_name)
end
end
あらら、コントローラに結構責務がよってしまっています。
このままでも動くには動くのですがテストがしにくいのと再利用性がないと不便な所が多々あるので
Fat Model, Skinny Controllerの原則を意識してみます。
※current_userというメソッドはapplication_controller.rbで定義しています。
今からmodelにかなり責務を渡します。
modelさん責務を全うしてください。(DB操作はコントローラではなく基本modelに投げる)
まずはカートが空なのかどうかを確認するメソッドをモデルに任せましょう。
現在をログインしているuserのカート確認したいのでuserモデル書きます
def cart_empty?
cart_products.empty?
end
至ってシンプルですね。
UserモデルはCartProductモデルとアソシエーションしています。
なのでこのようなシンプルな書き方ができます。
内容は、カートの中身が空ならtrue
で中身があればfalse
になります。
メソッド名は自分で考えました。
カート空ですか?みたいな感じです、真偽値を返すメソッドは?をつける習慣があるので自分も真似してみました。
余談ですが、逆に中身があればtrue
を返すpresent?
メソッドと言うものもあります。
次にどうやってコントローラに渡すのか?
if current_user.cart_empty?
flash[:danger] = 'カートが空です。商品を追加してから購入してください。'
render 'cart_products/index', status: :unprocessable_entity
return # AbstractController::DoubleRenderErrorが出るためここで処理を止める
end
current_userはapplication_controller
の内容でmodelでは使えないのでコントローラに書いてます
order関連なのになぜUserモデルに書くの?
このようにuser関係.メソッド名と続いてるのでUserモデルにメソッドを書きます
後は、フラッシュメッセージでなぜエラーが出たのかをユーザーに教えます。
render
で遷移せずに同じページにとどまることで入力された値が消えないで済みます。
今回の場合カート一覧と注文するformが同じページにあるので'cart_products/index'
と
このような書き方になってます。
return
は次の処理に行かないようにしています。(処理はまだ続きます)
次はDB操作が複数ある部分をmodelに任せましょう
ここで今回の本命、入力formはないけどDBに保存したい部分を叶える部分です
# 購入確定したらorder_detailsテーブルに買った時の注文の詳細を保存してカートの中身は全削除
def order_confirm(order)
ActiveRecord::Base.transaction do
if order.save
cart_products.each do |cart_product|
order.order_details.create!(product_id: cart_product.product_id,
amount: cart_product.amount,
price: cart_product.product.price,
product_name: cart_product.product.name)
end
cart_products.destroy_all
end
end
end
Order情報がDBに保存できたと同時に、現在ログインしているユーザーのカートの中身を一つずつ確認し注文詳細のテーブルのカラムに沿って保存しています。
このようにしてform入力なしでDBに保存することができました!
また、別テーブルに永久保存させたい内容のカラムを作ってあげる事で将来商品が編集されたり削除されたとしても
買った当時のデータのまま保存できます。
もしproduct_idとamount(購入確定時の数量)だけだったらproduct_idでたどった商品名と
product_idでたどったpriceなのでその時(編集された場合、買った当時ではない)の商品名と値段が表示されてしまいます。
商品が削除されていたら、、、、表示すらされません。
本記事とは関係ないので興味ない方は飛ばしてください
ActiveRecord::Base.transaction
この部分はトランザクションを使って整合性を保持しています。
(最近覚えたてなのでアウトプットします)
万が一注文が確定したのにそこから不具合があって処理が止まってしまった場合、
注文詳細は作られずに注文が確定してしまいます。
またカートの中身が前削除される前に不具合が起きた場合、
買ったのにカートの中身が残っているという奇妙な現象も起きます。
それだと、不整合なのでロールバック
という技を使ってトランザクションの中身の処理が途中で止まってしまった場合、処理を続けないで振り出しに戻ることができます。
またもう一つ、ロック
という技もあり、例えば、後1個で売り切れ!という商品がほぼ同時に2人のユーザーから注文があったとします。
トランザクションを使わないでいると、、、どっちも注文できてしまいます。
ですが実際は商品は1つだけ。非常に良くない状況になってしまうというわけです。
ロック
を使えば片方は注文処理を行わずに待機します。片方が処理が終わってそこでもう片方の人は
商品が売り切れという表示がされ「売り切れなんだなぁ」と知ることができます。
完成したcontorller
class OrdersController < ApplicationController
def create
@total_price = current_user.total_cart_price # renderで戻る時に使う
@cart_products = current_user.cart_products # renderで戻る時に使う
@order = Order.new(order_params)
@order.user_id = @current_user.id
# カートが空で購入するのはおかしいため
if current_user.cart_empty?
flash[:danger] = 'カートが空です。商品を追加してから購入してください。'
render 'cart_products/index', status: :unprocessable_entity
return # AbstractController::DoubleRenderErrorが出るためここで処理を止める
end
if current_user.order_confirm(@order)
flash[:success] = 'ご購入ありがとうございます'
redirect_to products_path
else
render 'cart_products/index', status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(:postal_code, :address, :apartment_name)
end
end
26行あったのが16行になりだいぶスッキリしました!
コントローラーはリクエストを受け取り、適切なレスポンスを生成する役割に集中できています。
これにより、コントローラーとモデルの責任が明確に分離され、テストやメンテナンスが容易になります。
また、コントローラーがスリムになることで、アクションの目的が一目で分かり、可読性も向上します
個人的感想なのですが、プログラミングは細かく細かく作るのがいいみたいですね。
管理しやすいから?テストしやすいから?目的がわかりやすいから?だと思います。
まだ実務もやったことない初学者ですがそう思いました。
では最後にサクッと注文詳細モデルとコントローラについて
order_detailモデル
class OrderDetail < ApplicationRecord
belongs_to :order
belongs_to :product
validates :amount, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :product_name, presence: true
# ひとつの商品の合計
def subtotal
price * amount
end
end
order_detailsコントローラ
module Admin
class OrderDetailsController < BaseController
skip_before_action :login_required, only: %i[index show]
def index
@orders = Order.latest
end
def show
@order = Order.find(params[:id])
@order_details = OrderDetail.where(order_id: params[:id])
@total_price = @order.total_billing_price
end
end
end
OrderDetailsControllerではビューに移すだけです
一覧画面は最新順に並び替えています
データ加工もDBに関連することなのでモデルに書きます。
詳細ページは、注文の主キーを取得し注文詳細の一覧を表示します
@total_price
は注文詳細の値段の合計金額を表示します。
bin/rails console
を使い確認しましょう。
Order.find(params[:id])
今回はorder_id: 2
の注文情報を例としてます
orderした人の住所や郵便番号や名前が取得できます
OrderDetail.where(order_id: params[:id])
OrderDetailのorder_idが2の注文詳細が取得できます。
今回の場合、4つ商品の注文詳細の情報が確認できるというわけです。
order_id
が1と9のものを取得しました。
product_id
に注目してください。
商品名と値段が変わっても当時の情報が保存されているのがわかると思います。
商品名は気にしないでくださいw
order.total_billing_priceの部分
def total_billing_price
order_details.inject(0) do |sum, order_detail|
sum + order_detail.subtotal
end
end
注文詳細の合計金額の計算です。
inject
を使い引数に0を渡すことで初期値が0になります。
よって、最初は0 + 1つ目の商品の金額(1個の価格と個数を掛け算した結果)
次に1つ目の商品の金額 + 2つ目の商品の金額
1つ目と2つ目の商品の金額 + 3つ目の商品の金額
このような処理が行われます。order_detail.subtotal
はOrderDetailモデルに書いてあります。
これにてformに入力しないでDBに保存でき、ビューに表示する事ができました!
最後に
最初は全然理解できませんでしたが、ようやく少しずつ理解ができてきました。
これからもどんどんアウトプットして理解を深めていきます
お疲れ様でした!!!