🦓

[Rails]enum

2023/09/24に公開

はじめに

enumをよく使いますが、enumに対しての理解度がまだ低いと感じたので復習を兼ねてまとめてみました。

tl;dr

1. enum
2. データ型
3. 定義
4. オプション
5. Rails7のenum構文
6. 日本語対応
7. enumと非同期処理

enumとは

enumはenumerated typeの略称で、列挙型のテータタイプを持ちます。

列挙型とは、プログラミング言語やデータベース管理システムなどにおけるデータ型の一つで、複数の異なる定数を一つの集合として定義するもの。 多くの言語ではenumの略号で示される。

Railsのenumは、特定の属性に対して許容される値のリストを定義するための便利な方法です。
値に対して名前が付けられるため、コードの可読性が向上します。

enumのソースコード:
https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activerecord/lib/active_record/enum.rb#L167-L177

定義

integerのデータ型を定義します。
これで、整数型(integer)のカラム(status)を持つProjectモデルを定義できました。

rails g Project status:integer

default: 0:初期値を0にします。
null:false:レコードにnullの値を保存されることを許可しない

db/schema.rb
t.integer "status", default: 0, null: false

statusのenumは5つの異なる状態を持ちます。
それぞれの状態は0、1、2、3、4の整数値にマップされます。

app/models/article.rb
class Project < ApplicationRecord
  enum status: { planned: 0, started: 1, completed: 2, archived: 3, other: 4 }
end

デフォルト値を設定することができます。

app/models/article.rb
class Project < ApplicationRecord
  enum status: { planned: 0, started: 1, completed: 2, archived: 3, other: 4 }, _default: 0
end

オプション

_prefix:

enumを定義する際に、各enum値のプレフィックスを自動的に追加するオプションです。statuscompletedの場合、status_completedといった形式でメソッドが生成されます。

app/models/article.rb
class Project < ApplicationRecord
  enum status: { planned: 0, started: 1, completed: 2, archived: 3, other: 4 }, _prefix: true
end
irb(main):010:0> Project.status_completed
  Project Load (0.3ms)  SELECT "projects".* FROM "projects" WHERE "projects"."status" = $1  [["status", 2]]
=> 

_suffix:

enumを定義する際に、各enum値のサフィックスを自動的に追加するオプションです。statusplanningの場合、planned_statusといった形式でメソッドが生成されます。

class Article < ApplicationRecord
  enum status: { planned: 0, started: 1, completed: 2, archived: 3, other: 4 }, _suffix: true
end
irb(main):011:0> Project.planned_status
  Project Load (0.2ms)  SELECT "projects".* FROM "projects" WHERE "projects"."status" = $1  [["status", 0]]
=> 

scope:

enumを使用すると、enumの値ごとに自動的にスコープが生成されます。スコープを明示的に定義する必要はありません。enumの値は、そのままスコープとして利用できます。

# BAD
scope :completed, -> { where(status: 2) }

# GOOD
Project.completed

_scopes: falseオプションを入れてスコープを作成させないこともできます。

class Project < ApplicationRecord
  enum status: { planned: 0, started: 1, completed: 2, archived: 3, other: 4 }, _scopes: false
end

enumの定義によって使用できるカスタムメソッド

scope以外、こちらのカスタムメソッドも利用可能になります。

class Conversation < ActiveRecord::Base
  enum :status, [ :active, :archived ]
end

# 更新
# conversation.update! status: 0
conversation.active!
conversation.active? # => true
conversation.status  # => "active"

# conversation.update! status: 1
conversation.archived!
conversation.archived? # => true
conversation.status    # => "archived"

# conversation.status = 1
conversation.status = "archived"

# nil
conversation.status = nil
conversation.status.nil? # => true
conversation.status      # => nil

# not_ メソッド
Conversation.active
Conversation.not_active
Conversation.archived
Conversation.not_archived

# カラム検索
Conversation.where(status: [:active, :archived])
Conversation.where.not(status: :active)

https://api.rubyonrails.org/classes/ActiveRecord/Enum.html

Rails7のenum構文

オプションのアンダースコアをなしに変更されました。

