Start Debugging

Primärschlüssel in EF Core mit einer Oracle-Sequence erzeugen

Einen EF-Core-Schlüssel mit UseSequence in Oracle.EntityFrameworkCore 10 auf eine Oracle-Sequence abbilden: das DDL- und INSERT-SQL, das der Provider tatsächlich erzeugt, wie Sie auf eine vorhandene Sequence oder einen Legacy-Trigger verweisen, warum Quoting und HiLo zu Stolperfallen werden und wie Sie eine Identity-Spalte ohne ORA-00001 auf eine Sequence umstellen.

Kurze Antwort: Mit Oracle.EntityFrameworkCore 10.x deklarieren Sie die Sequence und verweisen den Schlüssel mit dem providereigenen UseSequence darauf: modelBuilder.HasSequence<int>("ORDER_SEQ").StartsAt(1000); und dann modelBuilder.Entity<Order>().Property(o => o.Id).UseSequence("ORDER_SEQ");. Der Provider macht daraus einen Spalten-Default, "Id" NUMBER(10) DEFAULT ("ORDER_SEQ".NEXTVAL), lässt den Schlüssel im INSERT weg und liest den Wert mit RETURNING "Id" INTO zurück. Greifen Sie nicht allein zu ValueGeneratedOnAdd() (das liefert eine Identity-Spalte, nicht Ihre Sequence), schreiben Sie den Sequence-Namen exakt in der Schreibweise, in der Oracle ihn speichert (meist in Großbuchstaben), und verzichten Sie auf UseHiLo bei int/long-Schlüsseln, die Oracles eigene README als nicht unterstützt aufführt.

Alles Folgende wurde mit Oracle.EntityFrameworkCore 10.23.26301 (veröffentlicht am 2026-09-08, Assembly 10.0.23.1) auf EF Core 10 und dem .NET 10 SDK 10.0.302 geprüft. Auf dem Rechner, auf dem dieser Artikel entstand, läuft keine Oracle Database. Das gezeigte SQL ist also das, was der Provider erzeugt, offline mitgeschnitten: GenerateCreateScript() für DDL, IMigrationsSqlGenerator für Migrationen und ein Command Interceptor, der den Befehlstext von SaveChanges abgreift, bevor er über die Leitung ginge. Es werden keine Zeitmessungen oder echten Roundtrips behauptet.

Warum Oracle-Schlüssel auf Sequences landen

Oracle hatte Sequences lange vor Identity-Spalten (die kamen erst mit 12c). Die meisten Oracle-Schemas, die Sie übernehmen, verwenden daher eine Sequence pro Tabelle, meist <TABLE>_SEQ, und oft einen BEFORE INSERT-Trigger, der NEXTVAL in den Schlüssel kopiert. Auch bei neuen Schemas wählen Teams eine Sequence statt Identity, weil eine benannte Sequence von mehreren Tabellen gemeinsam genutzt, von anderen Anwendungen mit SELECT ORDER_SEQ.NEXTVAL FROM DUAL gelesen werden kann und explizite Schlüsselwerte ohne besonderen Modus akzeptiert.

Der Oracle-Provider für EF Core unterstützt all das, aber seine Standardeinstellungen zielen woanders hin. Ohne weitere Konfiguration erhält ein int-Schlüssel namens Id die Strategie IdentityColumn:

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

Das ist eine Identity-Spalte, hinter der eine versteckte System-Sequence (ISEQ$$_...) steht, keine, die Sie benannt haben. Wenn Ihr DBA Ihnen ORDER_SEQ vorgegeben hat oder Ihre Tabellen bereits Trigger haben, müssen Sie das im Modell angeben.

Die API des 10.x-Providers

Der Oracle-Provider bringt ein eigenes Enum für Wertgenerierungsstrategien mit, getrennt von dem für SQL Server. Reflection über die Assembly 10.23.26301 ergibt:

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

und diese Einstiegspunkte für Sequences:

