Ruby on Railsの習得のためタスクリスト的なの作ってみる④〜API開発・MySQL接続とモデル実装編〜

ここまでの記事はこちら
[Ruby on Railsの習得のためタスクリスト的なの作ってみる①〜構成、設計編〜](https://qiita.com/pyon_snore/items/6e35f291910fcc541116)
[Ruby on Railsの習得のためタスクリスト的なの作ってみる②〜Rails7.0+React18.0+ MySQL8.0 Docker開発環境構築編〜](https://qiita.com/pyon_snore/items/b8d860e5e3d94a322dfd)
[Ruby on Railsの習得のためタスクリスト的なの作ってみる③〜API開発・呼び出し編〜](https://qiita.com/pyon_snore/items/16b37134751e123ec36b)

今回は、引き続きバックエンド開発です。DB周りをやっていきます。

[RailsでDDDってどうなの？そもそもRailsの設計思想って？という疑問に関しては別記事で語ることにしたのでよかったら読んでください。](https://qiita.com/pyon_snore/items/74c46115564e75c9fd25)
やはりRailsで開発するメリットを活かすなら **Rail way** に従おう、という結論に至ったのでRailsらしいごく普通の実装をしていきます。ちっちゃいアプリだしこの方が勢いよく開発できそう。

[第一回](https://qiita.com/pyon_snore/items/6e35f291910fcc541116)、[第二回](https://qiita.com/pyon_snore/items/b8d860e5e3d94a322dfd)で選定及び環境構築したとおり、DBにはMySQLを使っていきます。


## MySQLを使用するための設定変更

RailsのデフォルトのDBはSQLiteになっています。
今回はMySQLを採用してDockerコンテナを立て、デフォルトの3306番ポートを開けてあるので、そこにアクセスできるようRails側の設定を変えたいです。
どうやらアプリケーション作成した時点でMySQLに指定できたらしいのですが、[後からでも大丈夫とのこと](https://railsdoc.com/page/rails_db_system_change)。

以下のコマンドを実行すればOK。

```sh
$ rails db:system:change --to=mysql
```
`/config/database.yml`を上書きしていいか聞かれるので、`Y`を返します。
これだけで
- Gemfileの書き換え
- configをMySQL用のデフォルト状態に上書き

してくれます。
Gemを書き換えたので
```sh
$ bundle install
```
もやっておきます。

このままでは個別のDBには適合していないので、[ドキュメント](https://railsdoc.com/page/database_yml)に従って`/config/database.yml`の開発用の設定箇所を書き換えます。

```/config/database.yml
development:
  <<: *default
  database: testdb
  username: user
  password: pass
```

## Railsのマイグレーションを使う

https://railsdoc.com/migration

[コマンド](https://railsdoc.com/page/rails_generate_migration)でファイル生成可能です。
（注意）[次のパート](##Active-Recordのモデル実装)でモデル生成のコマンドでマイグレーションファイルも一緒に生成できることが判明しました。そっちのコマンドの方が二度手間にならなくていいかも。
```sh
$ rails generate migration Task
```


```/db/migrate/20230306154320_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[7.0]
  def change
    create_table :tasks do |t|
      
      t.timestamps
    end
  end
end
```

こういう感じで生成されて、`create_table`ブロック内に追加するカラムを書いていきます。
書き方は上記ドキュメント参照。

```/db/migrate/20230306154320_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[7.0]
  def change
    create_table :tasks do |t|
      t.references :user, foreign_key: true
      t.string :name, :null => false
      t.datetime :limit, :null => false
      t.string :priority, :null => false
    end
  end
end
```
主キーについては何も指定しなければ`id`カラムが生成され、主キーとなります。
※外部キー設定しているusersテーブルは先にマイグレーションファイル作成済みです。

マイグレーションの実行も[コマンド](https://railsdoc.com/page/rails_db_migrate)で行います。
```sh
$ rails db:migrate
```

dry runはなさそうなので、一旦やってみて`rails db:rollback`すればいいのだと思います、多分。

無事テーブルが作成され、`/db/schema.rb`というファイルが生成されました。
複数の環境間でスキーマの整合性を保つために使われるらしい。へーっ。（[参考記事](https://qiita.com/k0kubun/items/491a9d9f2745335566e3)）
人間が見てもわかりやすくていいですね。

## Active Recordのモデル実装
https://railsguides.jp/active_record_basics.html

LaravelでいうEloquentみたいな感じ。いわゆるORMというやつで、SQLを直接書かずにデータベース操作するアレです。
コマンドでファイル生成。と、思ったら

```sh
$ rails generate model task
    invoke  active_record
    conflict    db/migrate/20230306204236_create_tasks.rb
Another migration is already named create_tasks: /achievelist/db/migrate/20230306154320_create_tasks.rb. Use --force to replace this migration or --skip to ignore conflicted file.
```
あー、マイグレーションのファイルも一緒に生成できたんですかそうですか。

```sh
$ rails generate model task --skip
```
これでコンフリクトしたマイグレーションファイルの生成はスキップしてくれます。助かった。

### ModelとControllerの役割分担どんな感じ？

早速Modelを書いていきたいのですが、Active Record便利すぎてほぼ書くことなくないですか？
whereとかfindとかの条件もControllerに書いている人が多い気がします。ていうかそんなんControllerに書いておk？ユースケースとか挟まなくていいんですか？
その割に[バリデーションはActive Recordの仕事](https://railsguides.jp/active_record_validations.html)みたいなこと書いてあるし。

うーん。
まず、今回の方針的にRailsに従う以上ControllerがActiveRecordに依存するのはもう仕方ないものとして諦めます。
とはいえ、全部が全部Controllerに書くのも本来MVCフレームワークとして存在するRailsの方針に合わないはずです。
なので、今回の開発では以下のような折衷案で進めることにしましょう。

- ユースケース的に使い回すものは、シンプルな操作であってもModelにメソッドを書く
- Modelにメソッドを置いたとしても単にActiveRecordを一度呼ぶだけになる場合はControllerに書く

利便性と保守性で妥協するとこんな感じかと思います。

### Model実装

#### 使い方その1 バリデーション
テーブルに保存する値のバリデーションをModelの機能として行ってくれます。
[各バリデーションはドキュメント参照。](https://railsguides.jp/active_record_validations.html)

```/app/models/task.rb
class Task < ApplicationRecord
  validates :user_id, numericality: true
  validates :name, length: { minimum: 1, maximum: 200 }
  validates :priority, inclusion: {
    in: %w[HIGH MIDDLE LOW)],
    message: '優先度は (HIGH MIDDLE LOW) から選択して下さい。'
  }
  validates :limit, datetime: {
    message: '正式な日時を入力してください。'
  }
end
```

呼び出すときは、`new`してから`valid?`するとバリデーションチェックしてくれます。

```/app/controllers/api/v1/tasks_controller.rb
# (略)
task = Task.new({ user_id:, name:, limit:, priority: })

result =
  if !task.valid? # バリデーションを実行し、判定結果を真偽値で返却
    ResponseGenerator.validation_error(task.errors.full_messages) 
      # full_messagesでバリデーションエラーのメッセージ文字列の配列が取得できる
  elsif !task.save # テーブル保存を実行
    ResponseGenerator.db_error
  else
    ResponseGenerator.success
  end
# (略)
```
`ResponseGenerator`というのは私が勝手に作った返却値生成用のモジュールです。一応以下のアコーディオンの中に置いておきます。

<details><summary>ResponseGenerator(参考)</summary><div>

```/app/controllers/concerns/response_generator.rb
module ResponseGenerator
  extend ActiveSupport::Concern

  def success(body = nil)
    return if body.nil?

    {
      body:,
      status: 200
    }
  end

  def validation_error(errors = [])
    {
      body: {
        'title': 'params not validated.',
        'invalid-params': errors
      },
      status: 400
    }
  end

  def un_authorized_error
    {
      body: {
        'title': 'unauthorized.'
      },
      status: 401
    }
  end

  def not_found
    {
      body: {
        'title': 'resource not found.'
      },
      status: 404
    }
  end

  def db_error
    {
      body: {
        'title': 'db operations failed.'
      },
      status: 500
    }
  end

  module_function :success, :validation_error, :un_authorized_error, :not_found, :db_error
end
```

</div></details>


#### 使い方その2 クラスメソッドの作成
Modelクラスに対して、クラスメソッドを定義することができます。`self`をつけ忘れるとインスタンスメソッドになってしまうので注意。
目的は、一連の操作をモデルの役割としてメソッドにまとめることです。
以下は、クラスメソッド内部でトランザクションを使用してみた例です。トランザクション内で例外が発生するとロールバックしてくれる便利仕様、ありがとうございます。

```/app/models/task_clear_event.rb
class TaskClearEvent < ApplicationRecord
  require 'date'

  def self.task_clear(task_id, user_id) # selfつけないとインスタンスメソッドになる
    # (略) cleared_taskとtaskはこの辺で定義してる
    transaction do # トランザクション開始
      cleared_task.save!
      task.destroy!

      event = new(
        {
          task_id: task.id,
          cleared_task_id: cleared_task.id,
          event_datetime: Time.current
        }
      )
      event.save!
    end # トランザクション終了

    !event.nil? && event.present?
  end
end
```


# 次回予告
今回はモデルを作成しました。次回はバックエンドで認証関係をやっていきたいと思います！