【Rails】Rakeタスクによる初期データ投入

# はじめに
今まで初期データ（ダミーデータ）を投入するとき、とりあえず`rails db:seed`コマンドを実行していました。
rakeタスクでも同様のことができると知ったので、こちらにまとめます。

# Rakeタスクとは
Rakeは、Rubyで書かれた特定の処理をタスクとして、まとめて実行するための仕組みです。
このRakeが実行する処理を**Rakeタスク**と呼びます。

Rubyで書かれた、タスクを定義・管理するためのビルドツールです。
C言語やUnixでいうところの[make](https://qiita.com/mizcii/items/cfbd2aa17f6b7517c37f)みたいなものらしいです。

## Rakeタスクのメリット
- 作業の自動化: 手作業で行っていた面倒な作業を自動化できます
- 再利用性: 一度定義したタスクは、何度でも実行できます
- 可読性: Rubyのコードで記述するため、他の開発者でも理解しやすくなります

## RakeタスクとSeedの違い
### Seedの特徴
- **目的**: データベースの初期化
- **機能**: 特定のテーブルに初期データを挿入する
- **実行方法**: rake db:seedコマンドで実行
- **ファイル**: db/seeds.rbファイルにRubyのコードで記述
- **特徴**:
  - データベースの初期状態を設定するためのシンプルな仕組み
  - 具体的なデータの挿入に特化している
  - 比較的簡単な構造で記述できる
### Rakeタスクの特徴
- **目的**: 様々な処理の自動化
- **機能**: データベースのマイグレーション、テストの実行、データの生成など、幅広い処理が可能
- **実行方法**: `rake <タスク名>`コマンドで実行
- **ファイル**: `lib/tasks`ディレクトリに作成したRakeファイルにRubyのコードで記述
- **特徴**:
  - 柔軟性が高く、複雑な処理も実装できる
  - 独自のタスクを定義できる
  - seed機能もRakeタスクの一種と考えることができる


上記のようにseedを実行するだけではなく、データベースのマイグレーションなど幅広い処理が実行可能です。
例えば、`rails db:migrate:reset`→`rails db:seed`とコマンドを一個ずつ実行していた作業がrakeタスクで1回にまとめられます。

## タスクファイル
### ファイル作成方法
```
$ rails g task タスク名
```

### 今回作成したファイル
```lib/tasks/dummy_all_data.rake
namespace :dummy do
  desc "ダミーデータの投入"
  task all_data: :environment do
    ActiveRecord::Base.transaction do
      puts '--- start ---'

      puts '--- DB migrate reset ---'
      Rake::Task['db:migrate:reset'].invoke

      puts '--- dummy:userinfo ----'
      Rake::Task['dummy:user_info'].invoke

      puts '--- end ---'
    end
  end
end
```

#### 補足
- `namespace`: タスクを整理するための名前空間を定義しています
- `desc`: タスクの説明文です。コマンドラインで[`rake -T`](#タスク一覧)を実行すると、この説明文が表示されます
- `:environment`: 、このタスクを実行する前にRailsの環境が設定されます。どうやらrakeタスクを実行する時に[rails内部](https://qiita.com/FumiyaShibusawa/items/11035fc640bb36a615ad)でこの`environment`を実行しているみたいです
- `Rake::Task`: 上記では`db:migrate:reset`というタスクで全てのマイグレーションを取り消してDBを再構築。その後に[`dummy:user_info`](#今回作成する初期データ)というタスクでユーザーデータを作成します


## 今回作成する初期データ
seedファイルではなく、タスクファイル上で以下のテストユーザーを作成してみます。

### seedファイル
```lib/tasks/dummy_user_info.rake
namespace :dummy do
  task user_info: :environment do
    begin
      users = [
        { name: 'test1', email: 'test1@example.com', password: 'password1', auth0_id: ENV['AUTH_ID1'], is_admin: false },
        { name: 'administrator', email: 'admin@example.com', password: 'password2', auth0_id: ENV['ADMIN_AUTH_ID'], is_admin: true }
      ]

      users.each do |user|
        User.create!(user)
      end
    rescue StandardError => e
      puts "--- ユーザー作成中のエラー ---"
      puts e.class, e.message
    end
  end
end
```
### マイグレーションファイル
```db/migrate/20241129232933_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.2]
  def change
    create_table :users do |t|
      t.string :auth0_id
      t.string :name
      t.string :email
      t.string :password_digest
      t.boolean :is_admin
      t.datetime :discarded_at

      t.timestamps
    end

    add_index :users, [:auth0_id, :email], unique: true
  end
end
```
#### マイグレーションの補足
今回の主旨ではないですが、
[`add_index`（ユニークなインデックス）](https://railsdoc.com/page/add_index)を追加しているのは、モデルのバリデーションを通過しても、**DBレベルでのユニーク制約**によって重複するレコードの挿入を防ぐためです。

## タスク実行結果
```
$ rake dummy_all_data

--- start ---
--- DB migrate reset ---
Dropped database 'my_app_development'
Dropped database 'my_app_test'
Created database 'my_app_development'
Created database 'my_app_test'
== 20241129232933 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0177s
-- add_index(:users, [:auth0_id, :email], {:unique=>true})
   -> 0.0151s
== 20241129232933 CreateUsers: migrated (0.0330s) =============================

--- dummy:userinfo ----
--- end ---
```

### DBに登録されたユーザー
```ruby
[1] pry(main)> User.all
=>   User Load (4.5ms)  SELECT `users`.* FROM `users` /* loading for pp */ LIMIT 11
[#<User:0x0000ffff9a1d8a40
  id: 1,
  auth0_id: "auth0|***************",
  name: "test1",
  email: [FILTERED],
  password_digest: [FILTERED],
  is_admin: false,
  discarded_at: nil,
  created_at: "2024-12-01 18:36:51.412288000 +0900",
  updated_at: "2024-12-01 18:36:51.412288000 +0900">,
 #<User:0x0000ffff9bd7adc0
  id: 2,
  auth0_id: "auth0|***************",
  name: "administrator",
  email: [FILTERED],
  password_digest: [FILTERED],
  is_admin: true,
  discarded_at: nil,
  created_at: "2024-12-01 18:36:51.791835000 +0900",
  updated_at: "2024-12-01 18:36:51.791835000 +0900">]
```
ユーザーが作成されました！

### ユーザー作成失敗時
例えば、ユーザーのメールアドレスの@を省いてrakeタスク実行してみます。
`email: 'test1@example.com'`→`email: 'test1example.com'`

（バリデーションは別途で実装しています）

```
$ rake dummy_all_data

--- start ---
--- DB migrate reset ---
Dropped database 'my_app_development'
Dropped database 'my_app_test'
Created database 'my_app_development'
Created database 'my_app_test'
--- dummy:userinfo ----
--- ユーザー作成中のエラー ---
ActiveRecord::RecordInvalid
バリデーションに失敗しました: メールアドレス は正しい形式で入力してください。
--- end ---
```
例外処理でバリデーションのエラーメッセージが表示されました。

## タスク一覧
`rake -T`: Railsアプリケーションで定義されているすべてのRakeタスクとその簡単な説明を一覧表示するコマンドです。
```
$ rake -T 
(一部省略)
rake db:create                          # Create the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create...
rake db:drop                            # Drop the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all d...
rake db:encryption:init                 # Generate a set of keys for configuring Active Record encryption in a given environment
rake db:environment:set                 # Set the environment value for the database
rake db:fixtures:load                   # Load fixtures into the current environment's database
rake db:migrate                         # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rake db:migrate:down                    # Run the "down" for a given migration VERSION
rake db:migrate:redo                    # Roll back the database one migration and re-migrate up (options: STEP=x, VERSION=x)
rake db:migrate:status                  # Display status of migrations
rake db:migrate:up                      # Run the "up" for a given migration VERSION
rake db:prepare                         # Run setup if database does not exist, or run migrations if it does
rake db:reset                           # Drop and recreate all databases from their schema for the current environment and load the seeds
rake db:rollback                        # Roll the schema back to the previous version (specify steps w/ STEP=n)
rake db:schema:cache:clear              # Clear a db/schema_cache.yml file
rake db:schema:cache:dump               # Create a db/schema_cache.yml file
rake db:schema:dump                     # Create a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `con...
rake db:schema:load                     # Load a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `confi...
rake db:seed                            # Load the seed data from db/seeds.rb
rake db:seed:replant                    # Truncate tables of each database for current environment and load the seeds
rake db:setup                           # Create all databases, load all schemas, and initialize with the seed data (use db:reset to also drop all databases ...
rake db:version                         # Retrieve the current schema version number
rake dummy:all_data                     # Create dummy data
rake dummy:user_info                    # Create new users
...
```

よく見ると、自分が作成したタスクも一覧に表示されています。
> rake dummy:all_data                     # Create dummy data
rake dummy:user_info                    # Create new users

# まとめ
seedとRakeタスクは、どちらもRailsアプリ開発においての目的や機能が異なります。seedはデータベースの初期化に特化しており、Rakeタスクはより柔軟に処理を自動化するための仕組みです。

シンプルなデータの挿入であればseed、より複雑な処理や独自の処理が必要な場合はRakeタスクが適しています。

# 参考記事
[RailsでRakeタスクの作成](https://qiita.com/suzuki-koya/items/787b5562d2ae1a215d94)
[Ruby 3.3 リファレンスマニュアル library rake](https://docs.ruby-lang.org/ja/latest/library/rake.html)
[Railsガイド rake](https://railsguides.jp/v4.2/command_line.html#rake)