formはないけどDBに保存したい時とFat Model, Skinny Controllerの原則を意識

# ユーザーからの入力formはないけどDBに保存したい場合
こういう場面ありますよね。
例えば今回説明で使う例として`order`が保存されると`order_detail`保存されるようにします。

流れで言うと、formで`注文`が確定したらそのまま一緒に`注文詳細`が確定される感じです。
ただ、そのまま保存するのではなく`注文詳細`は買った当時の情報を保存するようにしています。
そうする事で、商品名が改名や値段もしくは商品自体削除されてしまった場合でも買った当時の注文データが保存すると言うわけです

## ER図
今回出てくる奴ら
カラムはPKとFKしか書いてません
![スクリーンショット 2023-06-25 11.03.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/1e152e70-f653-a0c0-9bbe-e1e8bbd6fd6b.png)



## まずはモデルから
```ruby:order.rb
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
```
## 次にコントローラー
```ruby:orders_controller.rb
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の原則を意識してみます。

:::note warn
※current_userというメソッドはapplication_controller.rbで定義しています。
:::

### 今からmodelにかなり責務を渡します。
modelさん責務を全うしてください。(DB操作はコントローラではなく基本modelに投げる)
まずはカートが空なのかどうかを確認するメソッドをモデルに任せましょう。
現在をログインしているuserのカート確認したいのでuserモデル書きます
```ruby:user.rb
def cart_empty?
  cart_products.empty?
end
```
至ってシンプルですね。
UserモデルはCartProductモデルとアソシエーションしています。
なのでこのようなシンプルな書き方ができます。
内容は、カートの中身が空なら`true`で中身があれば`false`になります。
メソッド名は自分で考えました。
カート空ですか？みたいな感じです、真偽値を返すメソッドは？をつける習慣があるので自分も真似してみました。

余談ですが、逆に中身があれば`true`を返す`present?`メソッドと言うものもあります。

## 次にどうやってコントローラに渡すのか？
```ruby:orders_controller.rb
if current_user.cart_empty?
  flash[:danger] = 'カートが空です。商品を追加してから購入してください。'
  render 'cart_products/index', status: :unprocessable_entity
  return # AbstractController::DoubleRenderErrorが出るためここで処理を止める
end
```
current_userは`application_controller`の内容でmodelでは使えないのでコントローラに書いてます

:::note
order関連なのになぜUserモデルに書くの？
このようにuser関係.メソッド名と続いてるのでUserモデルにメソッドを書きます
:::

後は、フラッシュメッセージでなぜエラーが出たのかをユーザーに教えます。
`render`で遷移せずに同じページにとどまることで入力された値が消えないで済みます。
今回の場合カート一覧と注文するformが同じページにあるので`'cart_products/index'`と
このような書き方になってます。

`return`は次の処理に行かないようにしています。(処理はまだ続きます)

## 次はDB操作が複数ある部分をmodelに任せましょう
ここで今回の本命、入力formはないけどDBに保存したい部分を叶える部分です
```ruby:orders_controller.rb
# 購入確定したら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に保存することができました！

また、別テーブルに永久保存させたい内容のカラムを作ってあげる事で将来商品が編集されたり削除されたとしても
買った当時のデータのまま保存できます。

:::note warn
もしproduct_idとamount(購入確定時の数量)だけだったらproduct_idでたどった商品名と
product_idでたどったpriceなのでその時(編集された場合、買った当時ではない)の商品名と値段が表示されてしまいます。
商品が削除されていたら、、、、表示すらされません。
:::

::: note
本記事とは関係ないので興味ない方は飛ばしてください
`ActiveRecord::Base.transaction`
この部分はトランザクションを使って整合性を保持しています。
(最近覚えたてなのでアウトプットします)
万が一注文が確定したのにそこから不具合があって処理が止まってしまった場合、
注文詳細は作られずに注文が確定してしまいます。
またカートの中身が前削除される前に不具合が起きた場合、
買ったのにカートの中身が残っているという奇妙な現象も起きます。
それだと、不整合なので`ロールバック`という技を使ってトランザクションの中身の処理が途中で止まってしまった場合、処理を続けないで振り出しに戻ることができます。

またもう一つ、`ロック`という技もあり、例えば、後1個で売り切れ！という商品がほぼ同時に2人のユーザーから注文があったとします。
トランザクションを使わないでいると、、、どっちも注文できてしまいます。
ですが実際は商品は1つだけ。非常に良くない状況になってしまうというわけです。

`ロック`を使えば片方は注文処理を行わずに待機します。片方が処理が終わってそこでもう片方の人は
商品が売り切れという表示がされ「売り切れなんだなぁ」と知ることができます。
:::

## 完成したcontorller
```ruby:orders_controller.rb
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行になりだいぶスッキリしました！
コントローラーはリクエストを受け取り、適切なレスポンスを生成する役割に集中できています。
これにより、コントローラーとモデルの責任が明確に分離され、テストやメンテナンスが容易になります。
また、コントローラーがスリムになることで、アクションの目的が一目で分かり、可読性も向上します

:::note
個人的感想なのですが、**プログラミングは細かく細かく作るのがいいみたいですね。**
管理しやすいから？テストしやすいから？目的がわかりやすいから？だと思います。
まだ実務もやったことない初学者ですがそう思いました。
:::

## では最後にサクッと注文詳細モデルとコントローラについて

order_detailモデル
```ruby:order_detail.rb
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コントローラ
```ruby:order_controller.rb
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`は注文詳細の値段の合計金額を表示します。

![スクリーンショット 2023-06-25 9.46.57（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/7c21eda0-f8cf-ed0f-bbce-492db640061e.png)

`bin/rails console`を使い確認しましょう。

`Order.find(params[:id])`
今回は`order_id: 2`の注文情報を例としてます
orderした人の住所や郵便番号や名前が取得できます

`OrderDetail.where(order_id: params[:id])`
OrderDetailのorder_idが2の注文詳細が取得できます。
今回の場合、４つ商品の注文詳細の情報が確認できるというわけです。

そしてもう一つ商品が編集された場合も見ていきましょう
![スクリーンショット 2023-06-25 10.17.44（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/aede6d30-aae6-0483-eeb3-607cdea9e742.png)

`order_id`が１と9のものを取得しました。

`product_id`に注目してください。

商品名と値段が変わっても当時の情報が保存されているのがわかると思います。

商品名は気にしないでくださいw

order.total_billing_priceの部分
```ruby:order.rb
def total_billing_price
  order_details.inject(0) do |sum, order_detail|
    sum + order_detail.subtotal
  end
end
```
注文詳細の合計金額の計算です。

`inject`を使い引数に0を渡すことで初期値が０になります。
よって、最初は`0 + 1つ目の商品の金額(1個の価格と個数を掛け算した結果)`
次に`1つ目の商品の金額 + 2つ目の商品の金額` `1つ目と2つ目の商品の金額 + 3つ目の商品の金額`
このような処理が行われます。`order_detail.subtotal`はOrderDetailモデルに書いてあります。

**これにてformに入力しないでDBに保存でき、ビューに表示する事ができました！**

### 最後に
最初は全然理解できませんでしたが、ようやく少しずつ理解ができてきました。
これからもどんどんアウトプットして理解を深めていきます

お疲れ様でした！！！