Start Debugging

EF Core で Oracle のシーケンスを使って主キーを生成する方法

Oracle.EntityFrameworkCore 10 の UseSequence で EF Core のキーを Oracle のシーケンスにマッピングします。プロバイダーが実際に出力する DDL と INSERT の SQL、既存のシーケンスやレガシーなトリガーを使う方法、引用符と HiLo でつまずきやすい理由、そして ORA-00001 を起こさずに ID 列をシーケンスへ移行する方法を解説します。

短い答え: Oracle.EntityFrameworkCore 10.x では、シーケンスを宣言し、プロバイダー独自の UseSequence でキーをそのシーケンスに向けます。modelBuilder.HasSequence<int>("ORDER_SEQ").StartsAt(1000); の後に modelBuilder.Entity<Order>().Property(o => o.Id).UseSequence("ORDER_SEQ"); と書きます。プロバイダーはこれを列の既定値 "Id" NUMBER(10) DEFAULT ("ORDER_SEQ".NEXTVAL) に変換し、INSERT からキーを除外し、RETURNING "Id" INTO で値を読み戻します。ValueGeneratedOnAdd() だけに頼ってはいけません (それで得られるのは ID 列であり、自分のシーケンスではありません)。シーケンス名は Oracle が格納しているとおりの大文字小文字 (通常は大文字) で書き、int/long のキーでは UseHiLo を使わないでください。Oracle 自身の README でサポート対象外とされています。

以下の内容はすべて、EF Core 10 と .NET 10 SDK 10.0.302 上の Oracle.EntityFrameworkCore 10.23.26301 (2026-09-08 公開、アセンブリ 10.0.23.1) で確認しました。この記事を書いたマシンには Oracle Database がないため、掲載している SQL はプロバイダーが生成したものをオフラインで取得したものです。DDL は GenerateCreateScript()、マイグレーションは IMigrationsSqlGenerator、そして SaveChanges のコマンドテキストはネットワークに送られる直前にコマンドインターセプターで取得しました。実行時間や実際のラウンドトリップについては何も主張していません。

Oracle のキーがシーケンスに行き着く理由

Oracle には ID 列 (12c で登場) よりずっと前からシーケンスがありました。そのため、引き継ぐ Oracle スキーマの多くはテーブルごとに 1 つのシーケンス (通常は <TABLE>_SEQ) を使い、NEXTVAL をキーにコピーする BEFORE INSERT トリガーを持っていることもよくあります。新しいスキーマでも、チームが ID 列よりシーケンスを選ぶことがあります。名前付きシーケンスは複数のテーブルで共有でき、他のアプリケーションから SELECT ORDER_SEQ.NEXTVAL FROM DUAL で読み取れ、特別なモードなしで明示的なキー値を受け入れるからです。

EF Core の Oracle プロバイダーはこれらすべてをサポートしていますが、既定の動作は別の方向を向いています。何も設定しなければ、Id という名前の int キーには IdentityColumn 戦略が適用されます。

-- Oracle.EntityFrameworkCore 10.23.26301, default convention for an int key
"Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL,

これは隠れたシステムシーケンス (ISEQ$$_...) に支えられた ID 列であり、自分で名前を付けたシーケンスではありません。DBA から ORDER_SEQ を渡されている場合や、テーブルにすでにトリガーがある場合は、そのことをモデルで明示する必要があります。

10.x プロバイダーの API

Oracle プロバイダーは SQL Server のものとは別に、独自の値生成戦略の列挙型を持っています。10.23.26301 のアセンブリをリフレクションで調べると次のようになります。

OracleValueGenerationStrategy: None, SequenceHiLo, IdentityColumn, Sequence
OracleSQLCompatibility:        DatabaseVersion19, DatabaseVersion21, DatabaseVersion23

シーケンス関連のエントリポイントは次のとおりです。

互換性の列挙型が 19 から始まっている点に注意してください。古いプロバイダーのバージョンは UseOracleSQLCompatibility("11") を受け付け、その場合プロバイダーは生成されるキーごとにシーケンスとトリガーを作成していました。このモードは 10.x プロバイダーではなくなっています。DatabaseVersion19 を設定しても、23 と同じ GENERATED BY DEFAULT ON NULL AS IDENTITY 列が生成されます。シーケンスを使いたい場合は、プロパティごとに指定します。

手順: キーを名前付きシーケンスにマッピングする

  1. EF Core のメジャーバージョンに合ったプロバイダーをインストールします。EF Core 10 なら Oracle.EntityFrameworkCore 10.23.x です。nuspec は Microsoft.EntityFrameworkCore.Relational[10.0.0, 11.0.0) に固定しており、EF Core 11 向けのビルドはまだありません。
  2. HasSequence でシーケンスを宣言し、開始値、増分、上下限をマイグレーションで管理できるようにします。
  3. 同じ名前 (スキーマがあればスキーマも) でキーに UseSequence を呼び出します。
  4. マイグレーションを追加し、適用する前に生成された SQL を読みます。
