Ent ORM: `SELECT ... FOR UPDATE SKIP LOCKED` を実装する方法

に公開

はじめに

Ent ORMSELECT ... FOR UPDATE SKIP LOCKED を実装しようとしたところ、デフォルトでは同機能が有効になっていなかった(Ent が生成するクエリーに同機能が含まれていない)ので、やり方を備忘録として残します。

この機能は、並行処理においてデッドロックを回避しながら効率的にレコードを処理するために有用です。

SKIP LOCKED オプション自体の詳細についてはこちら

必要な機能フラグ

SELECT ... FOR UPDATE SKIP LOCKED を実装するために必要な機能フラグについて:

sql/lock 機能(必須)

この機能により、行レベルロックの機能が追加されます。

提供される機能:

  • ForUpdate() / ForShare() メソッド: クエリビルダーに ForUpdate()ForShare() メソッドを追加
  • ロックオプション: sql.LockOption インターフェースを通じた SkipLocked などのロックオプションのサポート
  • 適切なSQL生成: 行レベルロック用の適切なSQLを生成

この機能は SELECT ... FOR UPDATE SKIP LOCKED の実装に必須です。

sql/execquery 機能(オプション)

この機能は直接SQL実行機能を提供しますが、現在の実装では必須ではありません:

提供される機能:

  • QueryContext() メソッド: クライアント設定に直接SQLクエリ実行を可能にする QueryContext() メソッドを追加
  • Raw SQL実行: 高度なデータベース操作に必要な生のSQL実行機能

現在の実装ではEnt のクエリビルダーを使用しているため厳密には不要ですが、将来的な拡張性のために含めることができます。

機能の有効化

最小限の構成では sql/lock 機能のみを有効にします:

ent/generate.go
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --target ../entgen --feature sql/lock ./schema/...

将来的な拡張性を考慮する場合は、両方の機能を有効にできます:

ent/generate.go
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --target ../entgen --feature sql/lock --feature sql/execquery ./schema/...

これでコードを再生成すると必要な機能が含まれたコードが生成されます。

実装例

※処理の順序を問わない場合に利用可:

func processBatchJobs(ctx context.Context, client *ent.Client, limit int) error {
    // SELECT ... FOR UPDATE SKIP LOCKED を使用してジョブを取得
    pendingJobs, err := client.BatchJob.Query().
        Where(batchjob.Status("pending")).
        Order(
            entgen.Desc(batchjob.FieldPriority),    // 優先度の高い順
            entgen.Asc(batchjob.FieldCreatedAt),    // 同優先度では古い順
        ).
        Limit(limit).
        ForUpdate(                                  // sql/lock 機能により提供
            sql.WithLockAction(sql.SkipLocked),     // SkipLocked オプション
        ).
        All(ctx)

    if err != nil {
        return fmt.Errorf("failed to fetch pending jobs: %w", err)
    }

    // 取得したジョブを処理(各ジョブは独立)
    for _, job := range pendingJobs {
        // ...
        }
    }

    return nil
}

Discussion