Beachten Sie, dass das Kompatibilitäts-Enum bei 19 beginnt. Ältere Provider-Versionen akzeptierten UseOracleSQLCompatibility("11"), womit der Provider für jeden generierten Schlüssel eine Sequence samt Trigger anlegte. Diesen Modus gibt es im 10.x-Provider nicht mehr: Auch mit DatabaseVersion19 entsteht dieselbe GENERATED BY DEFAULT ON NULL AS IDENTITY-Spalte wie mit 23. Wer Sequences will, fordert sie pro Eigenschaft an.

Schritt für Schritt: einen Schlüssel auf eine benannte Sequence abbilden

  1. Installieren Sie den Provider, der zu Ihrer EF-Core-Hauptversion passt. Für EF Core 10 ist das Oracle.EntityFrameworkCore 10.23.x; die nuspec pinnt Microsoft.EntityFrameworkCore.Relational auf [10.0.0, 11.0.0), und einen Build für EF Core 11 gibt es noch nicht.
  2. Deklarieren Sie die Sequence mit HasSequence, damit Migrationen ihren Startwert, ihr Inkrement und ihre Grenzen verwalten.
  3. Rufen Sie UseSequence auf dem Schlüssel mit demselben Namen (und gegebenenfalls demselben Schema) auf.
  4. Fügen Sie eine Migration hinzu und lesen Sie das generierte SQL, bevor Sie sie anwenden.
// .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");
    }
}

Das DDL, das der Provider für dieses Modell erzeugt:

-- 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;

Die Sequence wird zu einem gewöhnlichen Spalten-Default, was Oracle ab 12c erlaubt. Das ist wichtig: Alles andere, was ohne Angabe von Id in die Tabelle einfügt (ein SQL*Plus-Skript, ein ETL-Job, ein anderer Dienst), erhält einen Schlüssel aus derselben Sequence, nicht nur EF.

Wenn Sie HasSequence weglassen und nur UseSequence("ORDER_SEQ") aufrufen, fügt der Provider die Sequence mit START WITH 1 INCREMENT BY 1 selbst zum Modell hinzu. Für ein frisches Schema ist das in Ordnung, aber nur bei expliziter Deklaration lassen sich StartsAt, HasMax oder IsCyclic setzen.

Was SaveChanges tatsächlich sendet

Ist der Schlüssel auf die Sequence abgebildet, lässt db.Orders.Add(new Order { Customer = "ACME" }) Id auf 0 und markiert es als temporären Wert. SaveChanges sendet dann einen einzelnen anonymen PL/SQL-Block:

-- 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)

Die Schlüsselspalte steht nicht in der Spaltenliste, Oracle füllt sie aus dem Default, und RETURNING ... INTO gibt den generierten Wert über einen Ref Cursor zurück, damit EF ihn in order.Id schreiben kann. Das ist ein Roundtrip pro SaveChanges-Batch und dieselbe Form wie im Identity-Fall. Aus Sicht von EF sind “Identity” und “Sequence-Default” beide “die Datenbank erzeugt den Wert beim Einfügen”; der Unterschied liegt ausschließlich im DDL.

