Start Debugging

Como usar uma sequence do Oracle para gerar chaves primárias no EF Core

Mapeie uma chave do EF Core para uma sequence do Oracle com UseSequence no Oracle.EntityFrameworkCore 10: o DDL e o SQL de INSERT que o provider realmente emite, como apontar para uma sequence existente ou um trigger legado, por que o uso de aspas e o HiLo confundem as pessoas, e como migrar uma coluna identity para uma sequence sem ORA-00001.

Resposta curta: com o Oracle.EntityFrameworkCore 10.x, declare a sequence e aponte a chave para ela com o próprio UseSequence do provider: modelBuilder.HasSequence<int>("ORDER_SEQ").StartsAt(1000); e depois modelBuilder.Entity<Order>().Property(o => o.Id).UseSequence("ORDER_SEQ");. O provider transforma isso em um default de coluna, "Id" NUMBER(10) DEFAULT ("ORDER_SEQ".NEXTVAL), deixa a chave fora do INSERT e lê o valor de volta com RETURNING "Id" INTO. Não recorra apenas a ValueGeneratedOnAdd() (isso gera uma coluna identity, não a sua sequence), escreva o nome da sequence exatamente na caixa em que o Oracle o armazena (geralmente maiúsculas) e evite UseHiLo em chaves int/long, que o próprio README da Oracle lista como não suportado.

Tudo o que vem a seguir foi verificado com o Oracle.EntityFrameworkCore 10.23.26301 (publicado em 2026-09-08, assembly 10.0.23.1) sobre o EF Core 10 e o .NET 10 SDK 10.0.302. Não há um Oracle Database na máquina em que escrevi este texto, então o SQL mostrado é o que o provider gera, capturado offline: GenerateCreateScript() para o DDL, IMigrationsSqlGenerator para as migrações e um interceptor de comandos que captura o texto do comando de SaveChanges antes que ele fosse para a rede. Nenhuma medição de tempo ou ida e volta real ao banco é alegada.

Por que as chaves no Oracle acabam em sequences

O Oracle tinha sequences muito antes de ter colunas identity (que chegaram no 12c), então a maioria dos schemas Oracle que você herda usa uma sequence por tabela, geralmente <TABLE>_SEQ, e com frequência um trigger BEFORE INSERT que copia NEXTVAL para a chave. Mesmo em schemas novos, as equipes escolhem uma sequence em vez de identity porque uma sequence nomeada pode ser compartilhada por várias tabelas, pode ser lida por outras aplicações com SELECT ORDER_SEQ.NEXTVAL FROM DUAL e aceita valores de chave explícitos sem nenhum modo especial.

O provider Oracle do EF Core suporta tudo isso, mas os seus padrões apontam para outro lugar. Por padrão, uma chave int chamada Id recebe a estratégia IdentityColumn:

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

Isso é uma coluna identity apoiada por uma sequence de sistema oculta (ISEQ$$_...), não uma que você nomeou. Se o seu DBA te entregou a ORDER_SEQ, ou se as suas tabelas já têm triggers, você precisa dizer isso no modelo.

A superfície da API no provider 10.x

O provider Oracle traz o seu próprio enum de estratégia de geração de valores, separado do enum do SQL Server. Inspecionar via reflection o assembly 10.23.26301 retorna:

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

e estes pontos de entrada para sequences:

Repare que o enum de compatibilidade começa em 19. Versões mais antigas do provider aceitavam UseOracleSQLCompatibility("11"), que fazia o provider criar uma sequence mais um trigger para cada chave gerada. Esse modo não existe mais no provider 10.x: definir DatabaseVersion19 ainda produz a mesma coluna GENERATED BY DEFAULT ON NULL AS IDENTITY que o 23. Se você quer sequences, precisa pedi-las por propriedade.

Passo a passo: mapear uma chave para uma sequence nomeada

  1. Instale o provider que corresponde à versão principal do seu EF Core. Para o EF Core 10, é o Oracle.EntityFrameworkCore 10.23.x; o nuspec fixa Microsoft.EntityFrameworkCore.Relational em [10.0.0, 11.0.0), e ainda não existe build para o EF Core 11.
  2. Declare a sequence com HasSequence para que as migrações controlem o valor inicial, o incremento e os limites.
  3. Chame UseSequence na chave com o mesmo nome (e schema, se houver).
  4. Adicione uma migração e leia o SQL gerado antes de aplicá-la.
