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:
PropertyBuilder.UseSequence(string name = null, string schema = null): una clave, una secuencia con nombre.ModelBuilder.UseKeySequences(string nameSuffix = null, string schema = null): una secuencia por entidad para cada clave generada.PropertyBuilder.UseHiLo(...)yModelBuilder.UseHiLo(...): bloques hi/lo del lado del cliente.PropertyBuilder.UseIdentityColumn(...): el valor predeterminado, hecho explícito.
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
- Instala el proveedor que corresponde a tu versión mayor de EF Core. Para EF Core 10 es
Oracle.EntityFrameworkCore10.23.x; el nuspec fijaMicrosoft.EntityFrameworkCore.Relationalen[10.0.0, 11.0.0), y todavía no hay una compilación para EF Core 11. - Declara la secuencia con
HasSequencepara que las migraciones controlen su valor inicial, su incremento y sus límites. - Llama a
UseSequencesobre la clave con el mismo nombre (y esquema, si lo hay). - 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:
- Los triggers incondicionales chocan con las claves explícitas. Si asignas
Id = 500, EF envía el valor y no lo pide de vuelta (mira el segundo insert capturado). Un trigger que siempre asignaNEXTVALlo sobrescribe, y la entidad en memoria sigue diciendo500mientras la fila dice otra cosa. O escribes el trigger comoIF :NEW."Id" IS NULL THEN ... END IF;o nunca asignas claves en el código. - Varias columnas generadas. El propio README de Oracle para 10.23.26301 documenta que hacer scaffolding de una tabla con más de una columna identity o de secuencia/trigger emite
ValueGeneratedOnAdd()en todas ellas, y las consultas fallan después conORA-50607o un error similar que dice que solo se permite una columna identity por tabla. La solución documentada es reemplazarValueGeneratedOnAdd()porUseSequence("<the sequence the trigger uses>")en las columnas que no son clave.
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
- Cómo generar una clave primaria desde una secuencia de base de datos al insertar en EF Core 11 cubre la misma idea en SQL Server, donde
UseSequenceviene del proveedor SqlServer y el insert usaOUTPUTen lugar deRETURNING. - Solución: The entity type requires a primary key to be defined para cuando una vista de Oracle generada por scaffolding o una tabla sin clave impide que EF construya el modelo.
- Cómo agregar pluralización personalizada a dotnet ef dbcontext scaffold si haces scaffolding de un esquema heredado de Oracle y los nombres de las tablas salen destrozados.
- Cómo renombrar una tabla en una migración de EF Core 11 sin perder datos para la otra migración en la que leer primero el SQL generado te salva.
Fuentes
- Oracle.EntityFrameworkCore 10.23.26301 en NuGet, incluida la sección “Tips, Limitations, and Known Issues” del README.
- Documentación de ODP.NET Entity Framework Core y la página de características de Oracle EF Core 7, donde se introdujo
UseSequence(). - Referencia de SQL de Oracle: CREATE SEQUENCE y ALTER SEQUENCE (19c).
- Secuencias en EF Core en Microsoft Learn.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.