Setzen Sie den Schlüssel nun selbst (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

Kein RETURNING, kein besonderer Modus. EF sendet einen vom Standard abweichenden Schlüsselwert unverändert, und ein Spalten-Default greift schlicht nicht, wenn ein Wert angegeben ist. Bedenken Sie, dass die Sequence davon ebenfalls nichts merkt: ORDER_SEQ gibt trotzdem 500 aus, wenn sie dort ankommt, und dieser Insert scheitert am Primärschlüssel. Die Identity-Spalten des Providers (BY DEFAULT ON NULL) verhalten sich genauso. Der praktische Unterschied: Eine benannte Sequence ist leicht nachzuvollziehen, lässt sich mit einem einzigen ALTER SEQUENCE hinter importierte Daten verschieben, und anderer Code kann NEXTVAL aufrufen, um vor dem Einfügen einen Schlüssel zu reservieren.

Auf eine bereits vorhandene Sequence verweisen

Bei einem bestehenden Schema soll EF die Sequence meist nicht anlegen, sondern die vorhandene verwenden. Drei Dinge müssen stimmen.

Groß- und Kleinschreibung. Oracle wandelt nicht gequotete Bezeichner in Großbuchstaben um, CREATE SEQUENCE order_seq erzeugt also ORDER_SEQ. Der EF-Provider quotet immer. UseSequence("order_seq") ergibt:

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

"order_seq" und ORDER_SEQ sind zwei verschiedene Objekte. Gegen eine bestehende Datenbank scheitert dieser Default mit ORA-02289: sequence does not exist. Verwenden Sie den Namen genau so, wie SELECT sequence_name FROM user_sequences ihn ausgibt.

Schema. Liegt die Sequence in einem anderen Schema, übergeben Sie es an beide Aufrufe:

// .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");

Das generierte Skript führt zuerst eine PL/SQL-Prüfung aus, die ORA-01435 auslöst, wenn der Benutzer SALES nicht existiert, und legt dann "SALES"."ORDER_SEQ" sowie einen Spalten-Default von "SALES"."ORDER_SEQ".NEXTVAL an. Ihr Anwendungsbenutzer braucht SELECT auf diese Sequence, sonst scheitern Inserts zur Laufzeit, obwohl die Migration unter einem höher berechtigten Konto erfolgreich war.

Migrationen. EF kennt kein ExcludeFromMigrations für Sequences. Existiert die Sequence bereits, löschen Sie den CreateSequence-Aufruf aus der ersten Migration, nachdem sie generiert wurde (der Model Snapshot hält sie trotzdem fest, und genau das wollen Sie), oder setzen Sie die Datenbank mit einer leeren initialen Migration auf eine Baseline.

Legacy-Tabellen, die ein Trigger befüllt

Viele Oracle-Schemas haben überhaupt keinen Spalten-Default. Stattdessen erledigt ein Trigger die Arbeit:

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

Das lässt sich abbilden, ohne die Datenbank anzufassen: Teilen Sie EF mit, dass der Wert beim Hinzufügen generiert wird, und schalten Sie die Identity-Konvention ab, damit eine künftige Migration nicht versucht, der Spalte GENERATED ... AS IDENTITY hinzuzufügen.

// .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);

Mit diesem Mapping ist der Insert, den EF sendet, Byte für Byte derselbe INSERT ... RETURNING "Id" INTO-Block wie oben. Der Trigger läuft, bevor die Zeile geschrieben wird, und RETURNING liest die endgültige Zeile, also erhält EF den Wert des Triggers.

Zwei Fallen bei Triggern:

Langfristig ist es die sauberere Lösung, die Logik des Triggers in einen Spalten-Default zu verlagern und ihn mit UseSequence abzubilden: ein Mechanismus, sichtbar im DDL, von Migrationen verstanden.

UseKeySequences für ein ganzes Modell

Soll jede Tabelle ihre eigene Sequence erhalten, wendet UseKeySequences() die Strategie Sequence auf jeden generierten Schlüssel an:

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

Für Order entstehen "OrderSequence" und DEFAULT ("OrderSequence".NEXTVAL). Der Name ist der Entitätsname plus Suffix und behält seine gemischte Schreibweise, weil der Provider ihn quotet. Wer rohes SQL schreibt, muss nun "OrderSequence".NEXTVAL samt Anführungszeichen tippen. Lautet Ihr Namensstandard ORDERS_SEQ, bilden Sie die Sequences stattdessen pro Eigenschaft ab oder kombinieren das mit einer Namenskonvention wie denen in eigene Namenskonventionen für Schlüssel und Indizes.

Warum nicht UseHiLo

Der Provider akzeptiert UseHiLo("ORDER_HILO") auf einem int-Schlüssel und tut, was Hi/Lo eben tut: Er legt "ORDER_HILO" START WITH 1 INCREMENT BY 10 an, ohne Spalten-Default, und führt beim ersten Add synchron SELECT "ORDER_HILO".NEXTVAL FROM DUAL aus, um einen Block zu reservieren. In meinem Mitschnitt erhielten drei Adds vor SaveChanges die IDs 1, 2 und 3, und der Insert sendete alle drei Schlüssel explizit.

Die README zu 10.23.26301 sagt jedoch unter Sequences, dass die HiLo-Erweiterungsmethoden nicht unterstützt werden, “except for columns with Char, UInt, ULong, and UByte data types”. Die eigene Schlüsselstrategie auf etwas aufzubauen, das der Hersteller für int und long als nicht unterstützt dokumentiert, ist ein schlechter Tausch, und Hi/Lo hat auf Oracle zwei weitere Kosten: Andere Schreiber, die ohne den EF-Client einfügen, kennen das Blockschema nicht, und die zusätzliche NEXTVAL-Abfrage passiert innerhalb von Add, also als synchroner Datenbankaufruf selbst aus asynchronem Code. Brauchen Sie Schlüssel vor SaveChanges, erzeugen Sie stattdessen clientseitig eine GUID oder eine ULID.

Eine Identity-Spalte auf eine Sequence umstellen

Das ist die Migration, die die meisten Teams tatsächlich brauchen: Die Tabelle wurde von einer früheren EF-Version mit der Standard-Identity angelegt und soll nun ORDER_SEQ verwenden. Ändern Sie das Mapping, fügen Sie eine Migration hinzu, und der Provider erzeugt:

-- 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)