// .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");
    }
}

O DDL que o provider gera para esse 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;

A sequence vira um simples default de coluna, o que o Oracle permite a partir do 12c. Isso importa: qualquer outra coisa que insira na tabela sem informar Id (um script do SQL*Plus, um job de ETL, outro serviço) recebe uma chave da mesma sequence, não só o EF.

Se você pular o HasSequence e chamar apenas UseSequence("ORDER_SEQ"), o provider adiciona a sequence ao modelo para você com START WITH 1 INCREMENT BY 1. Isso serve para um schema novo, mas declará-la explicitamente é o único lugar para definir StartsAt, HasMax ou IsCyclic.

O que o SaveChanges realmente envia

Com a chave mapeada para a sequence, db.Orders.Add(new Order { Customer = "ACME" }) deixa Id em 0 e o marca como valor temporário. O SaveChanges então envia um único bloco 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)

A coluna da chave não está na lista de colunas, o Oracle a preenche a partir do default, e RETURNING ... INTO devolve o valor gerado por meio de um ref cursor para que o EF possa gravá-lo em order.Id. É uma ida e volta por lote de SaveChanges, e o mesmo formato do caso com coluna identity. Do ponto de vista do EF, “identity” e “default de sequence” são ambos “o banco de dados gera no insert”; a diferença está inteiramente no DDL.

