LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

ここまでの記事はこちら
Ruby on Railsの習得のためタスクリスト的なの作ってみる①〜構成、設計編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる②〜Rails7.0+React18.0+ MySQL8.0 Docker開発環境構築編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる③〜API開発・呼び出し編〜

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

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

第一回第二回で選定及び環境構築したとおり、DBにはMySQLを使っていきます。

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

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

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

$ rails db:system:change --to=mysql

/config/database.ymlを上書きしていいか聞かれるので、Yを返します。
これだけで

  • Gemfileの書き換え
  • configをMySQL用のデフォルト状態に上書き

してくれます。
Gemを書き換えたので

$ bundle install

もやっておきます。

このままでは個別のDBには適合していないので、ドキュメントに従って/config/database.ymlの開発用の設定箇所を書き換えます。

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

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

コマンドでファイル生成可能です。
(注意)次のパートでモデル生成のコマンドでマイグレーションファイルも一緒に生成できることが判明しました。そっちのコマンドの方が二度手間にならなくていいかも。

$ 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テーブルは先にマイグレーションファイル作成済みです。

マイグレーションの実行もコマンドで行います。

$ rails db:migrate

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

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

Active Recordのモデル実装

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

$ 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.

あー、マイグレーションのファイルも一緒に生成できたんですかそうですか。

$ rails generate model task --skip

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

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

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

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

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

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

Model実装

使い方その1 バリデーション

テーブルに保存する値のバリデーションをModelの機能として行ってくれます。
各バリデーションはドキュメント参照。

/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というのは私が勝手に作った返却値生成用のモジュールです。一応以下のアコーディオンの中に置いておきます。

ResponseGenerator(参考)
/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

使い方その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

次回予告

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0