Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

ActiveRecord vs Ecto 徹底比較(前編):マイグレーションとデータマッピングの違い

データは、ほとんどのソフトウェアアプリケーションの中核を担う要素です。データベースからデータをマッピングし、クエリを実行することは、開発者の日常業務で繰り返し発生するタスクです。だからこそ、そのプロセスを深く理解し、作業を簡素化してくれる抽象化ツールを使いこなせることが重要になります。

本記事は2部構成シリーズの第1回目として、RubyのActiveRecordとElixirのEctoを比較します。両ツールが、開発者によるデータベーススキーマのマイグレーションやマッピングをどのように支援しているのかを見ていきましょう。

これはいわば「リンゴとオレンジの比較」です。一言も発する必要のなかった初代バットガールと、「俺はバットマンだ」と明言するバットマン。暗黙的で「設定より規約(CoC)」なアプローチと、明示的な意図の表明。さあ、第1ラウンド、ファイト!

ActiveRecordとは

リリースから10年以上が経過したActiveRecordは、Ruby on Railsプロジェクトにデフォルトで同梱されている有名なORM(Object-Relational Mapping)です。

ActiveRecordはMVCにおける「M」、すなわちモデルに相当し、ビジネスデータとロジックを表現するシステムのレイヤーを担当します。ActiveRecordは、データベースへの永続的な保存が必要なビジネスオブジェクトの作成と利用を容易にします。これはActiveRecordパターンの実装であり、このパターン自体がオブジェクトリレーショナルマッピングシステムの一種の記述となっています。

Railsとともに使われるイメージが強いですが、ActiveRecordはスタンドアロンツールとして単体で利用し、他のプロジェクトに組み込むことも可能です。

Ectoとは

ActiveRecordと比べると、Ectoはかなり新しい(そして現時点ではまだ有名ではない)ツールです。Elixirで書かれており、Phoenixプロジェクトにはデフォルトで含まれています。

ActiveRecordとは異なり、EctoはORMではありません。Elixirを使ってクエリを記述し、データベースとやり取りできるようにするライブラリです。

Ectoは、Elixirでクエリを書き、データベースと対話するためのドメイン固有言語(DSL)です。

設計上、Ectoはスタンドアロンツールであり、さまざまなElixirプロジェクトで利用され、特定のフレームワークに結びついていません。

リンゴとオレンジを比較しているのでは?

その通りです!ActiveRecordとEctoは意味論的に異なる存在ですが、データベースマイグレーション、データベースマッピング、クエリ、バリデーションといった共通機能は両方でサポートされています。つまり、どちらのツールを使っても同じ結果を得ることができます。Ruby出身でElixirに関心を持つ開発者にとって、この比較はきっと参考になるはずです。

例題:請求書システム

以降のセクションでは、架空の請求書システムを使って説明します。スーパーヒーロー向けにスーツを販売する店舗を想像してみてください。話をシンプルにするため、請求書システムにはusers(ユーザー)とinvoices(請求書)の2つのテーブルだけを用意します。

以下は、各テーブルのフィールドと型の構造です。

usersテーブル

フィールド
full_namestring
emailstring
created_at(ActiveRecord)/ inserted_at(Ecto)datetime
updated_atdatetime

invoicesテーブル

フィールド
user_idinteger
payment_methodstring
paid_atdatetime
created_at(ActiveRecord)/ inserted_at(Ecto)datetime
updated_atdatetime

usersテーブルには4つのフィールドがあります:full_nameemailupdated_at、そして使用するツールによって名前が変わる4つ目のフィールドです。ActiveRecordはcreated_atフィールドを生成し、Ectoはinserted_atフィールドを生成します。どちらも、レコードが最初にデータベースへ挿入された時点のタイムスタンプを表します。

2つ目のテーブルはinvoicesです。user_idpayment_methodpaid_atupdated_atの5つのフィールドを持ち、usersテーブルと同様に、使用するツールに応じてcreated_atまたはinserted_atのいずれかが作られます。

usersテーブルとinvoicesテーブルには、次のような関連付けがあります。

  • ユーザー(user)は複数の請求書(invoices)を持つ(has many)
  • 請求書(invoice)は1人のユーザー(user)に属する(belongs to)

マイグレーション

マイグレーションを使うと、反復的なプロセスを通じてデータベーススキーマを簡単に進化させられます。ActiveRecordとEctoはどちらも、SQLを直接扱う代わりに高水準言語(RubyとElixir)でスキーマのマイグレーションを行えるようにしています。

実際にusersテーブルとinvoicesテーブルを作成しながら、ActiveRecordとEctoそれぞれでマイグレーションがどう動くのかを見ていきましょう。

ActiveRecord:usersテーブルの作成

マイグレーション

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :full_name, null: false
      t.string :email, index: {unique: true}, null: false
      t.timestamps
    end
  end
end

ActiveRecordのマイグレーションでは、create_tableメソッドを使ってテーブルを作成します。マイグレーションファイル内でcreated_atupdated_atフィールドを明示的に定義していませんが、t.timestampsを記述するだけで、ActiveRecordが両方のカラムを自動生成してくれます。