Agora defina a chave você mesmo (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

Sem RETURNING, sem modo especial. O EF envia um valor de chave diferente do padrão como está, e um default de coluna simplesmente não dispara quando um valor é informado. Lembre-se de que a sequence também não percebe nada: a ORDER_SEQ ainda vai entregar 500 quando chegar lá, e esse insert falha na chave primária. As colunas identity do provider (BY DEFAULT ON NULL) se comportam da mesma forma. A diferença prática é que uma sequence nomeada é fácil de entender e de avançar além de dados importados com um único ALTER SEQUENCE, e outro código pode chamar NEXTVAL para reservar uma chave antes de inserir.

Apontando para uma sequence que já existe

Em um schema existente, você geralmente não quer que o EF crie a sequence, e sim que use a que já está lá. Há três coisas para acertar.

Caixa. O Oracle converte identificadores sem aspas para maiúsculas, então CREATE SEQUENCE order_seq cria ORDER_SEQ. O provider do EF sempre usa aspas. UseSequence("order_seq") produz:

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

"order_seq" e ORDER_SEQ são dois objetos diferentes. Contra um banco de dados existente, esse default falha com ORA-02289: sequence does not exist. Use o nome exatamente como SELECT sequence_name FROM user_sequences o exibe.

Schema. Se a sequence estiver em outro schema, passe-o para as duas chamadas:

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

O script gerado primeiro executa uma verificação em PL/SQL que lança ORA-01435 se o usuário SALES não existir, depois cria "SALES"."ORDER_SEQ" e um default de coluna "SALES"."ORDER_SEQ".NEXTVAL. O usuário da sua aplicação precisa de SELECT nessa sequence, ou os inserts falham em runtime mesmo que a migração tenha sido bem-sucedida com uma conta mais privilegiada.

Migrações. O EF não tem ExcludeFromMigrations para sequences. Se a sequence já existir, remova a chamada CreateSequence da primeira migração depois de gerá-la (o snapshot do modelo continua registrando a sequence, que é o que você quer), ou crie uma linha de base do banco de dados com uma migração inicial vazia.

Tabelas legadas preenchidas por trigger

Muitos schemas Oracle não têm default de coluna algum. Em vez disso, um trigger faz o trabalho:

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

Você pode mapear isso sem mexer no banco de dados: diga ao EF que o valor é gerado na inclusão e desative a convenção de identity para que uma migração futura não tente adicionar GENERATED ... AS IDENTITY à coluna.

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

Com esse mapeamento, o insert que o EF envia é, byte a byte, o mesmo bloco INSERT ... RETURNING "Id" INTO mostrado acima. O trigger roda antes de a linha ser gravada, e o RETURNING lê a linha final, então o EF recebe o valor do trigger.

Duas armadilhas com triggers:

No longo prazo, mover a lógica do trigger para um default de coluna e mapeá-lo com UseSequence é o formato mais limpo: um único mecanismo, visível no DDL, compreendido pelas migrações.

UseKeySequences para um modelo inteiro

Se cada tabela deve ter a sua própria sequence, UseKeySequences() aplica a estratégia Sequence a toda chave gerada:

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

Para Order, ele cria "OrderSequence" e DEFAULT ("OrderSequence".NEXTVAL). O nome é o nome da entidade mais o sufixo, e ele mantém a caixa mista porque o provider o coloca entre aspas. Quem escrever SQL puro agora precisa digitar "OrderSequence".NEXTVAL com as aspas. Se o seu padrão de nomenclatura é ORDERS_SEQ, mapeie as sequences por propriedade, ou combine isso com uma convenção de nomenclatura como as de convenções de nomenclatura personalizadas para chaves e índices.

Por que não UseHiLo

O provider aceita UseHiLo("ORDER_HILO") em uma chave int e faz o que o hi/lo faz: cria "ORDER_HILO" START WITH 1 INCREMENT BY 10, sem default de coluna, e no primeiro Add executa SELECT "ORDER_HILO".NEXTVAL FROM DUAL de forma síncrona para reservar um bloco. Na minha captura, três adds receberam os ids 1, 2 e 3 antes do SaveChanges, e o insert enviou as três chaves explicitamente.

Mas o README do 10.23.26301 diz, na seção de Sequences, que os métodos de extensão de HiLo não são suportados “except for columns with Char, UInt, ULong, and UByte data types”. Construir a sua estratégia de chaves sobre algo que o fornecedor documenta como não suportado para int e long é uma troca ruim, e o hi/lo tem outros dois custos no Oracle: outros processos que inserem sem o cliente do EF não conhecem o esquema de blocos, e a consulta extra de NEXTVAL acontece dentro do Add, o que é uma chamada síncrona ao banco de dados mesmo a partir de código assíncrono. Se você precisa das chaves antes do SaveChanges, gere um GUID ou um ULID no lado do cliente.

Migrando uma coluna identity para uma sequence

Esta é a migração de que a maioria das equipes realmente precisa: a tabela foi criada por uma versão anterior do EF com a identity padrão, e agora deve usar a ORDER_SEQ. Altere o mapeamento, adicione uma migração, e o provider gera:

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

Ele remove a identity e adiciona o default sem reconstruir a tabela, que é o que você quer. O que ele não tem como saber é quantas linhas já existem. StartsAt(1000) em uma tabela cujo MAX("Id") é 48210 significa que o primeiro insert após a implantação falha com ORA-00001: unique constraint (PK_Orders) violated, e os 47210 seguintes também. Antes de gerar a migração, consulte o máximo atual e defina StartsAt com folga acima dele, ou adicione um passo migrationBuilder.Sql(...) depois do CreateSequence que avance a sequence além dos dados.

Alterar StartsAt mais tarde produz uma RestartSequenceOperation, que o provider emite como:

ALTER SEQUENCE "ORDER_SEQ" RESTART START WITH 5000

ALTER SEQUENCE ... RESTART é documentado a partir do Oracle 19c. O README do provider ainda traz uma linha mais antiga dizendo “A sequence cannot be restarted”, então revise essa instrução antes de depender dela em uma migração de produção, especialmente se você a executa por meio de um migrations bundle em que ninguém lê o SQL no momento da implantação.

Lacunas, cache e RAC

Uma sequence nunca devolve valores. A referência de CREATE SEQUENCE da Oracle é explícita: o padrão é CACHE 20, os valores em cache são perdidos quando a instância falha, e os números usados em uma transação que sofre rollback são pulados. O provider emite CREATE SEQUENCE sem cláusula CACHE, então você fica com esse padrão. Lacunas são normais e baratas; não adicione NOCACHE para “corrigi-las”, isso serializa cada insert em uma atualização do dicionário de dados.

No RAC, cada instância mantém em cache o seu próprio intervalo, então as chaves de dois nós se intercalam fora de ordem. A Oracle observa que a ordenação “is usually not important for sequences used to generate primary keys”. Se você precisa de números sem lacunas e voltados a pessoas (números de nota fiscal com exigências legais), esse é outro problema: uma sequence é a ferramenta errada para isso em qualquer banco de dados.

Leia a seguir

Fontes

Comments

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

< Voltar