Sie entfernt die Identity und fügt den Default hinzu, ohne die Tabelle neu aufzubauen, und genau das wollen Sie. Was sie nicht wissen kann, ist, wie viele Zeilen bereits vorhanden sind. StartsAt(1000) auf einer Tabelle, deren MAX("Id") 48210 beträgt, bedeutet, dass der erste Insert nach dem Deployment mit ORA-00001: unique constraint (PK_Orders) violated scheitert, und die nächsten 47210 ebenso. Fragen Sie vor dem Generieren der Migration das aktuelle Maximum ab und setzen Sie StartsAt mit reichlich Abstand darüber, oder fügen Sie nach dem CreateSequence einen migrationBuilder.Sql(...)-Schritt hinzu, der die Sequence hinter die Daten verschiebt.

Eine spätere Änderung von StartsAt erzeugt eine RestartSequenceOperation, die der Provider so ausgibt:

ALTER SEQUENCE "ORDER_SEQ" RESTART START WITH 5000

ALTER SEQUENCE ... RESTART ist ab Oracle 19c dokumentiert. Die README des Providers enthält noch eine ältere Zeile mit dem Wortlaut “A sequence cannot be restarted”. Prüfen Sie diese Anweisung also, bevor Sie sich in einer Produktionsmigration darauf verlassen, besonders wenn Sie sie über ein Migrations-Bundle ausführen, bei dem zum Zeitpunkt der Bereitstellung niemand das SQL liest.

Lücken, Caching und RAC

Eine Sequence gibt niemals Werte zurück. Oracles Referenz zu CREATE SEQUENCE ist eindeutig: Der Standard ist CACHE 20, zwischengespeicherte Werte gehen verloren, wenn die Instanz ausfällt, und Nummern, die in einer zurückgerollten Transaktion verwendet wurden, werden übersprungen. Der Provider gibt CREATE SEQUENCE ohne CACHE-Klausel aus, Sie erhalten also diesen Standard. Lücken sind normal und kosten nichts; fügen Sie kein NOCACHE hinzu, um sie zu “beheben”, denn das serialisiert jeden Insert über ein Update im Data Dictionary.

Bei RAC speichert jede Instanz ihren eigenen Bereich zwischen, sodass Schlüssel von zwei Knoten ungeordnet ineinandergreifen. Oracle merkt an, dass die Reihenfolge “is usually not important for sequences used to generate primary keys”. Brauchen Sie lückenlose, für Menschen sichtbare Nummern (Rechnungsnummern mit gesetzlichen Anforderungen), ist das ein anderes Problem: Dafür ist eine Sequence auf jeder Datenbank das falsche Werkzeug.

Weiterlesen

Quellen

Comments

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

< Zurück