Start Debugging

Cómo usar una secuencia de Oracle para generar claves primarias en EF Core

Mapea una clave de EF Core a una secuencia de Oracle con UseSequence en Oracle.EntityFrameworkCore 10: el DDL y el SQL de INSERT que el proveedor emite de verdad, cómo apuntar a una secuencia existente o a un trigger heredado, por qué las comillas y HiLo confunden a tanta gente, y cómo pasar una columna identity a una secuencia sin ORA-00001.

Respuesta corta: con Oracle.EntityFrameworkCore 10.x, declara la secuencia y apunta la clave hacia ella con el UseSequence propio del proveedor: modelBuilder.HasSequence<int>("ORDER_SEQ").StartsAt(1000); y luego modelBuilder.Entity<Order>().Property(o => o.Id).UseSequence("ORDER_SEQ");. El proveedor convierte eso en un valor predeterminado de columna, "Id" NUMBER(10) DEFAULT ("ORDER_SEQ".NEXTVAL), deja la clave fuera del INSERT y lee el valor de vuelta con RETURNING "Id" INTO. No recurras solo a ValueGeneratedOnAdd() (eso te da una columna identity, no tu secuencia), escribe el nombre de la secuencia exactamente con las mayúsculas y minúsculas con que Oracle lo almacena (normalmente en mayúsculas) y evita UseHiLo en claves int/long, que el propio README de Oracle marca como no soportado.

Todo lo que sigue se comprobó contra Oracle.EntityFrameworkCore 10.23.26301 (publicado el 2026-09-08, ensamblado 10.0.23.1) sobre EF Core 10 y el SDK de .NET 10 10.0.302. En la máquina donde escribí esto no hay ninguna Oracle Database, así que el SQL que se muestra es el que genera el proveedor, capturado sin conexión: GenerateCreateScript() para el DDL, IMigrationsSqlGenerator para las migraciones y un interceptor de comandos que toma el texto del comando de SaveChanges antes de que llegue a la red. No se afirman tiempos ni viajes de ida y vuelta reales.

Por qué las claves de Oracle terminan en secuencias

Oracle tuvo secuencias mucho antes que columnas identity (estas llegaron en 12c), así que la mayoría de los esquemas de Oracle que heredas usan una secuencia por tabla, normalmente <TABLE>_SEQ, y a menudo un trigger BEFORE INSERT que copia NEXTVAL en la clave. Incluso en esquemas nuevos, los equipos eligen una secuencia en lugar de identity porque una secuencia con nombre puede compartirse entre varias tablas, otras aplicaciones pueden leerla con SELECT ORDER_SEQ.NEXTVAL FROM DUAL y acepta valores de clave explícitos sin ningún modo especial.

El proveedor de Oracle para EF Core soporta todo esto, pero sus valores predeterminados apuntan a otro lado. De fábrica, una clave int llamada Id recibe la estrategia IdentityColumn:

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

Eso es una columna identity respaldada por una secuencia oculta del sistema (ISEQ$$_...), no una a la que tú le pusiste nombre. Si tu DBA te dio ORDER_SEQ, o tus tablas ya tienen triggers, tienes que indicarlo en el modelo.

La superficie de la API en el proveedor 10.x

El proveedor de Oracle trae su propio enum de estrategias de generación de valores, separado del de SQL Server. Inspeccionar por reflexión el ensamblado 10.23.26301 da:

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

y estos puntos de entrada para secuencias:

Fíjate en que el enum de compatibilidad empieza en 19. Las versiones antiguas del proveedor aceptaban UseOracleSQLCompatibility("11"), que hacía que el proveedor creara una secuencia más un trigger para cada clave generada. Ese modo ya no existe en el proveedor 10.x: configurar DatabaseVersion19 sigue produciendo la misma columna GENERATED BY DEFAULT ON NULL AS IDENTITY que 23. Si quieres secuencias, las pides propiedad por propiedad.

Paso a paso: mapear una clave a una secuencia con nombre

  1. Instala el proveedor que corresponde a tu versión mayor de EF Core. Para EF Core 10 es Oracle.EntityFrameworkCore 10.23.x; el nuspec fija Microsoft.EntityFrameworkCore.Relational en [10.0.0, 11.0.0), y todavía no hay una compilación para EF Core 11.
  2. Declara la secuencia con HasSequence para que las migraciones controlen su valor inicial, su incremento y sus límites.
  3. Llama a UseSequence sobre la clave con el mismo nombre (y esquema, si lo hay).
  4. Agrega una migración y lee el SQL generado antes de aplicarla.
