🫐

ActiveRecord と Ecto の比較②: コンテキスト

2024/12/28に公開

本稿は、ActiveRecord と Ecto を比較するシリーズの第 2 回です。

ActiveRecord は Ruby on Rails の一部を構成するライブラリです。Ecto は Elixir のライブラリです。いずれもデータベースを操作するために利用します。

今回は、Elixir/Phoenix の世界で使われる「コンテキスト」という概念について書きます。前回の話の続きです。


前回の記事で使用した Elixir の User 構造体モジュールのソースコードを再掲します。

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email])
    |> prepare_changes(&downcase_email/1)
  end

  defp downcase_email(changeset) do
    email = get_field(changeset, :email)
    put_change(changeset, :email, String.downcase(email))
  end
end

ActiveRecord の before_save コールバックを直接的に Ecto に翻訳しようとするとこうなりますけれども、実際の Web システム開発ではあまりこんな風には書きません。

私なら User 構造体を操作する関数群を集めたモジュールを別に作り、downcase_email/1 のような処理はそちらに移します。例えば、次のように MyApp.Account モジュールを定義します。

defmodule MyApp.Account do
  alias Account.User
  alias MyApp.Repo

  def create_user(attrs) do
    %User{}
    |> User.changeset(attrs)
    |> downcase_email()
    |> Repo.insert()
  end

  def update_user(user, attrs) do
    user
    |> User.changeset(attrs)
    |> downcase_email()
    |> Repo.update()
  end

  defp downcase_email(changeset) do
    email = get_field(changeset, :email)
    put_change(changeset, :email, String.downcase(email))
  end
end

そして、User 構造体は MyApp.Account.User 構造体に改名し、そのソースコードを次のように書き換えます。

defmodule MyApp.Account.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(user, attrs) do
    cast(user, attrs, [:email])
  end
end

MyApp.Account モジュールと MyApp.Account.User モジュールのパスは次の通りです:

  • my_app/lib/my_app/account/account.ex
  • my_app/lib/my_app/account/account/user.ex

使用例は次のようになります。

{:ok, u} = MyApp.Account.create_user(%{email: "ALICE@EXAMPLE.COM"})
{:ok, u} = MyApp.Account.update_user(%{email: "BOB@example.com"})

Elixir/Phoenix の世界では MyApp.Account のようなモジュールをコンテキストと呼びます。コンテキストには、Web システムの様々な機能(ビジネスロジック)を実現する関数群を集めます。そして、構造体モジュールには、フィールドの定義、外部から送られてきたパラメータのフィルタリング、バリデーションだけを残します。

コントローラから構造体モジュールの関数を呼ぶのは正しい作法ではありません。MyApp.Account.User モジュールの関数 changeset/2 の定義の直前に @doc false というアノテーションが付加されています。これは、この関数はパブリックではあるけれども、コンテキストを経由せずに直接呼び出すべきではない、という意味になります。

こういった事情があるので、Elixir/Phoenix による Web システム開発において、Ecto.Changeset モジュールの関数 prepare_changes/2 はあまり使われません。

Rails の ActiveRecord では、モデルクラスに多くの機能を集約します。Web システム開発の初期段階においては、その方が有利です。ディレクトリ構造が単純になり、ソースコードの個数が減るので。しかし、開発が進むにつれてモデルクラスの役割が肥大化し、管理しづらくなります。Elixir/Phoenix では、コンテキストと構造体モジュールを分離することにより、その問題を回避しています。

次回は「バリデーション」の観点から ActiveRecord と Ecto を比較する予定です。

Discussion