~part2~【FC版】React + Rails API + axios + react-router-domでCRUDを実装する

こんにちは！スージーです。
前回書いた[こちら](https://qiita.com/suzy1031/items/cc708fc705158b774fd6)の続きです

### やりたい事

- モデルを1対1で関連付け
- CRUDを実装

React × Rails APIで関連付けをしたモデルのCRUDを実装するにはこんな感じでやるんだなーって感じで見ていただけると幸いです

### 開発環境

Ruby 2.7.1
Rails 6.0.4
MySQL
node.js 14.8.0
React 17.0.2

### 参考

- [【Rails】モデルの関連付けで用いられるbuildメソッドまとめ](【Rails】モデルの関連付けで用いられるbuildメソッドまとめ)


### やらないこと
- rails
    - トランザクションの説明
    - リレーションの説明
    - エラーハンドリング

### まず`gem foreman`を入れる
毎回ローカルサーバを立ち上げる時に、`rails s`と`yarn start`をするのが面倒くさいので

```terminal
backend $ gem install foreman
touch Procfile
```
backendディレクトリに`Procfile`を作成します

```
// Procfile
web: bundle exec rails s
npm: cd ../frontend && yarn start
```

`gem foreman`はデフォルトだとrails側が`5000`番ポート、client側が`5100`番ポートをlistenするので`cors.rb`と`client.js`を修正します

```ruby
# cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:5100'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

```js
// client.js
const client = applyCaseMiddleware(
  axios.create({
    baseURL: 'http://localhost:5000/api/v1',
  }),
  options
);
```

これで設定完了です

```terminal
backend $ foreman start
07:06:36 web.1  | started with pid 6341
07:06:36 npm.1  | started with pid 6342
07:06:37 npm.1  | yarn run v1.22.10
07:06:37 npm.1  | $ react-scripts start
07:06:38 web.1  | => Booting Puma
07:06:38 web.1  | => Rails 6.0.4 application starting in development 
07:06:38 web.1  | => Run `rails server --help` for more startup options
07:06:39 web.1  | Puma starting in single mode...
07:06:39 web.1  | * Version 4.3.8 (ruby 2.7.1-p83), codename: Mysterious Traveller
07:06:39 web.1  | * Min threads: 5, max threads: 5
07:06:39 web.1  | * Environment: development
07:06:39 web.1  | * Listening on tcp://127.0.0.1:5000
07:06:39 web.1  | * Listening on tcp://[::1]:5000
07:06:39 web.1  | Use Ctrl-C to stop
07:06:45 npm.1  | ℹ ｢wds｣: Project is running at http://172.20.10.5/
07:06:45 npm.1  | ℹ ｢wds｣: webpack output is served from 
07:06:45 npm.1  | ℹ ｢wds｣: Content not from webpack is served from /Users/sugawarakouhei/project/local-project/react-form-sample/frontend/public
07:06:45 npm.1  | ℹ ｢wds｣: 404s will fallback to /
07:06:45 npm.1  | Starting the development server...
```
web => rails
npm => react
`foreman start`コマンド1つで両方のプロセスが実行される事が確認できればOK

### modelを作成

`post`モデルと1対1(has_one)で関連付ける`detailInfo`モデルを作成します

```terminal
backend $ rails g model detailInfo
```

```ruby
# 日付_create_detail_infos.rb
class CreateDetailInfos < ActiveRecord::Migration[6.0]
  def change
    create_table :detail_infos do |t|
      t.references :post, null: false
      t.string :favorite_food, limit: 100
      t.string :favorite_toy, limit: 100

      t.timestamps
    end
  end
end
```
外部キーと他に2つのカラムを持たせます。`好きな食べ物`と`好きなおもちゃ`です。全くテーブルを分ける必要がないデータ構造ですが、練習なので悪しからず...

```ruby
# post.rb
class Post < ApplicationRecord
    has_one :detail_info, dependent: :destroy
end
```

`dependent: :destroy`は親モデルが物理削除された時に一緒に子モデルも物理削除します。

```ruby
# detail_iinfo.rb
class DetailInfo < ApplicationRecord
    belongs_to :post
end
```

これで1対1の関係になりました

### コントローラーの修正

```ruby
class Api::V1::PostsController < ApplicationController

  def index
    render json: Post.all.to_json(include: :detail_info)
  end

  def show
    render json: Post.find(params[:id]).to_json(include: :detail_info)
  end

  def create
    ActiveRecord::Base.transaction do
      post = Post.new(post_params)
      detail_info = post.build_detail_info(detail_info_params)
      if post.save && detail_info.save
        render json: post.to_json(include: :detail_info)
      else
        ender json: { post: post.errors, detail_info: detail_info.errors }, status: 422
      end
    end
  end

  def update
    ActiveRecord::Base.transaction do
      post = Post.find(params[:id])
      detail_info = DetailInfo.find_by(post_id: params[:id])
        if post.update(post_params) && detail_info.update(detail_info_params)
          render json: post.to_json(include: :detail_info)
        else
          render json: { post: post.errors, detail_info: detail_info.errors }, status: 422
        end
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
    render json: post.to_json(include: :detail_info)
  end

  private
    def post_params
      params.require(:post).permit(:name, :neko_type)
    end

    def detail_info_params
      params.require(:detail_info).permit(:favorite_food, :favorite_toy)
    end
end
```

render末尾に`.to_json(include: :detail_info)`を追加しました。リレーションを設定したデータを紐付けてjsonを返してくれるようになります。後ほどcurlコマンドを叩いて確認します

`createアクション`は`buildメソッド`を使って保存します。1対1のときは`build_モデル名`で保存します。

あとトランザクションを使い、失敗した時にはrollbackするように処理を囲みます。

複数テーブルへの保存処理には[accepts_nested_attributes_for](accepts_nested_attributes_for)(非推奨)や[Formオブジェクト](https://applis.io/posts/rails-design-pattern-form-objects)を使う方法など存在します。ただこれらをうまく動作させる事ができなかった（筆者の知識が乏しく）ので`transaction`と`buildメソッド`を使い、2つのモデルに登録する処理をcontrollerに書きました(fatコントローラーになってしまいました)

`updateアクション`も`createアクション`同様に`trunsaction`を使い処理を囲みます。`findメソッド`で`postモデル`から該当レコードを1件取得します。`find_byメソッド`を使い、親モデルと同じid(post_id)を持つ該当レコードを`detail_infoモデル`から1件取得します。

### `seeds.rb`を修正

```ruby
post1 = Post.create!(name: 'ニャア', neko_type: 'アメリカンショートヘア')
DetailInfo.create!(post: post1, favorite_food: '魚', favorite_toy: '猫じゃらし')

post2 = Post.create!(name: 'まる', neko_type: 'スコッティシュフォールド')
DetailInfo.create!(post: post2, favorite_food: '野菜', favorite_toy: 'まりたん')

post3 = Post.create!(name: 'むぎ', neko_type: 'スコッティシュフォールド')
DetailInfo.create!(post: post3, favorite_food: '肉', favorite_toy: 'ダンボール')
```
[以前の記事](https://qiita.com/suzy1031/items/cc708fc705158b774fd6#seedrb)で作成した`seeds.rb`を修正しました。`post: post1`のように記載して、外部キーを持っている子モデルに対してcreateしています

curlコマンドを叩いて確認してみます

```terminal
backend $  rails db:migrate:reset
rails db:seed

// 一覧
curl http://localhost:5000/api/v1/posts
[{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}},{"id":2,"name":"まる","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.111Z","updated_at":"2021-07-30T23:22:17.111Z","detail_info":{"id":2,"post_id":2,"favorite_food":"野菜","favorite_toy":"まりたん","created_at":"2021-07-30T23:22:17.116Z","updated_at":"2021-07-30T23:22:17.116Z"}},{"id":3,"name":"むぎ","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.121Z","updated_at":"2021-07-30T23:22:17.121Z","detail_info":{"id":3,"post_id":3,"favorite_food":"肉","favorite_toy":"ダンボール","created_at":"2021-07-30T23:22:17.126Z","updated_at":"2021-07-30T23:22:17.126Z"}}]