// .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");
    }
}

El DDL que genera el proveedor para ese modelo:

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

La secuencia se convierte en un simple valor predeterminado de columna, algo que Oracle permite desde 12c. Eso importa: cualquier otra cosa que inserte en la tabla sin proporcionar Id (un script de SQL*Plus, un proceso ETL, otro servicio) obtiene una clave de la misma secuencia, no solo EF.

Si omites HasSequence y solo llamas a UseSequence("ORDER_SEQ"), el proveedor agrega la secuencia al modelo por ti con START WITH 1 INCREMENT BY 1. Eso está bien para un esquema nuevo, pero declararla explícitamente es el único lugar donde puedes configurar StartsAt, HasMax o IsCyclic.

Lo que SaveChanges envía realmente

Con la clave mapeada a la secuencia, db.Orders.Add(new Order { Customer = "ACME" }) deja Id en 0 y lo marca como valor temporal. Luego SaveChanges envía un único bloque PL/SQL anónimo:

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

La columna de la clave no está en la lista de columnas, Oracle la rellena desde el valor predeterminado y RETURNING ... INTO devuelve el valor generado a través de un ref cursor para que EF pueda escribirlo en order.Id. Es un viaje de ida y vuelta por cada lote de SaveChanges, con la misma forma que en el caso de la columna identity. Desde el punto de vista de EF, “identity” y “valor predeterminado de secuencia” son ambos “la base de datos lo genera al insertar”; la diferencia vive por completo en el DDL.