従来の構文 rails7以後
_default default
_prefix prefix
_suffix suffix
_scopes scopes

日本語対応

enum の日本語対応を行う前のstatusハッシュです。

irb(main):005:0> Projec.statuses
=> {"planned"=>0, "started"=>1, "completed"=>2, "archived"=>3, "other"=>4}
irb(main):006:0> Project.statuses.keys
=> ["planned", "started", "completed", "archived", "other"]
irb(main):00:0> Project.statuses.values
=> [0, 1, 2, 3, 4]

フロント側では日本語を表示させたいので日本語の訳文を追加します。

config/locales/ja.yml
ja:
  enums:
    project:
      status:
        planned: 未着手
        started: 着手中
        completed: 完了
        archived: 保留
        other: その他

日本語対応の設定を追加する

railsアプリのデフォルトの言語を日本語にします。
また、翻訳ファイルの読み込みの記述を追加します。

config/application.rb
class Application < Rails::Application
    # ymlファイルを読み込む
    config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.{rb,yml}").to_s]
    config.i18n.default_locale = :ja
end

mapメソッドを使ってstatusの配列を取得します。
I18nヘルパメソッドを使うと対応した日本語を取得することができます。

irb(main):002:0> Project.statuses.keys.map{|status|[I18n.t("enums.project.status.#{status}"), Project.statuses[status]]}
=> [["未着手", 0], ["着手中", 1], ["完了", 2], ["保留", 3], ["その他", 4]]
irb(main):002:0> Project.statuses.keys.map { |status| [status, I18n.t("enums.project.status.#{status}"
)] }
=> [["planned", "未着手"], ["started", "着手中"], ["completed", "完了"], ["archived", "保留"], ["other", "その他"]]

セレクトボックスの形で選択できるようにします。

irb(main):004:0> p = Project.last
irb(main):005:0> p.status
=> "started"
irb(main):006:0> I18n.t("enums.project.status.#{p.status}")
=> "着手中"
irb(main):007:0> p.status_before_type_cast
=> 1

DBに保存されている整数値を取得するにはbefore_type_castメソッドがあります。
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html

enumと非同期処理

最後ではenumを非同期に更新する方法を見ていきます。
セレクトボックスを使用して非同期にプロジェクトの状態を変更できるようにしていきます。

app/views/projects/_project.html.erb
<%= form_with model: project do |form|%>
  <%= form.select :status, Project.statuses.keys.map { |status|         
      [I18n.t("enums.project.status.#{status}"), status] },    
      onhange:"this.form.requestSubmit()" %>
<% end %>

onchangeは、フォーム内の要素の内容が変更された時に発火させるイベントハンドラーです。
this.form.requestSubmit() は、フォーム内の現在のデータを使用してフォームを送信するために使用されます。
this.form は、this 要素が所属しているフォーム要素を参照します。
requestSubmit() メソッドは、フォームを送信するためのメソッドです。通常、フォームを送信するには、フォーム内のボタンをクリックするか、JavaScriptでこのメソッドを呼び出します。このメソッドを使用すると、フォームが通常の方法で送信されるのと同じように、フォームのデータがサーバーに送信されます。
https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit

turboでの非同期処理を追加する

フォームの送信後に非同期処理をさせたいのでupdate.turbo_stream.erbを作成し、フラッシュメッセージを表示させます。

app/views/projects/update.turbo_stream.erb
<%= turbo_stream.after "project_phase" do %>
  <span id="message">更新しました!</span>
<% end %>
<%= turbo_stream_action_tag :remove_later, targer: "message", after: "2000" %>

turboのカスタムアクションを作成します。application.jsに読み込みます。

app/javascript/remove_later.js
import { StreamActions } from "@hotwired/turbo-rails"

StreamActions.remove_later = function() {
  setTimeout(()=>{
    this.targetElements.forEach((element) => element.remove())
  }, this.getAttribute("after"))
}

終わりに

enumを定義することで、国際化に簡単に対応でき、スコープやカスタムメソッドを使えるようになるところ便利だと感じました。しっかり理解した上で使っていきたいです。

Discussion