// 詳細
curl http://localhost:5000/api/v1/posts/1
{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}}

// 新規作成
curl -X POST "http://localhost:5000/api/v1/posts" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "test","favorite_toy":"test"}}'
{"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"test","favorite_toy":"test","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:33:18.496Z"}}%

// 更新
curl -X PATCH "http://localhost:5000/api/v1/posts/4" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "update3","favorite_toy":"update3"}}'
{"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}}%

// 削除
curl -X DELETE http://localhost:5000/api/v1/posts/4
{"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}}
```

`[{"id": 1 ~~ detailInfo: {"id": 1, "post_id": 1 ~~} }]`
オブジェクトの中に`detailInfo`というキーでオブジェクトがネストされたデータ構造になっています。想定通りにデータが入っています

また`post` / `patch`のリクエストを受けて`createアクション` / `updateアクション`が正常に動いている事が確認できました

`delete`のリクエストを受けて親モデルのレコードが削除された時にはモデルに追記した`dependent: :destroy`によって子モデルのレコードも削除されている事が確認できました

### 一覧画面の修正

以前の記事でapiコールの実装は完了しているので、取得したデータの中にある`detailInfo: {"id": 1, "post_id": 1 ~~}`のオブジェクト内から必要なプロパティを展開し表示します

```jsx
// List.jsx
import React, { useEffect, useState } from 'react';
// deletePostを追加
import { getList, deletePost } from '../lib/api/post';
import { useHistory, Link } from 'react-router-dom';

const List = () => {
  const [dataList, setDataList] = useState([]);

  useEffect(() => {
    handleGetList();
  }, []);

  const handleGetList = async () => {
    try {
      const res = await getList();
      console.log(res.data);
      setDataList(res.data);
    } catch (e) {
      console.log(e);
    }
  };

  const history = useHistory();

  const handleDelete = async (item) => {
    console.log('click', item.id);
    try {
      const res = await deletePost(item.id);
      console.log(res.data);
      handleGetList();
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <>
      <h1>HOME</h1>
      <button onClick={() => history.push('/new')}>新規作成</button>
      <table>
        <thead>
          <tr>
            <th>名前</th>
            <th>猫種</th>
            {/* 追加 */}
            <th>好きな食べ物</th>
            {/* 追加 */}
            <th>好きなおもちゃ</th>
            <th colSpan='1'></th>
            <th colSpan='1'></th>
            <th colSpan='1'></th>
          </tr>
        </thead>
        {dataList.map((item, index) => (
          <tbody key={index}>
            <tr>
              <td>{item.name}</td>
              <td>{item.nekoType}</td>
              {/* 追加 */}
              <td>{item.detailInfo.favoriteFood}</td>
              {/* 追加 */}
              <td>{item.detailInfo.favoriteToy}</td>
              <td>
                <Link to={`/edit/${item.id}`}>更新</Link>
              </td>
              <td>
                <Link to={`/post/${item.id}`}>詳細へ</Link>
              </td>
              <td>
                <button onClick={() => handleDelete(item)}>削除</button>
              </td>
            </tr>
          </tbody>
        ))}
      </table>
    </>
  );
};
export default List;
```
一覧に`好きな食べ物`と`好きなおもちゃ`が表示されていればOK
![screencapture-localhost-3001-2021-08-01-08_51_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/ff377db0-de5a-dcca-047c-10abbc0cf48c.png)

### 詳細画面の修正

同じ要領で詳細画面の修正をしていきます。詳細画面では`query.id`の取得タイミングがrenderingより遅くなってしまうので`{data.detailInfo.favoriteFood}`が`TypeError: Cannot read property 'favoriteFood' of undefined`と怒られてしまいます。なのでstateに初期値を設定します

```jsx
// Detail.jsx
import React, { useEffect, useState } from 'react';
import { getDetail } from '../lib/api/post';
import { useHistory, useParams } from 'react-router-dom';

const Detail = () => {
  // 修正
  const [data, setData] = useState({
    name: '',
    neko_type: '',
    detailInfo: {
      favorite_food: '',
      favorite_toy: '',
    },
  });

  const query = useParams();
  console.log(query.id);

  const history = useHistory();

  useEffect(() => {
    handleGetDetail(query);
  }, [query]);

  const handleGetDetail = async (query) => {
    try {
      const res = await getDetail(query.id);
      console.log(res.data);
      setData(res.data);
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <>
      <h1>DETAIL</h1>
      <div>ID：{data.id}</div>
      <div>名前：{data.name}</div>
      <div>猫種：{data.nekoType}</div>
      {/* 追加 */}
      <div>好きな食べ物：{data.detailInfo.favoriteFood}</div>
      {/* 追加 */}
      <div>好きなおもちゃ：{data.detailInfo.favoriteToy}</div>
      <button onClick={() => history.push('/')}>戻る</button>
    </>
  );
};
export default Detail;
```
`好きな食べ物`と`好きなおもちゃ`が表示されていればOK
![screencapture-localhost-3001-post-1-2021-08-01-09_01_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/b82baa0f-a259-1223-bc1c-7553bc630d6f.png)

### 削除機能（追加無し）

削除機能は変更なしで削除できることが確認できればOK

### 新規作成の修正

新規作成を修正します

- テキストフィールを`Form.jsx`に追加
- `Submitイベント`でapiに送るパラメータのオブジェクト構造を加工する

```jsx
// Form.jsx
import React from 'react';

const Form = (props) => {
  const { handleChange, handleSubmit, value, buttonType } = props;
  return (
    <>
      <form>
        <div>
          <label htmlFor='name'>猫の名前：</label>
          <input
            type='text'
            name='name'
            id='name'
            onChange={(e) => handleChange(e)}
            value={value.name}
          />
        </div>
        <div>
          <label htmlFor='nekoType'>猫種</label>
          <input
            type='text'
            name='nekoType'
            id='nekoType'
            onChange={(e) => handleChange(e)}
            value={value.nekoType}
          />
        </div>
        {/* 追加 */}
        <div>
          <label htmlFor='nekoType'>好きな食べ物</label>
          <input
            type='text'
            name='favoriteFood'
            id='favoriteFood'
            onChange={(e) => handleChange(e)}
            value={value.favoriteFood}
          />
        </div>
        <div>
          <label htmlFor='nekoType'>好きなおもちゃ</label>
          <input
            type='text'
            name='favoriteToy'
            id='favoriteToy'
            onChange={(e) => handleChange(e)}
            value={value.favoriteToy}
          />
        </div>
        {/* ここまで */}
        <input
          type='submit'
          value={buttonType}
          onClick={(e) => handleSubmit(e)}
        />
      </form>
    </>
  );
};
export default Form;
```

```jsx
// New.jsx
import React, { useState } from 'react';
import FormBody from './Form';
import { createPost } from '../lib/api/post';
import { useHistory } from 'react-router-dom';

const New = () => {
  const [value, setValue] = useState({});
  const history = useHistory();

  const handleChange = (e) => {
    setValue({
      ...value,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    console.log(value);
    // 追加
    const params = generateParams();
    try {
      const res = await createPost(params);
      console.log(res);
      history.push('/');
    } catch (e) {
      console.log(e);
    }
  };

  // パラメータのオブジェクト構造を加工
  const generateParams = () => {
    const params = {
      name: value.name,
      nekoType: value.nekoType,
      // detailInfoというキーでオブジェクトをネストする
      detailInfo: {
        favoriteFood: value.favoriteFood,
        favoriteToy: value.favoriteToy,
      },
    };
    return params;
  };

  return (
    <>
      <h1>NEW</h1>
      <FormBody
        handleChange={handleChange}
        handleSubmit={handleSubmit}
        value={value}
        buttonType='登録'
      />
    </>
  );
};
export default New;
```
`params`の中身を見てみるとこのようなオブジェクトになっています
![スクリーンショット 2021-08-02 10.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/74025588-7585-4e36-e6ef-d0ee08f4a02d.png)

登録ボタンを押下してデータが登録できればOK
railsのログを見てみると、パラメータを受け取って2つの`insert`処理が走っている事がわかります。`BEGIN`と`COMMIT`に挟まれ処理が実行されているので`trunsaction`が効いている事も確認できます

```terminal
10:11:26 web.1  | Started POST "/api/v1/posts" for ::1 at 2021-08-02 10:11:26 +0900
10:11:26 web.1  | Processing by Api::V1::PostsController#create as HTML
10:11:26 web.1  |   Parameters: {"name"=>"test", "neko_type"=>"test", "detail_info"=>{"favorite_food"=>"test", "favorite_toy"=>"test"}, "post"=>{"name"=>"test", "neko_type"=>"test"}}
10:11:26 web.1  |    (54.6ms)  BEGIN
10:11:26 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create'
10:11:26 web.1  |   Post Create (14.8ms)  INSERT INTO `posts` (`name`, `neko_type`, `created_at`, `updated_at`) VALUES ('test', 'test', '2021-08-02 01:11:26.894098', '2021-08-02 01:11:26.894098')
10:11:26 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create'
10:11:27 web.1  |   DetailInfo Create (26.6ms)  INSERT INTO `detail_infos` (`post_id`, `favorite_food`, `favorite_toy`, `created_at`, `updated_at`) VALUES (4, 'test', 'test', '2021-08-02 01:11:26.977189', '2021-08-02 01:11:26.977189')
10:11:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create'
10:11:27 web.1  |    (6.9ms)  COMMIT
10:11:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:12:in `create'
10:11:27 web.1  | Completed 200 OK in 142ms (Views: 0.4ms | ActiveRecord: 102.9ms | Allocations: 4071)
```

### 更新画面を修正

最後に更新処理を修正します

- stateの初期値を修正する
- `Submitイベント`でapiに送るパラメータのオブジェクト構造を加工する

```jsx
// Edit.jsx
import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { updatePost, getDetail } from '../lib/api/post';
import FormBody from './Form';

const Edit = () => {
  const [value, setValue] = useState({
    name: '',
    nekoType: '',
    // 追加
    favoriteFood: '',
    // 追加
    favoriteToy: '',
  });

  const query = useParams();

  const history = useHistory();

  useEffect(() => {
    handleGetData(query);
  }, [query]);

  const handleGetData = async (query) => {
    try {
      const res = await getDetail(query.id);
      console.log(res.data);
      setValue({
        name: res.data.name,
        nekoType: res.data.nekoType,
        // 追加
        favoriteFood: res.data.detailInfo.favoriteFood,
        // 追加
        favoriteToy: res.data.detailInfo.favoriteToy,
      });
    } catch (e) {
      console.log(e);
    }
  };

  const handleChange = (e) => {
    setValue({
      ...value,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    // 追加
    const params = generateParams();
    try {
      const res = await updatePost(query.id, params);
      console.log(res);
      history.push('/');
    } catch (e) {
      console.log(e);
    }
  };

  // パラメータのオブジェクト構造を加工
  const generateParams = () => {
    const params = {
      name: value.name,
      nekoType: value.nekoType,
      detailInfo: {
        favoriteFood: value.favoriteFood,
        favoriteToy: value.favoriteToy,
      },
    };
    return params;
  };

  return (
    <>
      <h1>Edit</h1>
      <FormBody
        handleChange={handleChange}
        handleSubmit={handleSubmit}
        value={value}
        buttonType='更新'
      />
    </>
  );
};
export default Edit;

```

修正箇所は`New.jsx`とほとんど同じです

更新ボタンを押下してデータが更新できればOK
railsのログを見てみると、パラメータを受け取って2つの`select`と2つの`update`処理が走っている事がわかります。`id=4`のレコードを`post`テーブルから取得して更新処理をしています。`post_id=4`のレコードを`detailInfo`テーブルから取得して更新処理をしています。`BEGIN`と`COMMIT`に挟まれ処理が実行されているので`trunsaction`が効いている事も確認できます

```terminal
10:16:04 web.1  | Started PATCH "/api/v1/posts/4" for ::1 at 2021-08-02 10:16:04 +0900
10:16:04 web.1  | Processing by Api::V1::PostsController#update as HTML
10:16:04 web.1  |   Parameters: {"name"=>"update", "neko_type"=>"update", "detail_info"=>{"favorite_food"=>"udpate", "favorite_toy"=>"update"}, "id"=>"4", "post"=>{"name"=>"update", "neko_type"=>"update"}}
10:16:04 web.1  |    (0.2ms)  BEGIN
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update'
10:16:04 web.1  |   Post Load (0.6ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update'
10:16:04 web.1  |   DetailInfo Load (21.6ms)  SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:26:in `block in update'
10:16:04 web.1  |   Post Update (12.4ms)  UPDATE `posts` SET `posts`.`name` = 'update', `posts`.`neko_type` = 'update', `posts`.`updated_at` = '2021-08-02 01:16:04.763790' WHERE `posts`.`id` = 4
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update'
10:16:04 web.1  |   Post Load (14.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update'
10:16:04 web.1  |   DetailInfo Update (1.0ms)  UPDATE `detail_infos` SET `detail_infos`.`favorite_food` = 'udpate', `detail_infos`.`favorite_toy` = 'update', `detail_infos`.`updated_at` = '2021-08-02 01:16:04.821784' WHERE `detail_infos`.`id` = 4
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update'
10:16:04 web.1  |   DetailInfo Load (21.8ms)  SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:28:in `block in update'
10:16:04 web.1  |    (7.4ms)  COMMIT
10:16:04 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:24:in `update'
10:16:04 web.1  | Completed 200 OK in 184ms (Views: 0.2ms | ActiveRecord: 79.4ms | Allocations: 6798)

```

### 最後に
コントローラがfatになっているので、もっと良い書き方があればご教示ください！

##### おわり