// .NET 10, EF Core 10, Oracle.EntityFrameworkCore 10.23.26301
using Microsoft.EntityFrameworkCore;

public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
}

public class ShopContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
        options.UseOracle(
            "User Id=app;Password=...;Data Source=dbhost:1521/FREEPDB1",
            o => o.UseOracleSQLCompatibility(OracleSQLCompatibility.DatabaseVersion23));

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasSequence<int>("ORDER_SEQ")
            .StartsAt(1000)
            .IncrementsBy(1);

        modelBuilder.Entity<Order>()
            .Property(o => o.Id)
            .UseSequence("ORDER_SEQ");
    }
}

このモデルに対してプロバイダーが生成する DDL は次のとおりです。

-- Oracle.EntityFrameworkCore 10.23.26301, UseSequence("ORDER_SEQ")
CREATE SEQUENCE "ORDER_SEQ" START WITH 1000 INCREMENT BY 1 NOMINVALUE NOMAXVALUE NOCYCLE

BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE
"Orders" (
    "Id" NUMBER(10) DEFAULT ("ORDER_SEQ".NEXTVAL) NOT NULL,
    "Customer" NVARCHAR2(2000) NOT NULL,
    CONSTRAINT "PK_Orders" PRIMARY KEY ("Id")
)';
END;

シーケンスは単なる列の既定値になります。これは Oracle 12c 以降で許可されています。これは重要な点で、Id を指定せずにテーブルへ挿入する他のもの (SQL*Plus のスクリプト、ETL ジョブ、別のサービス) も、EF だけでなく同じシーケンスからキーを取得します。

HasSequence を省略して UseSequence("ORDER_SEQ") だけを呼び出すと、プロバイダーが START WITH 1 INCREMENT BY 1 でシーケンスをモデルに追加してくれます。新しいスキーマならそれで問題ありませんが、StartsAtHasMaxIsCyclic を設定できるのは明示的に宣言した場合だけです。

SaveChanges が実際に送るもの

キーをシーケンスにマッピングした状態で db.Orders.Add(new Order { Customer = "ACME" }) を実行すると、Id0 のまま一時的な値としてマークされます。続いて SaveChanges は 1 つの匿名 PL/SQL ブロックを送信します。

-- captured from SaveChanges, Oracle.EntityFrameworkCore 10.23.26301
DECLARE
TYPE "rOrders_0" IS RECORD
(
"Id" NUMBER(10)
);
TYPE "tOrders_0" IS TABLE OF "rOrders_0";
"lOrders_0" "tOrders_0";
BEGIN
"lOrders_0" := "tOrders_0"();
"lOrders_0".extend(1);
INSERT INTO "Orders" ("Customer")
VALUES (:p0)
RETURNING "Id" INTO "lOrders_0"(1)."Id";
OPEN :cur0 FOR SELECT "lOrders_0"(1)."Id" FROM DUAL;
END;
-- params: :p0=ACME (Input), :cur0 (Output, ref cursor)

キー列は列リストに含まれず、Oracle が既定値から値を埋め、RETURNING ... INTO が生成された値を REF カーソル経由で返すので、EF はそれを order.Id に書き込めます。SaveChanges のバッチごとに 1 回のラウンドトリップで、ID 列の場合と同じ形です。EF から見れば、「ID 列」も「シーケンスの既定値」も「挿入時にデータベースが生成する」という点で同じであり、違いはすべて DDL の中にあります。

次に、キーを自分で設定してみます (new Order { Id = 500, Customer = "ACME" })。

-- captured from SaveChanges when Id is set explicitly
BEGIN
INSERT INTO "Orders" ("Id", "Customer")
VALUES (:p0, :p1);
END;
-- params: :p0=500, :p1=ACME

RETURNING もなく、特別なモードもありません。EF は既定値でないキー値をそのまま送信し、値が指定されていれば列の既定値は単に使われません。ただし、シーケンスもそのことに気付かない点に注意してください。ORDER_SEQ は 500 に到達すればそのまま 500 を払い出し、その挿入は主キー違反で失敗します。プロバイダーの ID 列 (BY DEFAULT ON NULL) も同じ動作をします。実用上の違いは、名前付きシーケンスは挙動を把握しやすく、ALTER SEQUENCE 1 回でインポート済みデータの先へ進められること、そして他のコードが挿入前に NEXTVAL を呼び出してキーを予約できることです。