作成されたテーブル構造

CreateUsersマイグレーションを実行すると、次の構造のテーブルが作成されます。

   Column   |            Type             | Nullable |              Default
------------+-----------------------------+----------+-----------------------------------
 id         | bigint                      | not null | nextval('users_id_seq'::regclass)
 full_name  | character varying           | not null |
 email      | character varying           | not null |
 created_at | timestamp without time zone | not null |
 updated_at | timestamp without time zone | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "index_users_on_email" UNIQUE, btree (email)

このマイグレーションは、emailフィールドに対するユニークインデックスの作成も担っています。emailフィールドの定義にindex: {unique: true}というオプションを渡しているため、テーブル構造に"index_users_on_email" UNIQUE, btree (email)というインデックスが含まれているわけです。

Ecto:usersテーブルの作成

マイグレーション

defmodule Financex.Repo.Migrations.CreateUsers do
  use Ecto.Migration
 
  def change do
    create table(:users) do
      add :full_name, :string, null: false
      add :email, :string, null: false
      timestamps()
    end
 
    create index(:users, [:email], unique: true)
  end
end

Ectoのマイグレーションでは、create()関数とtable()関数を組み合わせてusersテーブルを作成します。マイグレーションファイルはActiveRecord版とかなり似ていますね。タイムスタンプ系のカラムについては、ActiveRecordではt.timestampscreated_atupdated_atを生成するのに対し、Ectoではtimestamps()関数がinserted_atupdated_atを生成します。

インデックスの作成方法には、両者に小さな違いがあります。ActiveRecordではインデックスをフィールド定義のオプションとして指定しますが、Ectoではcreate()index()関数を組み合わせて実現します。これはテーブル作成時の関数の組み合わせ方と一貫した設計になっています。

作成されたテーブル構造

   Column    |            Type             | Nullable |              Default
-------------+-----------------------------+----------+-----------------------------------
 id          | bigint                      | not null | nextval('users_id_seq'::regclass)
 full_name   | character varying(255)      | not null |
 email       | character varying(255)      | not null |
 inserted_at | timestamp without time zone | not null |
 updated_at  | timestamp without time zone | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_index" UNIQUE, btree (email)

Financex.Repo.Migrations.CreateUsersマイグレーションを実行して作成されるテーブルは、ActiveRecordで作成したテーブルと同一の構造になります。

ActiveRecord:invoicesテーブルの作成

マイグレーション

class CreateInvoices < ActiveRecord::Migration[5.2]
  def change
    create_table :invoices do |t|
      t.references :user
      t.string :payment_method
      t.datetime :paid_at
      t.timestamps
    end
  end
end

このマイグレーションには、前の例にはなかったt.referencesメソッドが登場します。これはusersテーブルへの参照を作成するためのものです。先ほど述べたように、ユーザーは複数の請求書を持ち、請求書は1人のユーザーに属します。t.referencesメソッドは、invoicesテーブルにその参照を保持するためのuser_idカラムを作成します。

作成されたテーブル構造

     Column     |            Type             | Nullable |               Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint                      | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint                      |          |
 payment_method | character varying           |          |
 paid_at        | timestamp without time zone |          |
 created_at     | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |
Indexes:
    "invoices_pkey" PRIMARY KEY, btree (id)
    "index_invoices_on_user_id" btree (user_id)

作成されたテーブルは、先ほどのテーブルと同じパターンに従っています。唯一の違いは、追加のインデックス(index_invoices_on_user_id)が存在することです。これはt.referencesメソッドを使うと、ActiveRecordが自動的にインデックスを追加してくれるためです。

Ecto:invoicesテーブルの作成

マイグレーション

defmodule Financex.Repo.Migrations.CreateInvoices do
  use Ecto.Migration
 
  def change do
    create table(:invoices) do
      add :user_id, references(:users)
      add :payment_method, :string
      add :paid_at, :utc_datetime
      timestamps()
    end
 
    create index(:invoices, [:user_id])
  end
end

Ectoもreferences()関数を使ってデータベース参照の作成をサポートしています。ただし、カラム名を推測してくれるActiveRecordとは異なり、Ectoでは開発者がuser_idというカラム名を明示的に定義する必要があります。また、references()関数では参照先のテーブル(この例ではusersテーブル)も明示的に指定しなければなりません。

作成されたテーブル構造

     Column     |            Type             | Nullable |               Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint                      | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint                      |          |
 payment_method | character varying(255)      |          |
 paid_at        | timestamp without time zone |          |
 inserted_at    | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |

Indexes:
    "invoices_pkey" PRIMARY KEY, btree (id)
    "invoices_user_id_index" btree (user_id)
Foreign-key constraints:
    "invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

