👨‍💻

【多言語比較】型安全なENUM定義でマイグレーションを楽にするパターン

に公開

はじめに

PostgreSQLでENUM型を使っていると、こんな状況に遭遇しませんか?

# アプリケーションコードでENUMを追加
class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    COMPLETED = "completed"  # ← 新しく追加

# マイグレーションファイルも別途更新が必要
def upgrade():
    op.execute("ALTER TYPE status ADD VALUE 'completed'")  # ← 手動で追加

ENUMの値を追加するたびに、アプリケーションコードとマイグレーションの両方を更新しなければならず、同期漏れが発生しやすい状態になります。

この記事では、ENUM定義を1箇所にまとめ、マイグレーションを自動化するパターンを紹介します。
Python/Laravel/Rails/TypeORMの4つの環境で実装例を示すので、使っている言語に合わせて参考にしてもらえると嬉しいです。

このパターンで得られる3つのメリット

  1. 同期漏れがなくなる
    • ENUM定義を1箇所に書くだけで、マイグレーションが自動的に追従
  2. レビューが楽になる
    • 確認すべき場所が1箇所だけになり、タイプミスや更新漏れを防げる
  3. 型安全性が向上
    • アプリケーションコード全体でENUM値の補完とチェックが効く

この記事はこんな人におすすめ

  • ENUMの追加・変更が頻繁に発生するプロジェクトで開発している
  • アプリケーションコードとマイグレーションの同期漏れに悩んでいる
  • チーム開発で型安全性保守性を向上させたい
  • 複数の言語を使う環境で統一的なパターンを知りたい
  • PostgreSQLのENUM型を使ったマイグレーション戦略を学びたい

なぜENUM管理が面倒なのか

従来の方法では、ENUMを追加するたびにアプリケーションコードとマイグレーションの両方を更新する必要がありました。