Ahora asigna la clave tú mismo (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

Sin RETURNING, sin modo especial. EF envía tal cual un valor de clave que no es el predeterminado, y un valor predeterminado de columna simplemente no se dispara cuando se proporciona un valor. Ten en cuenta que la secuencia tampoco se entera: ORDER_SEQ seguirá entregando 500 cuando llegue ahí, y ese insert falla en la clave primaria. Las columnas identity del proveedor (BY DEFAULT ON NULL) se comportan igual. La diferencia práctica es que una secuencia con nombre es fácil de razonar y de adelantar más allá de los datos importados con un solo ALTER SEQUENCE, y otro código puede llamar a NEXTVAL para reservar una clave antes de insertar.

Apuntar a una secuencia que ya existe

En un esquema existente normalmente no quieres que EF cree la secuencia, quieres que use la que ya está. Hay tres cosas que debes hacer bien.

Mayúsculas y minúsculas. Oracle pasa a mayúsculas los identificadores sin comillas, así que CREATE SEQUENCE order_seq crea ORDER_SEQ. El proveedor de EF siempre usa comillas. UseSequence("order_seq") produce:

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

"order_seq" y ORDER_SEQ son dos objetos distintos. Contra una base de datos existente, ese valor predeterminado falla con ORA-02289: sequence does not exist. Usa el nombre exactamente como lo imprime SELECT sequence_name FROM user_sequences.

Esquema. Si la secuencia vive en otro esquema, pásalo en ambas llamadas:

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

El script generado primero ejecuta una verificación en PL/SQL que lanza ORA-01435 si el usuario SALES no existe, y luego crea "SALES"."ORDER_SEQ" y un valor predeterminado de columna "SALES"."ORDER_SEQ".NEXTVAL. El usuario de tu aplicación necesita SELECT sobre esa secuencia, o los inserts fallan en runtime aunque la migración haya funcionado con una cuenta con más privilegios.

Migraciones. EF no tiene ExcludeFromMigrations para secuencias. Si la secuencia ya existe, elimina la llamada a CreateSequence de la primera migración después de generarla (el snapshot del modelo sigue registrándola, que es lo que quieres), o establece una línea base de la base de datos con una migración inicial vacía.

Tablas heredadas que rellena un trigger

Muchos esquemas de Oracle no tienen ningún valor predeterminado de columna. En su lugar, un trigger hace el trabajo:

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

Puedes mapear esto sin tocar la base de datos: dile a EF que el valor se genera al agregar y desactiva la convención de identity para que una migración futura no intente agregar GENERATED ... AS IDENTITY a la columna.

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

Con ese mapeo, el insert que envía EF es idéntico byte a byte al bloque INSERT ... RETURNING "Id" INTO mostrado arriba. El trigger se ejecuta antes de que se escriba la fila, y RETURNING lee la fila final, así que EF obtiene el valor del trigger.

Dos trampas con los triggers:

A largo plazo, mover la lógica del trigger a un valor predeterminado de columna y mapearlo con UseSequence es la forma más limpia: un solo mecanismo, visible en el DDL y comprendido por las migraciones.

UseKeySequences para todo un modelo

Si cada tabla debe tener su propia secuencia, UseKeySequences() aplica la estrategia Sequence a cada clave generada:

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

Para Order crea "OrderSequence" y DEFAULT ("OrderSequence".NEXTVAL). El nombre es el nombre de la entidad más el sufijo, y conserva sus mayúsculas y minúsculas mixtas porque el proveedor lo pone entre comillas. Cualquiera que escriba SQL a mano ahora tiene que teclear "OrderSequence".NEXTVAL con las comillas. Si tu estándar de nombres es ORDERS_SEQ, mapea las secuencias por propiedad, o combina esto con una convención de nombres como las de convenciones de nombres personalizadas para claves e índices.

Por qué no UseHiLo

El proveedor acepta UseHiLo("ORDER_HILO") en una clave int y hace lo que hace hi/lo: crea "ORDER_HILO" START WITH 1 INCREMENT BY 10, sin valor predeterminado de columna, y en el primer Add ejecuta SELECT "ORDER_HILO".NEXTVAL FROM DUAL de forma síncrona para reservar un bloque. En mi captura, tres adds obtuvieron los ids 1, 2 y 3 antes de SaveChanges, y el insert envió las tres claves explícitamente.

Pero el README de 10.23.26301 dice, en la sección Sequences, que los métodos de extensión de HiLo no están soportados “except for columns with Char, UInt, ULong, and UByte data types”. Construir tu estrategia de claves sobre algo que el fabricante documenta como no soportado para int y long es un mal negocio, y hi/lo tiene otros dos costos en Oracle: otros escritores que insertan sin el cliente de EF no conocen el esquema de bloques, y la consulta extra de NEXTVAL ocurre dentro de Add, que es una llamada síncrona a la base de datos incluso desde código asíncrono. Si necesitas claves antes de SaveChanges, genera un GUID o un ULID del lado del cliente.

Pasar una columna identity a una secuencia

Esta es la migración que la mayoría de los equipos realmente necesita: la tabla la creó una versión anterior de EF con la identity predeterminada, y ahora debe usar ORDER_SEQ. Cambia el mapeo, agrega una migración y el proveedor genera:

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

Elimina la identity y agrega el valor predeterminado sin reconstruir la tabla, que es lo que quieres. Lo que no puede saber es cuántas filas hay ya. StartsAt(1000) en una tabla cuyo MAX("Id") es 48210 significa que el primer insert después de implementar falla con ORA-00001: unique constraint (PK_Orders) violated, y lo mismo les pasa a los siguientes 47210. Antes de generar la migración, consulta el máximo actual y configura StartsAt con holgura por encima de él, o agrega un paso migrationBuilder.Sql(...) después del CreateSequence que adelante la secuencia más allá de los datos.

Cambiar StartsAt más adelante produce una RestartSequenceOperation, que el proveedor emite como:

ALTER SEQUENCE "ORDER_SEQ" RESTART START WITH 5000

ALTER SEQUENCE ... RESTART está documentado desde Oracle 19c. El README del proveedor todavía conserva una línea antigua que dice “A sequence cannot be restarted”, así que revisa esa sentencia antes de confiar en ella en una migración de producción, sobre todo si la ejecutas mediante un migrations bundle donde nadie lee el SQL en el momento de la implementación.

Huecos, caché y RAC

Una secuencia nunca devuelve valores. La referencia de CREATE SEQUENCE de Oracle es explícita: el valor predeterminado es CACHE 20, los valores en caché se pierden cuando la instancia falla y los números usados en una transacción que se revierte se saltan. El proveedor emite CREATE SEQUENCE sin cláusula CACHE, así que obtienes ese valor predeterminado. Los huecos son normales y baratos; no agregues NOCACHE para “arreglarlos”, porque serializa cada insert en una actualización del diccionario de datos.

En RAC, cada instancia guarda en caché su propio rango, así que las claves de dos nodos se intercalan fuera de orden. Oracle señala que el orden “is usually not important for sequences used to generate primary keys”. Si necesitas números sin huecos y visibles para personas (números de factura con requisitos legales), ese es otro problema: una secuencia es la herramienta equivocada para eso en cualquier base de datos.

Lecturas recomendadas

Fuentes

Comments

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

< Volver