どちらのマイグレーションもかなり似ていますが、references機能の扱い方にはいくつか違いがあります。

  1. Ectoはuser_idフィールドに対して外部キー制約("invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id))を作成し、usersテーブルとinvoicesテーブル間の参照整合性を維持します。

  2. ActiveRecordはuser_idカラムのインデックスを自動的に作成しますが、Ectoでは開発者が明示的に宣言する必要があります。そのため、このマイグレーションにはcreate index(:invoices, [:user_id])という文が含まれています。

ActiveRecord:データマッピングと関連付け

ActiveRecordは「設定より規約(convention over configuration)」のスローガンで知られています。デフォルトでは、モデルのクラス名からデータベースのテーブル名を推測します。たとえばUserというクラスは、デフォルトでusersテーブルをデータソースとして使用します。さらにActiveRecordは、テーブルの全カラムをインスタンス属性として自動的にマッピングします。開発者が定義する必要があるのは、テーブル間の関連付けだけです。この関連付け情報もまた、ActiveRecordが関係するクラスやテーブルを推測するために使われます。

ActiveRecordでusersテーブルとinvoicesテーブルをマッピングすると、次のようになります。

users

class User < ApplicationRecord
  has_many :invoices
end

invoices

class Invoice < ApplicationRecord
  belongs_to :user
end

Ecto:データマッピングと関連付け

一方のEctoでは、データソースとそのフィールドについて開発者が明示的に宣言することが求められます。EctoにもActiveRecordと似たhas_manybelongs_toの機能はありますが、それに加えて、関連先のテーブルと、そのテーブルのスキーマを扱うスキーマモジュールを明示的に指定する必要があります。

Ectoでusersテーブルとinvoicesテーブルをマッピングすると、次のようになります。

users

defmodule Financex.Accounts.User do
  use Ecto.Schema
 
  schema "users" do
    field :full_name, :string
    field :email, :string
    has_many :invoices, Financex.Accounts.Invoice
    timestamps()
  end
end

invoices

defmodule Financex.Accounts.Invoice do
  use Ecto.Schema
 
  schema "invoices" do
    field :payment_method, :string
    field :paid_at, :utc_datetime
    belongs_to :user, Financex.Accounts.User
    timestamps()
  end
end

まとめ

今回は、少しのためらいもなくリンゴとオレンジを比較しました。ActiveRecordとEctoがそれぞれどのようにデータベースマイグレーションとマッピングを扱うかを見てきました。それは、暗黙的な初代バットガールと、明示的な「俺はバットマンだ」のバットマンとの戦いだったのです。

「設定より規約」のおかげで、ActiveRecordを使う場合は通常、書くべきコード量が少なくて済みます。一方のEctoは真逆の方向に進み、開発者により明示的な意図の表明を求めます。「少ないコード」が一般に良いことであるだけでなく、ActiveRecordには多くの場面で最適なデフォルトが用意されており、開発者があらゆる事項について決定を下したり、背後にあるすべての設定を理解したりする必要がありません。初心者にとっては、標準的な流儀に厳密に従う限り「十分に良い」判断をデフォルトで行ってくれるActiveRecordの方が適した選択肢と言えるでしょう。

Ectoの明示性は、コードの挙動を読んで理解しやすくする一方で、データベースの特性や利用可能な機能について、開発者により深い理解を要求します。一見すると煩雑に感じられるかもしれないこの特性こそが、実はEctoの美徳なのです。ActiveRecordとEctoの両方の世界での経験から言えば、Ectoの明示性は、ActiveRecordプロジェクトでありがちな「舞台裏」の影響や不確実性を取り除いてくれます。開発者がコードで読んだことが、そのままアプリケーションで起こることであり、暗黙的な振る舞いは一切存在しないのです。

数週間後に公開予定のシリーズ第2回「ActiveRecord vs Ecto」では、両ツールにおけるクエリとバリデーションの仕組みを取り上げます。お楽しみに!

本記事についてのご感想をお聞かせください。私たちは常に新しいトピックを探しています。詳しく知りたいテーマがあれば、@AppSignalまでお気軽にお知らせください!

  1. ER図の最小化:カーディナリティ別に見る最適なテーブル数

    問題の概要ER図(Entity Relationship Diagram)とは、データベースを構成するさまざまなテーブルと、それらの間の関係を視覚的に表現した図のことです。ER図を適切に設計・分析することで、データベースに必要なテーブル数を削減し、効率的なスキーマを構築することができます。1対1のカーディナリティまず、1対1のカーディナリティを持つ以下のER図を見てみましょう。このER図は、次の3つのエンティティで構成されています。従業員(Employee)エンティティ:emp_name という属性を持ち、emp_id が主キーとなります。会社(Company)エンティティ:cmp_name

  2. ChromeでERR_CONNECTION_CLOSEDエラーが出るときの原因と5つの解決策

    Google Chromeは現在もっとも人気のあるインターネットブラウザの一つで、ハイスペックPCでは優れたパフォーマンスを発揮します。低スペックPCでも動作しますが、メモリ消費量が多いため、RAMが3GB未満の環境では特に負担になりがちです。タブを多数開いている場合はなおさらです。ChromeはGoogleによって継続的にアップデートされており、Googleアカウントでログインすれば、すべてのデバイスで履歴などを共有できる同期体験を提供しています。本記事では、この特定のエラーの原因と対処法を詳しく解説します。ChromeにおけるERR_CONNECTION_CLOSEDエラーとはこのエラーは