既存のシーケンスを使う

既存のスキーマでは、通常 EF にシーケンスを作成させるのではなく、すでにあるものを使わせたいはずです。正しく押さえるべき点が 3 つあります。

大文字小文字。 Oracle は引用符で囲まれていない識別子を大文字に変換するため、CREATE SEQUENCE order_seqORDER_SEQ を作成します。EF プロバイダーは常に引用符で囲みます。UseSequence("order_seq") は次を生成します。

CREATE SEQUENCE "order_seq" START WITH 1 INCREMENT BY 1 NOMINVALUE NOMAXVALUE NOCYCLE
"Id" NUMBER(10) DEFAULT ("order_seq".NEXTVAL) NOT NULL,

"order_seq"ORDER_SEQ は別々のオブジェクトです。既存のデータベースに対しては、この既定値は ORA-02289: sequence does not exist で失敗します。名前は SELECT sequence_name FROM user_sequences が出力するとおりに使ってください。

スキーマ。 シーケンスが別のスキーマにある場合は、両方の呼び出しにスキーマを渡します。

// .NET 10, EF Core 10, Oracle.EntityFrameworkCore 10.23.26301
modelBuilder.HasSequence<long>("ORDER_SEQ", "SALES")
    .StartsAt(1000)
    .HasMax(99999999);

modelBuilder.Entity<Order>()
    .Property(o => o.Id)
    .UseSequence("ORDER_SEQ", "SALES");

生成されるスクリプトは、まず SALES ユーザーが存在しなければ ORA-01435 を発生させる PL/SQL のチェックを実行し、その後 "SALES"."ORDER_SEQ" と、"SALES"."ORDER_SEQ".NEXTVAL を既定値とする列を作成します。アプリケーションのユーザーにはそのシーケンスに対する SELECT 権限が必要です。そうでないと、より強い権限のアカウントでマイグレーションが成功していても、実行時に挿入が失敗します。

マイグレーション。 EF にはシーケンス用の ExcludeFromMigrations がありません。シーケンスがすでに存在する場合は、最初のマイグレーションをスキャフォールドした後にその中の CreateSequence 呼び出しを削除するか (モデルスナップショットには記録が残り、それが望ましい状態です)、空の初期マイグレーションでデータベースのベースラインを作成してください。

トリガーで値が埋められるレガシーなテーブル

列の既定値をまったく持たない Oracle スキーマもたくさんあります。代わりにトリガーが処理を行います。

CREATE OR REPLACE TRIGGER ORDERS_BI
BEFORE INSERT ON "Orders" FOR EACH ROW
BEGIN
  :NEW."Id" := ORDER_SEQ.NEXTVAL;
END;

これはデータベースに手を加えずにマッピングできます。値が追加時に生成されることを EF に伝え、ID 列の規約を無効にして、将来のマイグレーションが列に GENERATED ... AS IDENTITY を追加しようとしないようにします。

// .NET 10, EF Core 10, Oracle.EntityFrameworkCore 10.23.26301
using Oracle.EntityFrameworkCore.Metadata;

modelBuilder.Entity<Order>()
    .Property(o => o.Id)
    .ValueGeneratedOnAdd()
    .Metadata.SetValueGenerationStrategy(OracleValueGenerationStrategy.None);

このマッピングでは、EF が送信する挿入は上で示した INSERT ... RETURNING "Id" INTO ブロックとバイト単位で同じです。トリガーは行が書き込まれる前に実行され、RETURNING は最終的な行を読み取るので、EF はトリガーが設定した値を受け取ります。

トリガーには落とし穴が 2 つあります。

長期的には、トリガーのロジックを列の既定値に移し、UseSequence でマッピングするほうがすっきりした形です。仕組みが 1 つになり、DDL で確認でき、マイグレーションにも理解されます。

モデル全体に UseKeySequences を適用する

すべてのテーブルにそれぞれシーケンスを持たせたい場合、UseKeySequences() は生成されるすべてのキーに Sequence 戦略を適用します。

// .NET 10, EF Core 10, Oracle.EntityFrameworkCore 10.23.26301
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.UseKeySequences();

Order に対しては "OrderSequence"DEFAULT ("OrderSequence".NEXTVAL) が作成されます。名前はエンティティ名にサフィックスを付けたもので、プロバイダーが引用符で囲むため大文字小文字が混在したまま残ります。生の SQL を書く人は、引用符付きで "OrderSequence".NEXTVAL と入力しなければならなくなります。命名規則が ORDERS_SEQ であれば、代わりにプロパティごとにシーケンスをマッピングするか、キーとインデックスのカスタム命名規則 で紹介しているような命名規約と組み合わせてください。