# 1. アプリケーションコードを更新
class Status(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    COMPLETED = "completed"  # ← 追加

# 2. マイグレーションファイルを別途作成
def upgrade():
    op.execute("ALTER TYPE status ADD VALUE 'completed'")  # ← これも書く

レビュー時に片方の更新を見落としたり、タイプミスで値が食い違ったりといった問題がよく起こります。
また、PostgreSQLのENUM型は値の削除が難しく、順序変更には型の再作成が必要になるなど、マイグレーション自体も煩雑です。

以下のようにすると、かなりスマートな実装になると思います。

# ENUM定義を1箇所だけ変更
class Status(BaseEnum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    COMPLETED = "completed"  # ← ここだけ追加

# マイグレーションは自動的に対応
status_enum = Status.sa_enum_type()
status_enum.create(op.get_bind(), checkfirst=True)  # ← 自動で最新の値を使う

この仕組みを、各言語で実装した例を書いていきます。

Python (SQLAlchemy + Alembic) での実装

基底クラスの定義

app/enums/base_enum.py
from enum import Enum
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM

class BaseEnum(str, Enum):
    """
    Enum基底クラス
    
    SQLAlchemyとの統合を提供します。
    """
    
    @classmethod
    def sa_enum_type(cls) -> PG_ENUM:
        """
        SQLAlchemy Enum型を取得します。
        
        Returns:
            Enum型
        """
        return PG_ENUM(
            *[e.value for e in cls],
            name=cls.sa_enum_name(),
            create_type=False,
            checkfirst=True,
        )
    
    @classmethod
    def sa_enum_name(cls) -> str:
        """
        SQLAlchemy Enum名を取得します。
        
        Returns:
            Enum名
        
        Raises:
            NotImplementedError: サブクラスで実装されていない場合
        """
        raise NotImplementedError("Subclasses must implement sa_enum_name")

ポイント解説

  1. sa_enum_type() メソッド

    • Enum値のリストを自動的に取得 (*[e.value for e in cls])
    • create_type=False: マイグレーションで明示的に作成
    • checkfirst=True: 既存の型をチェック
  2. sa_enum_name() メソッド

    • サブクラスで実装を強制
    • PostgreSQLの型名を定義

ENUM定義

app/enums/post_status.py
from app.enums.base_enum import BaseEnum

class PostStatus(BaseEnum):
    """
    記事の公開ステータス
    """
    DRAFT = "draft"          # 下書き
    PUBLISHED = "published"  # 公開中
    ARCHIVED = "archived"    # アーカイブ済み
    
    @classmethod
    def sa_enum_name(cls) -> str:
        return "poststatus"
app/enums/user_role.py
from app.enums.base_enum import BaseEnum

class UserRole(BaseEnum):
    """
    ユーザーの権限ロール
    """
    ADMIN = "admin"      # 管理者
    EDITOR = "editor"    # 編集者
    VIEWER = "viewer"    # 閲覧者
    
    @classmethod
    def sa_enum_name(cls) -> str:
        return "userrole"

マイグレーションファイル

alembic/versions/xxxx_create_posts_table.py
"""create_posts_table"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from app.enums import PostStatus, UserRole

# revision identifiers
revision: str = "e318dcbdd5bc"
down_revision: Union[str, None] = "99c734664219"

# Enum型オブジェクトを取得
post_status_enum = PostStatus.sa_enum_type()
user_role_enum = UserRole.sa_enum_type()

def upgrade() -> None:
    # Enum型を作成
    post_status_enum.create(op.get_bind(), checkfirst=True)
    user_role_enum.create(op.get_bind(), checkfirst=True)
    
    # ユーザーテーブル
    op.create_table(
        "users",
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("name", sa.String(100), nullable=False),
        sa.Column("email", sa.String(255), nullable=False, unique=True),
        sa.Column(
            "role",
            user_role_enum,
            nullable=False,
            server_default="viewer",
            comment="ユーザーロール(admin/editor/viewer)",
        ),
        sa.Column("created_at", sa.DateTime(), nullable=False),
        sa.PrimaryKeyConstraint("id"),
    )
    
    # 記事テーブル
    op.create_table(
        "posts",
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("title", sa.String(200), nullable=False),
        sa.Column("content", sa.Text(), nullable=False),
        sa.Column("author_id", sa.Integer(), nullable=False),
        sa.Column(
            "status",
            post_status_enum,
            nullable=False,
            server_default="draft",
            comment="公開ステータス(draft/published/archived)",
        ),
        sa.Column("created_at", sa.DateTime(), nullable=False),
        sa.Column("published_at", sa.DateTime(), nullable=True),
        sa.PrimaryKeyConstraint("id"),
        sa.ForeignKeyConstraint(["author_id"], ["users.id"]),
    )

def downgrade() -> None:
    op.drop_table("posts")
    op.drop_table("users")
    
    # Enum型を削除
    post_status_enum.drop(op.get_bind(), checkfirst=True)
    user_role_enum.drop(op.get_bind(), checkfirst=True)

ENUM定義をクラスとして管理することで、以下が実現できました。

  • ENUM値の追加・削除がマイグレーションに自動反映される
  • コード内でENUM値が明確になる
  • downgrade() でのロールバックにも簡単に対応できる

Laravel (PHP) での実装

Laravelでも、同じパターンを実現できます。

共通処理の抽出

まず、ENUM型の作成・削除処理を共通化します。

database/migrations/Concerns/ManagesEnumTypes.php
<?php

namespace Database\Migrations\Concerns;

use Illuminate\Support\Facades\DB;

trait ManagesEnumTypes
{
    /**
     * ENUM型を作成
     */
    protected function createEnumType(string $enumClass): void
    {
        $typeName = $enumClass::typeName();
        $values = $enumClass::values();
        $valuesList = "'" . implode("', '", $values) . "'";
        
        DB::statement("
            DO $$
            BEGIN
                IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '{$typeName}') THEN
                    CREATE TYPE {$typeName} AS ENUM ({$valuesList});
                END IF;
            END
            $$;
        ");
    }
    
    /**
     * ENUM型を削除
     */
    protected function dropEnumType(string $enumClass): void
    {
        $typeName = $enumClass::typeName();
        DB::statement("DROP TYPE IF EXISTS {$typeName}");
    }
}

ENUM定義(PHP 8.1+)

app/Enums/PostStatus.php
<?php

namespace App\Enums;

enum PostStatus: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
    
    /**
     * すべての値を配列で取得
     */
    public static function values(): array
    {
        return array_map(fn($case) => $case->value, self::cases());
    }
    
    /**
     * ENUM型名を取得
     */
    public static function typeName(): string
    {
        return 'poststatus';
    }
}
app/Enums/UserRole.php
<?php

namespace App\Enums;

enum UserRole: string
{
    case ADMIN = 'admin';
    case EDITOR = 'editor';
    case VIEWER = 'viewer';
    
    public static function values(): array
    {
        return array_map(fn($case) => $case->value, self::cases());
    }
    
    public static function typeName(): string
    {
        return 'userrole';
    }
}

マイグレーション

database/migrations/2025_10_23_065747_create_posts_table.php
<?php

use App\Enums\PostStatus;
use App\Enums\UserRole;
use Database\Migrations\Concerns\ManagesEnumTypes;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    use ManagesEnumTypes;  // トレイトを使用
    
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        // ENUM型を作成
        $this->createEnumType(PostStatus::class);
        $this->createEnumType(UserRole::class);
        
        // ユーザーテーブル
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name', 100);
            $table->string('email', 255)->unique();
            $table->addColumn('role', UserRole::typeName())
                  ->default('viewer')
                  ->comment('ユーザーロール(admin/editor/viewer)');
            $table->timestamps();
        });
        
        // 記事テーブル
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title', 200);
            $table->text('content');
            $table->foreignId('author_id')->constrained('users');
            $table->addColumn('status', PostStatus::typeName())
                  ->default('draft')
                  ->comment('公開ステータス(draft/published/archived)');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
        Schema::dropIfExists('users');
        
        // ENUM型を削除
        $this->dropEnumType(PostStatus::class);
        $this->dropEnumType(UserRole::class);
    }
};

モデルでの使用

app/Models/Post.php
<?php

namespace App\Models;

use App\Enums\PostStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    protected $fillable = [
        'title',
        'content',
        'author_id',
        'status',
        'published_at',
    ];
    
    /**
     * キャスト定義
     */
    protected $casts = [
        'status' => PostStatus::class,
        'published_at' => 'datetime',
    ];
    
    /**
     * 著者との関連
     */
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'author_id');
    }
    
    /**
     * 公開済みかどうか
     */
    public function isPublished(): bool
    {
        return $this->status === PostStatus::PUBLISHED;
    }
}
app/Models/User.php
<?php

namespace App\Models;

use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'role',
    ];
    
    /**
     * キャスト定義
     */
    protected $casts = [
        'role' => UserRole::class,
    ];
    
    /**
     * 記事との関連
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class, 'author_id');
    }
    
    /**
     * 管理者かどうか
     */
    public function isAdmin(): bool
    {
        return $this->role === UserRole::ADMIN;
    }
}

Rails (Ruby) での実装

RailsではActiveRecord::Enumを活用して、型安全なENUM管理を実現します。

共通処理の抽出

lib/migration_helpers/enum_type_helper.rb
# frozen_string_literal: true

module MigrationHelpers
  module EnumTypeHelper
    def create_enum_type(enum_class)
      type_name = enum_class::TYPE_NAME
      values = enum_class::VALUES.keys.map { |v| "'#{v}'" }.join(', ')
      
      execute <<-SQL
        DO $$
        BEGIN
          IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '#{type_name}') THEN
            CREATE TYPE #{type_name} AS ENUM (#{values});
          END IF;
        END
        $$;
      SQL
    end
    
    def drop_enum_type(enum_class)
      type_name = enum_class::TYPE_NAME
      execute "DROP TYPE IF EXISTS #{type_name}"
    end
  end
end
config/application.rb
# マイグレーションヘルパーを自動読み込み
config.autoload_paths += %W[#{config.root}/lib]

# マイグレーションにヘルパーをインクルード
ActiveRecord::Migration.include(MigrationHelpers::EnumTypeHelper)

ENUM定義

PostgreSQL用のENUM値定義とActiveRecord::Enumの設定を1箇所にまとめます。

app/enums/post_status.rb
# frozen_string_literal: true

module PostStatus
  # PostgreSQL ENUM型用の定義
  TYPE_NAME = 'poststatus'
  VALUES = {
    draft: 'draft',
    published: 'published',
    archived: 'archived'
  }.freeze
end
app/enums/user_role.rb
# frozen_string_literal: true

module UserRole
  # PostgreSQL ENUM型用の定義
  TYPE_NAME = 'userrole'
  VALUES = {
    admin: 'admin',
    editor: 'editor',
    viewer: 'viewer'
  }.freeze
end

マイグレーション

db/migrate/20251023065747_create_posts.rb
# frozen_string_literal: true

class CreatePosts < ActiveRecord::Migration[7.0]
  def up
    # ENUM型を作成(ヘルパーメソッドを使用)
    create_enum_type(PostStatus)
    create_enum_type(UserRole)
    
    # ユーザーテーブル
    create_table :users do |t|
      t.string :name, limit: 100, null: false
      t.string :email, limit: 255, null: false, index: { unique: true }
      t.column :role, UserRole::TYPE_NAME, null: false, default: 'viewer', comment: 'ユーザーロール(admin/editor/viewer)'
      
      t.timestamps
    end
    
    # 記事テーブル
    create_table :posts do |t|
      t.string :title, limit: 200, null: false
      t.text :content, null: false
      t.references :author, null: false, foreign_key: { to_table: :users }
      t.column :status, PostStatus::TYPE_NAME, null: false, default: 'draft', comment: '公開ステータス(draft/published/archived)'
      t.timestamp :published_at
      
      t.timestamps
    end
  end

  def down
    drop_table :posts
    drop_table :users
    
    # ENUM型を削除
    drop_enum_type(PostStatus)
    drop_enum_type(UserRole)
  end
end

モデルでの使用

ActiveRecord::Enumを使うことで、便利なヘルパーメソッドが自動生成されます。

app/models/user.rb
# frozen_string_literal: true

class User < ApplicationRecord
  # ActiveRecord::Enumを使用(ENUM定義を参照)
  enum role: UserRole::VALUES
  
  has_many :posts, foreign_key: :author_id
  
  validates :email, presence: true, uniqueness: true
  validates :name, presence: true
end

# 使い方
user = User.new(name: 'Alice', email: 'alice@example.com', role: :admin)

# 判定メソッド(自動生成)
user.admin?    # => true
user.editor?   # => false
user.viewer?   # => false

# ステータス変更メソッド(自動生成)
user.editor!   # role を 'editor' に変更
user.viewer!   # role を 'viewer' に変更

# スコープ(自動生成)
User.admin     # 管理者ユーザーを取得
User.editor    # 編集者ユーザーを取得
app/models/post.rb
# frozen_string_literal: true

class Post < ApplicationRecord
  # ActiveRecord::Enumを使用(ENUM定義を参照)
  enum status: PostStatus::VALUES
  
  belongs_to :author, class_name: 'User'
  
  validates :title, presence: true
  validates :content, presence: true
  
  # カスタムメソッド
  def publish!
    published!  # statusを'published'に変更(ActiveRecord::Enumのメソッド)
    update!(published_at: Time.current)
  end
end

# 使い方
post = Post.new(title: 'Hello', content: 'World', author: user, status: :draft)

# 判定メソッド(自動生成)
post.draft?      # => true
post.published?  # => false
post.archived?   # => false

# ステータス変更メソッド(自動生成)
post.published!  # status を 'published' に変更
post.archived!   # status を 'archived' に変更

# スコープ(自動生成)
Post.draft       # 下書き記事を取得
Post.published   # 公開記事を取得
Post.archived    # アーカイブ記事を取得

ActiveRecord::Enumの利点

  • 判定メソッド自動生成: draft?, published? などが自動で使える
  • 変更メソッド自動生成: draft!, published! で状態変更可能
  • スコープ自動生成: Post.published で公開記事を取得
  • バリデーション: 不正な値の代入を防ぐ
  • 型安全性: ENUMの値がコード補完で表示される

TypeORM (TypeScript) での実装

TypeScriptでも、型安全なENUM管理が可能です。

共通処理の抽出

src/migrations/helpers/EnumTypeHelper.ts
import { QueryRunner } from 'typeorm';

export class EnumTypeHelper {
  /**
   * ENUM型を作成
   */
  static async createEnumType(
    queryRunner: QueryRunner,
    meta: { typeName: string; values: string[] },
  ): Promise<void> {
    const valuesList = meta.values.map((v) => `'${v}'`).join(', ');
    await queryRunner.query(`
      DO $$
      BEGIN
        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '${meta.typeName}') THEN
          CREATE TYPE ${meta.typeName} AS ENUM (${valuesList});
        END IF;
      END
      $$;
    `);
  }

  /**
   * ENUM型を削除
   */
  static async dropEnumType(
    queryRunner: QueryRunner,
    meta: { typeName: string },
  ): Promise<void> {
    await queryRunner.query(`DROP TYPE IF EXISTS ${meta.typeName}`);
  }
}

ENUM定義

src/enums/PostStatus.ts
export enum PostStatus {
  DRAFT = 'draft',
  PUBLISHED = 'published',
  ARCHIVED = 'archived',
}

export const PostStatusMeta = {
  typeName: 'poststatus',
  values: Object.values(PostStatus),
};
src/enums/UserRole.ts
export enum UserRole {
  ADMIN = 'admin',
  EDITOR = 'editor',
  VIEWER = 'viewer',
}

export const UserRoleMeta = {
  typeName: 'userrole',
  values: Object.values(UserRole),
};

マイグレーション

src/migrations/1698073847000-CreatePosts.ts
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
import { PostStatusMeta, PostStatus } from '../enums/PostStatus';
import { UserRoleMeta, UserRole } from '../enums/UserRole';
import { EnumTypeHelper } from './helpers/EnumTypeHelper';

export class CreatePosts1698073847000 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    // ENUM型を作成(ヘルパークラスを使用)
    await EnumTypeHelper.createEnumType(queryRunner, PostStatusMeta);
    await EnumTypeHelper.createEnumType(queryRunner, UserRoleMeta);

    // ユーザーテーブル
    await queryRunner.createTable(
      new Table({
        name: 'users',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'name',
            type: 'varchar',
            length: '100',
            isNullable: false,
          },
          {
            name: 'email',
            type: 'varchar',
            length: '255',
            isNullable: false,
            isUnique: true,
          },
          {
            name: 'role',
            type: UserRoleMeta.typeName,
            isNullable: false,
            default: "'viewer'",
            comment: 'ユーザーロール(admin/editor/viewer)',
          },
          {
            name: 'created_at',
            type: 'timestamp',
            default: 'now()',
          },
          {
            name: 'updated_at',
            type: 'timestamp',
            default: 'now()',
          },
        ],
      }),
      true,
    );

    // 記事テーブル
    await queryRunner.createTable(
      new Table({
        name: 'posts',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'title',
            type: 'varchar',
            length: '200',
            isNullable: false,
          },
          {
            name: 'content',
            type: 'text',
            isNullable: false,
          },
          {
            name: 'author_id',
            type: 'int',
            isNullable: false,
          },
          {
            name: 'status',
            type: PostStatusMeta.typeName,
            isNullable: false,
            default: "'draft'",
            comment: '公開ステータス(draft/published/archived)',
          },
          {
            name: 'published_at',
            type: 'timestamp',
            isNullable: true,
          },
          {
            name: 'created_at',
            type: 'timestamp',
            default: 'now()',
          },
          {
            name: 'updated_at',
            type: 'timestamp',
            default: 'now()',
          },
        ],
      }),
      true,
    );

    // 外部キー制約
    await queryRunner.createForeignKey(
      'posts',
      new TableForeignKey({
        columnNames: ['author_id'],
        referencedColumnNames: ['id'],
        referencedTableName: 'users',
        onDelete: 'CASCADE',
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('posts');
    await queryRunner.dropTable('users');
    
    // ENUM型を削除
    await EnumTypeHelper.dropEnumType(queryRunner, PostStatusMeta);
    await EnumTypeHelper.dropEnumType(queryRunner, UserRoleMeta);
  }
}

エンティティでの使用

src/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { UserRole } from '../enums/UserRole';
import { Post } from './Post';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: 'varchar', length: 100 })
  name!: string;

  @Column({ type: 'varchar', length: 255, unique: true })
  email!: string;

  @Column({
    type: 'enum',
    enum: UserRole,
    default: UserRole.VIEWER,
    comment: 'ユーザーロール(admin/editor/viewer)',
  })
  role!: UserRole;

  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;

  // ヘルパーメソッド
  isAdmin(): boolean {
    return this.role === UserRole.ADMIN;
  }

  isEditor(): boolean {
    return this.role === UserRole.EDITOR;
  }

  isViewer(): boolean {
    return this.role === UserRole.VIEWER;
  }
}
src/entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { PostStatus } from '../enums/PostStatus';
import { User } from './User';

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: 'varchar', length: 200 })
  title!: string;

  @Column({ type: 'text' })
  content!: string;

  @Column({ name: 'author_id' })
  authorId!: number;

  @ManyToOne(() => User, (user) => user.posts)
  @JoinColumn({ name: 'author_id' })
  author!: User;

  @Column({
    type: 'enum',
    enum: PostStatus,
    default: PostStatus.DRAFT,
    comment: '公開ステータス(draft/published/archived)',
  })
  status!: PostStatus;

  @Column({ name: 'published_at', type: 'timestamp', nullable: true })
  publishedAt?: Date;

  @CreateDateColumn({ name: 'created_at' })
  createdAt!: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt!: Date;

  // ヘルパーメソッド
  isDraft(): boolean {
    return this.status === PostStatus.DRAFT;
  }

  isPublished(): boolean {
    return this.status === PostStatus.PUBLISHED;
  }

  isArchived(): boolean {
    return this.status === PostStatus.ARCHIVED;
  }

  // ステータス変更メソッド
  publish(): void {
    this.status = PostStatus.PUBLISHED;
    this.publishedAt = new Date();
  }

  archive(): void {
    this.status = PostStatus.ARCHIVED;
  }
}

各言語の実装比較

特徴 Python (SQLAlchemy) Laravel (PHP) Rails (Ruby) TypeORM (TypeScript)
ENUM定義 class BaseEnum(str, Enum) enum PostStatus: string module PostStatus::VALUES enum PostStatus
値の取得 [e.value for e in cls] array_map(fn($case) => $case->value, self::cases()) VALUES.keys Object.values(PostStatus)
型安全性 ✅ 強い ✅ 強い(PHP 8.1+) ✅ 強い(ActiveRecord::Enum) ✅ 強い
共通処理 基底クラス Trait Module ヘルパークラス
マイグレーション sa_enum_type().create() $this->createEnumType() create_enum_type() EnumTypeHelper.createEnumType()
モデル統合 Column(enum_type) $casts enum status: VALUES @Column({ type: 'enum' })

共通パターン

すべての実装で共通するパターンは以下の通りです。

  1. ENUM定義の一元化

    • ENUM値とDB型名を1箇所で管理
    • values()typeName() メソッドを提供
  2. 共通処理の抽出

    • 型作成・削除の処理を共通化
    • 各マイグレーションファイルでコードの重複を排除
    • Python: 基底クラス、Laravel: Trait、Rails: Module、TypeORM: ヘルパークラス
  3. マイグレーションの自動化

    • ENUM定義から型を自動生成
    • checkfirst=True / IF NOT EXISTS で冪等性を確保
  4. 型安全性の確保

    • アプリケーションコードで型チェック
    • IDEの補完機能を活用

まとめ

ENUM定義を一元化することで、アプリケーションコードとマイグレーションの二重管理から解放されます。
Python/Laravel/Rails/TypeORMのどれを使っていても、基本的なアプローチは同じです。

  • ENUM定義を1箇所にまとめる
  • 共通処理を抽出して再利用する
  • マイグレーションで自動的に型を生成する

これにより、レビュー漏れや同期ミスを防ぎ、保守性の高いコードベースを実現できます。

Discussion