Как использовать последовательность Oracle для генерации первичных ключей в EF Core
Привязка ключа EF Core к последовательности Oracle через UseSequence в Oracle.EntityFrameworkCore 10: какой DDL и какой SQL для INSERT на самом деле генерирует провайдер, как сослаться на существующую последовательность или унаследованный триггер, почему на кавычках и HiLo спотыкаются чаще всего и как перевести identity-столбец на последовательность без ORA-00001.
Коротко: в 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() (так вы получите identity-столбец, а не свою последовательность), пишите имя последовательности ровно в том регистре, в котором его хранит Oracle (обычно в верхнем), и не используйте UseHiLo для ключей int/long: собственный README Oracle называет это неподдерживаемым.
Все, что ниже, проверено на Oracle.EntityFrameworkCore 10.23.26301 (опубликован 2026-09-08, сборка 10.0.23.1) с EF Core 10 и .NET 10 SDK 10.0.302. На машине, где я писал эту статью, нет Oracle Database, поэтому показанный SQL это то, что генерирует провайдер, снятое офлайн: GenerateCreateScript() для DDL, IMigrationsSqlGenerator для миграций и перехватчик команд, который забирает текст команды SaveChanges до того, как она ушла бы по сети. Никаких замеров времени и реальных обращений к базе здесь не заявляется.
Почему ключи в Oracle оказываются на последовательностях
Последовательности появились в Oracle задолго до identity-столбцов (те пришли в 12c), поэтому в большинстве доставшихся вам схем Oracle используется по одной последовательности на таблицу, обычно <TABLE>_SEQ, и часто триггер BEFORE INSERT, который копирует NEXTVAL в ключ. Даже в новых схемах команды выбирают последовательность вместо identity, потому что именованную последовательность можно разделить между несколькими таблицами, другие приложения могут читать ее через SELECT ORDER_SEQ.NEXTVAL FROM DUAL, а явные значения ключа она принимает без какого-либо особого режима.
Провайдер Oracle для EF Core все это поддерживает, но по умолчанию смотрит в другую сторону. Из коробки ключ int с именем Id получает стратегию IdentityColumn:
-- Oracle.EntityFrameworkCore 10.23.26301, default convention for an int key
"Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL,
Это identity-столбец на основе скрытой системной последовательности (ISEQ$$_...), а не той, которой вы дали имя. Если DBA выдал вам ORDER_SEQ или на ваших таблицах уже есть триггеры, это нужно явно указать в модели.
API провайдера 10.x
Провайдер Oracle поставляет собственное перечисление стратегий генерации значений, отдельное от SQL Server. Рефлексия по сборке 10.23.26301 дает:
OracleValueGenerationStrategy: None, SequenceHiLo, IdentityColumn, Sequence
OracleSQLCompatibility: DatabaseVersion19, DatabaseVersion21, DatabaseVersion23
и такие точки входа для последовательностей:
PropertyBuilder.UseSequence(string name = null, string schema = null): один ключ, одна именованная последовательность.ModelBuilder.UseKeySequences(string nameSuffix = null, string schema = null): по последовательности на сущность для каждого генерируемого ключа.PropertyBuilder.UseHiLo(...)иModelBuilder.UseHiLo(...): блоки hi/lo на стороне клиента.PropertyBuilder.UseIdentityColumn(...): поведение по умолчанию, заданное явно.
Обратите внимание, что перечисление совместимости начинается с 19. Старые версии провайдера принимали UseOracleSQLCompatibility("11"), и тогда провайдер создавал последовательность плюс триггер для каждого генерируемого ключа. В провайдере 10.x этого режима больше нет: значение DatabaseVersion19 дает тот же столбец GENERATED BY DEFAULT ON NULL AS IDENTITY, что и 23. Если нужны последовательности, их приходится запрашивать для каждого свойства.
Пошагово: привязка ключа к именованной последовательности
- Установите провайдер, соответствующий мажорной версии EF Core. Для EF Core 10 это
Oracle.EntityFrameworkCore10.23.x; nuspec фиксируетMicrosoft.EntityFrameworkCore.Relationalв диапазоне[10.0.0, 11.0.0), а сборки под EF Core 11 пока нет. - Объявите последовательность через
HasSequence, чтобы начальным значением, шагом и границами управляли миграции. - Вызовите
UseSequenceдля ключа с тем же именем (и схемой, если она есть). - Добавьте миграцию и прочитайте сгенерированный 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. Для новой схемы это нормально, но только явное объявление позволяет задать StartsAt, HasMax или IsCyclic.
Что на самом деле отправляет SaveChanges
Когда ключ привязан к последовательности, db.Orders.Add(new Order { Customer = "ACME" }) оставляет Id равным 0 и помечает его как временное значение. Затем SaveChanges отправляет один анонимный блок 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 cursor, чтобы EF мог записать его в order.Id. Это одно обращение к базе на пакет SaveChanges, и форма та же, что и в случае с identity-столбцом. С точки зрения EF и “identity”, и “значение по умолчанию из последовательности” означают “база генерирует значение при вставке”; вся разница живет в 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, когда до него дойдет, и эта вставка упадет на первичном ключе. Identity-столбцы провайдера (BY DEFAULT ON NULL) ведут себя так же. Практическая разница в том, что поведение именованной последовательности легко понять, ее можно сдвинуть за импортированные данные одним ALTER SEQUENCE, а другой код может вызвать NEXTVAL, чтобы зарезервировать ключ до вставки.
Ссылка на уже существующую последовательность
В существующей схеме обычно не нужно, чтобы EF создавал последовательность, нужно, чтобы он использовал ту, что уже есть. Здесь важны три вещи.
Регистр. Oracle переводит идентификаторы без кавычек в верхний регистр, поэтому CREATE SEQUENCE order_seq создает ORDER_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");
Сгенерированный скрипт сначала выполняет проверку на PL/SQL, которая выбрасывает ORA-01435, если пользователя SALES не существует, затем создает "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, что значение генерируется при добавлении, и отключите соглашение об identity, чтобы будущая миграция не попыталась добавить к столбцу 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 получает значение из триггера.
Две ловушки с триггерами:
- Безусловные триггеры конфликтуют с явными ключами. Если задать
Id = 500, EF отправляет значение и не запрашивает его обратно (см. второй перехваченный insert). Триггер, который всегда присваиваетNEXTVAL, перезаписывает его, и сущность в памяти по-прежнему говорит500, а строка содержит что-то другое. Либо пишите триггер какIF :NEW."Id" IS NULL THEN ... END IF;, либо никогда не задавайте ключи в коде. - Несколько генерируемых столбцов. Собственный README Oracle для 10.23.26301 документирует, что при скаффолдинге таблицы с более чем одним identity-столбцом или столбцом на последовательности/триггере
ValueGeneratedOnAdd()генерируется для всех них, и запросы затем падают сORA-50607или похожей ошибкой о том, что в таблице допустим только один identity-столбец. Документированный обходной путь: заменитьValueGeneratedOnAdd()наUseSequence("<the sequence the trigger uses>")для неключевых столбцов.
В долгосрочной перспективе чище перенести логику триггера в значение столбца по умолчанию и отобразить его через UseSequence: один механизм, видимый в 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
Провайдер принимает UseHiLo("ORDER_HILO") для ключа int и делает то, что делает hi/lo: создает "ORDER_HILO" START WITH 1 INCREMENT BY 10 без значения столбца по умолчанию и при первом Add синхронно выполняет SELECT "ORDER_HILO".NEXTVAL FROM DUAL, чтобы зарезервировать блок. В моем перехвате три добавления получили id 1, 2, 3 еще до SaveChanges, а вставка отправила все три ключа явно.
Но README 10.23.26301 в разделе о последовательностях говорит, что методы расширения HiLo не поддерживаются, “за исключением столбцов с типами данных Char, UInt, ULong и UByte”. Строить стратегию ключей на том, что поставщик документирует как неподдерживаемое для int и long, это плохая сделка, а у hi/lo в Oracle есть еще две издержки: другие клиенты, которые вставляют строки не через EF, ничего не знают о схеме блоков, а дополнительный запрос NEXTVAL выполняется внутри Add, то есть это синхронное обращение к базе даже из асинхронного кода. Если ключи нужны до SaveChanges, генерируйте GUID или ULID на стороне клиента.
Перевод identity-столбца на последовательность
Именно эта миграция нужна большинству команд: таблица была создана более ранней версией EF с identity по умолчанию, а теперь должна использовать 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)
Она удаляет identity и добавляет значение по умолчанию без пересоздания таблицы, что и требуется. Чего она знать не может, так это сколько строк уже есть в таблице. StartsAt(1000) для таблицы, у которой MAX("Id") равен 48210, означает, что первая вставка после развёртывания упадет с 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, кешированные значения теряются при сбое экземпляра, а номера, использованные в транзакции, которая откатилась, пропускаются. Провайдер генерирует CREATE SEQUENCE без предложения CACHE, так что вы получаете это значение по умолчанию. Пропуски нормальны и ничего не стоят; не добавляйте NOCACHE, чтобы их “исправить”: это сериализует каждую вставку на обновлении словаря данных.
В RAC каждый экземпляр кеширует собственный диапазон, поэтому ключи с двух узлов перемежаются не по порядку. Oracle отмечает, что порядок “обычно не важен для последовательностей, генерирующих первичные ключи”. Если нужны номера без пропусков, видимые людям (номера счетов с юридическими требованиями), это другая задача: последовательность для нее неподходящий инструмент в любой базе данных.
Что почитать дальше
- Как генерировать первичный ключ из последовательности базы данных при вставке в EF Core 11 разбирает ту же идею на SQL Server, где
UseSequenceберется из провайдера SqlServer, а вставка используетOUTPUTвместоRETURNING. - Исправление: The entity type requires a primary key to be defined на случай, когда представление Oracle или таблица без ключа после скаффолдинга не дают EF построить модель.
- Как добавить пользовательскую плюрализацию в dotnet ef dbcontext scaffold, если вы генерируете код по унаследованной схеме Oracle и имена таблиц получаются искаженными.
- Как переименовать таблицу в миграции EF Core 11 без потери данных о другой миграции, где предварительное чтение сгенерированного SQL спасает положение.
Источники
- Oracle.EntityFrameworkCore 10.23.26301 на NuGet, включая раздел README “Tips, Limitations, and Known Issues”.
- Документация ODP.NET Entity Framework Core и страница возможностей Oracle EF Core 7, где появился
UseSequence(). - Справочник Oracle SQL: CREATE SEQUENCE и ALTER SEQUENCE (19c).
- Последовательности в EF Core на Microsoft Learn.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.