UseHiLo を使わない理由

プロバイダーは int キーに対する UseHiLo("ORDER_HILO") を受け付け、hi/lo の通常の動作をします。"ORDER_HILO" START WITH 1 INCREMENT BY 10 を作成し、列の既定値は設定せず、最初の Add でブロックを予約するために SELECT "ORDER_HILO".NEXTVAL FROM DUAL を同期的に実行します。私が取得した結果では、3 回の追加で SaveChanges の前に ID 1、2、3 が割り当てられ、挿入では 3 つのキーすべてが明示的に送信されました。

しかし 10.23.26301 の README の Sequences の項には、HiLo 拡張メソッドは Char、UInt、ULong、UByte のデータ型の列を除いてサポートされないと書かれています。ベンダーが intlong でサポート対象外と明記しているものをキー戦略の土台にするのは割に合いません。さらに Oracle での hi/lo には別のコストが 2 つあります。EF クライアントを介さずに挿入する他の書き込み元はブロック方式を知りませんし、追加の NEXTVAL クエリは Add の中で実行されるため、非同期コードからであっても同期的なデータベース呼び出しになります。SaveChanges の前にキーが必要なら、代わりにクライアント側で GUID か ULID を生成してください。

ID 列をシーケンスに移行する

多くのチームが実際に必要とするのはこのマイグレーションです。テーブルは以前の EF バージョンで既定の ID 列として作成されており、今後は ORDER_SEQ を使うべきだというケースです。マッピングを変更してマイグレーションを追加すると、プロバイダーは次を生成します。

-- IMigrationsSqlGenerator output, identity -> UseSequence("ORDER_SEQ")
CREATE SEQUENCE "ORDER_SEQ" START WITH 1000 INCREMENT BY 1 NOMINVALUE NOMAXVALUE NOCYCLE

DECLARE
   v_Count INTEGER;
BEGIN
  SELECT COUNT(*) INTO v_Count
  FROM ALL_TAB_IDENTITY_COLS T
  WHERE T.TABLE_NAME = N'Orders'
  AND T.COLUMN_NAME = 'Id';
  IF v_Count > 0 THEN
    EXECUTE IMMEDIATE 'ALTER  TABLE "Orders" MODIFY "Id" DROP IDENTITY';
  END IF;
END;

-- followed by a block that runs:
-- ALTER TABLE "Orders" MODIFY "Id" DEFAULT ("ORDER_SEQ".NEXTVAL)

テーブルを再構築せずに ID 列の設定を削除して既定値を追加するので、これは望みどおりの動作です。ただし、すでに何行あるかはプロバイダーにはわかりません。MAX("Id") が 48210 のテーブルで StartsAt(1000) を使うと、デプロイ後の最初の挿入が ORA-00001: unique constraint (PK_Orders) violated で失敗し、その後の 47210 回も同様に失敗します。マイグレーションを生成する前に現在の最大値を調べて StartsAt をそれより十分大きな値に設定するか、CreateSequence の後にシーケンスをデータの先まで進める migrationBuilder.Sql(...) のステップを追加してください。

後で StartsAt を変更すると RestartSequenceOperation が生成され、プロバイダーはそれを次のように出力します。

ALTER SEQUENCE "ORDER_SEQ" RESTART START WITH 5000

ALTER SEQUENCE ... RESTART は Oracle 19c 以降でドキュメント化されています。プロバイダーの README には「シーケンスは再開できない」という古い記述がまだ残っているので、本番のマイグレーションでこのステートメントに頼る前に確認してください。特に、デプロイ時に誰も SQL を読まない マイグレーションバンドル で実行する場合は注意が必要です。

欠番、キャッシュ、RAC

シーケンスは値を返却しません。Oracle の CREATE SEQUENCE リファレンスには明記されています。既定は CACHE 20 で、インスタンスに障害が発生するとキャッシュされた値は失われ、ロールバックされたトランザクションで使われた番号はスキップされます。プロバイダーは CACHE 句なしで CREATE SEQUENCE を出力するため、この既定値が適用されます。欠番は正常であり、コストもかかりません。それを「修正」するために NOCACHE を追加しないでください。すべての挿入がデータディクショナリの更新で直列化されてしまいます。

RAC では各インスタンスが独自の範囲をキャッシュするため、2 つのノードから払い出されたキーは順不同で入り混じります。Oracle は、主キーの生成に使うシーケンスでは通常、順序は重要ではないと述べています。欠番のない、人の目に触れる番号 (法的要件のある請求書番号など) が必要なら、それは別の問題です。どのデータベースであっても、シーケンスはその用途に適したツールではありません。

次に読む記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る