Це продовження статті «Індекси, Обмеження». Читайте послідовно.
Check Constraint (обмеження перевірки) — SQL-вираз, що перевіряється базою даних при кожному INSERT і UPDATE. Якщо вираз повертає false — операція відхиляється з помилкою. Це остання лінія захисту цілісності даних після валідації на рівні C# і бізнес-логіки.
Здавалося б: у нас є FluentValidation, є доменні валідатори, є перевірки у сервісному шарі. Навіщо ще й Check Constraint у базі?
Тому що дані можуть потрапити до бази в обхід вашого C# коду:
Check Constraint — це інваріант у самій базі. Він не знає нічого про C#, але гарантує, що правило «кількість не може бути від'ємною» ніколи не порушиться, хто б не писав у таблицю.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public decimal? DiscountPercent { get; set; }
public int Stock { get; set; }
public int ReservedStock { get; set; }
public DateTime? ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
}
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).IsRequired().HasMaxLength(200);
builder.Property(p => p.Price).HasPrecision(12, 2);
builder.Property(p => p.DiscountPercent).HasPrecision(5, 2);
// Ціна не може бути від'ємною
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Products_Price",
"[Price] >= 0"
));
// Знижка від 0 до 100 відсотків (або NULL — немає знижки)
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Products_DiscountPercent",
"[DiscountPercent] IS NULL OR ([DiscountPercent] >= 0 AND [DiscountPercent] <= 100)"
));
// Залишок не може бути від'ємним
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Products_Stock",
"[Stock] >= 0"
));
// Зарезервований не може перевищувати загальний залишок
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Products_ReservedStock",
"[ReservedStock] >= 0 AND [ReservedStock] <= [Stock]"
));
// Дата закінчення строку (якщо є) — не в минулому відносно CreatedAt
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Products_ExpiresAt",
"[ExpiresAt] IS NULL OR [ExpiresAt] > [CreatedAt]"
));
}
}
Генерований DDL (SQL Server):
CREATE TABLE [Products] (
[Id] INT NOT NULL IDENTITY,
[Name] NVARCHAR(200) NOT NULL,
[Price] DECIMAL(12,2) NOT NULL,
[DiscountPercent] DECIMAL(5,2) NULL,
[Stock] INT NOT NULL,
[ReservedStock] INT NOT NULL,
[ExpiresAt] DATETIME2 NULL,
[CreatedAt] DATETIME2 NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
CONSTRAINT [CK_Products_Price] CHECK ([Price] >= 0),
CONSTRAINT [CK_Products_DiscountPercent] CHECK ([DiscountPercent] IS NULL OR
([DiscountPercent] >= 0 AND [DiscountPercent] <= 100)),
CONSTRAINT [CK_Products_Stock] CHECK ([Stock] >= 0),
CONSTRAINT [CK_Products_ReservedStock] CHECK ([ReservedStock] >= 0
AND [ReservedStock] <= [Stock]),
CONSTRAINT [CK_Products_ExpiresAt] CHECK ([ExpiresAt] IS NULL
OR [ExpiresAt] > [CreatedAt])
);
Тепер будь-яка спроба вставити Price = -50 або ReservedStock > Stock — база поверне помилку, незалежно від того, хто і як пише у таблицю.
public class Order
{
public int Id { get; set; }
public string Status { get; set; } = "Pending";
public string PaymentMethod { get; set; } = string.Empty;
public int Priority { get; set; }
}
builder.ToTable(tb =>
{
// Enum як рядок: обмежити допустимі значення
tb.HasCheckConstraint(
"CK_Orders_Status",
"[Status] IN ('Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled', 'Refunded')"
);
// Числовий пріоритет: від 1 до 5
tb.HasCheckConstraint(
"CK_Orders_Priority",
"[Priority] BETWEEN 1 AND 5"
);
// PaymentMethod: допустимі значення
tb.HasCheckConstraint(
"CK_Orders_PaymentMethod",
"[PaymentMethod] IN ('CreditCard', 'DebitCard', 'BankTransfer', 'Cash', 'Crypto')"
);
});
// PostgreSQL підтримує regex у CHECK:
builder.ToTable(tb => tb.HasCheckConstraint(
"CK_Customers_Email",
@"""Email"" ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'"
));
-- PostgreSQL:
CONSTRAINT "CK_Customers_Email" CHECK (
"Email" ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
)
SQL Server не підтримує regex у CHECK без UDF. Для складних валідацій — або додаткова логіка у C#, або SQL UDF.
Database Sequence (послідовність бази даних) — об'єкт БД, що генерує монотонно зростаючі числа. На відміну від IDENTITY/AUTOINCREMENT (прив'язані до конкретної таблиці), Sequence — незалежний об'єкт, що може бути спільним для кількох таблиць або використовуватися для будь-яких цілей.
Стандартний IDENTITY генерує ID в момент INSERT, а значення стає відомим лише після INSERT. Це ускладнює:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Проста послідовність
modelBuilder.HasSequence<int>("invoice_number_seq")
.StartsAt(10000) // починаємо з 10000
.IncrementsBy(1); // збільшення на 1
// Послідовність з циклом (після max → починає знову)
modelBuilder.HasSequence<long>("batch_id_seq")
.StartsAt(1)
.IncrementsBy(1)
.HasMin(1)
.HasMax(long.MaxValue)
.IsCyclic(false); // false = зупинитися при max
// Послідовність у конкретній схемі
modelBuilder.HasSequence<int>("order_seq", schema: "billing")
.StartsAt(1)
.IncrementsBy(10); // блоки по 10 (Hi-Lo варіант)
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
-- SQL Server DDL:
CREATE SEQUENCE [invoice_number_seq]
AS INT
START WITH 10000
INCREMENT BY 1
NO CYCLE;
CREATE SEQUENCE [billing].[order_seq]
AS INT
START WITH 1
INCREMENT BY 10;
public class Invoice
{
public int Id { get; set; }
public int InvoiceNumber { get; set; } // з послідовності
public string Title { get; set; } = string.Empty;
}
public class InvoiceConfiguration : IEntityTypeConfiguration<Invoice>
{
public void Configure(EntityTypeBuilder<Invoice> builder)
{
builder.HasKey(i => i.Id);
// InvoiceNumber береться з послідовності, не з IDENTITY
builder.Property(i => i.InvoiceNumber)
.HasDefaultValueSql("NEXT VALUE FOR [invoice_number_seq]")
.ValueGeneratedOnAdd();
builder.Property(i => i.Title).IsRequired().HasMaxLength(200);
// InvoiceNumber унікальний
builder.HasIndex(i => i.InvoiceNumber).IsUnique();
}
}
CREATE TABLE [Invoices] (
[Id] INT NOT NULL IDENTITY,
[InvoiceNumber] INT NOT NULL DEFAULT (NEXT VALUE FOR [invoice_number_seq]),
[Title] NVARCHAR(200) NOT NULL,
CONSTRAINT [PK_Invoices] PRIMARY KEY ([Id]),
CONSTRAINT [UX_Invoices_InvoiceNumber] UNIQUE ([InvoiceNumber])
);
PostgreSQL (nextval):
builder.Property(i => i.InvoiceNumber)
.HasDefaultValueSql("nextval('invoice_number_seq')")
.ValueGeneratedOnAdd();
Hi-Lo (High-Low) — алгоритм, що отримує блоки ID з послідовності замість одного за раз. EF Core бере один блок (наприклад, 10 ID) і генерує всі 10 у пам'яті без додаткових запитів до БД.
public class AppDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Послідовність з кроком 10 (блок Hi-Lo)
modelBuilder.HasSequence<int>("entity_id_seq")
.StartsAt(1)
.IncrementsBy(10);
// Всі entity використовують Hi-Lo з цієї послідовності
modelBuilder.UseHiLo("entity_id_seq");
// Або для конкретної entity:
// modelBuilder.Entity<Product>().Property(p => p.Id).UseHiLo("entity_id_seq");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
Як Hi-Lo працює:
SELECT NEXT VALUE FOR entity_id_seq → отримує, наприклад, 101101 * 10 до 101 * 10 + 9 = ID від 1010 до 1019SELECT NEXT VALUE FORДля TPC-ієрархій UseSequence — більш чистий підхід, ніж UseHiLo:
modelBuilder.HasSequence<int>("publication_id_seq")
.StartsAt(1)
.IncrementsBy(1);
modelBuilder.Entity<Publication>(b =>
{
b.UseTpcMappingStrategy();
b.Property(p => p.Id)
.UseSequence("publication_id_seq");
});
CREATE TABLE [Books] (
[Id] INT NOT NULL DEFAULT (NEXT VALUE FOR [publication_id_seq]),
-- ...
);
CREATE TABLE [Articles] (
[Id] INT NOT NULL DEFAULT (NEXT VALUE FOR [publication_id_seq]),
-- Та сама послідовність! Глобально унікальні ID
);
Collation (зіставлення) визначає правила порівняння і сортування символів у рядках. Це впливає на:
'А' = 'а' (case sensitive чи insensitive)'е' = 'є' (accent sensitive чи insensitive)public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Login { get; set; } = string.Empty;
}
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasKey(c => c.Id);
// Name: Ukrainian CI (case insensitive), AS (accent sensitive)
builder.Property(c => c.Name)
.IsRequired()
.HasMaxLength(200)
.UseCollation("Ukrainian_CI_AS"); // SQL Server
// Email: case insensitive (latin)
builder.Property(c => c.Email)
.IsRequired()
.HasMaxLength(320)
.UseCollation("SQL_Latin1_General_CP1_CI_AS");
// Login: case sensitive! login ≠ Login
builder.Property(c => c.Login)
.IsRequired()
.HasMaxLength(50)
.UseCollation("Latin1_General_CS_AS");
}
}
CREATE TABLE [Customers] (
[Id] INT NOT NULL IDENTITY,
[Name] NVARCHAR(200) NOT NULL COLLATE Ukrainian_CI_AS,
[Email] NVARCHAR(320) NOT NULL COLLATE SQL_Latin1_General_CP1_CI_AS,
[Login] NVARCHAR(50) NOT NULL COLLATE Latin1_General_CS_AS,
-- ...
);
// OnModelCreating: Collation для всіх рядкових стовпців за замовчуванням
modelBuilder.UseCollation("Ukrainian_CI_AS");
-- SQL Server: змінює collation бази даних
ALTER DATABASE [MyDatabase] COLLATE Ukrainian_CI_AS;
// PostgreSQL: ICU collation
builder.Property(c => c.Name)
.UseCollation("uk-UA-x-icu"); // Українська через ICU
// Або для всього DbContext:
modelBuilder.UseCollation("uk-UA-x-icu");
// З Ukrainian_CI_AS (case insensitive):
context.Customers.Where(c => c.Name == "іван")
// Знайде: "Іван", "ІВАН", "іван", "іВан" — всі варіанти
// З Latin1_General_CS_AS (case sensitive):
context.Customers.Where(c => c.Login == "john")
// Знайде лише "john", НЕ "John" або "JOHN"
public class Payment
{
public int Id { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; } = string.Empty;
public int GatewayCode { get; set; }
}
public class PaymentConfiguration : IEntityTypeConfiguration<Payment>
{
public void Configure(EntityTypeBuilder<Payment> builder)
{
// Коментар до таблиці
builder.ToTable(tb => tb.HasComment("Таблиця платежів. GDPR: не зберігати PAN."));
builder.HasKey(p => p.Id);
builder.Property(p => p.Amount)
.HasPrecision(14, 2)
.HasComment("Сума у валюті Currency. Завжди позитивна.");
builder.Property(p => p.Currency)
.HasMaxLength(3)
.IsUnicode(false)
.HasComment("ISO 4217 код валюти (UAH, USD, EUR).");
builder.Property(p => p.GatewayCode)
.HasComment("Код повернення від платіжного шлюзу. 0 = успіх, інші = помилки по специфікації шлюзу.");
}
}
-- SQL Server: через extended properties
EXEC sp_addextendedproperty
@name = N'MS_Description',
@value = N'Таблиця платежів. GDPR: не зберігати PAN.',
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = 'Payments';
-- PostgreSQL: через COMMENT ON
COMMENT ON TABLE "Payments" IS 'Таблиця платежів. GDPR: не зберігати PAN.';
COMMENT ON COLUMN "Payments"."Amount" IS 'Сума у валюті Currency. Завжди позитивна.';
Коментарі видно у SQL Management Studio, pgAdmin, DBeaver — корисно для DBA і інших команд.
Schema (схема) у SQL Server і PostgreSQL — простір імен для таблиць. Це дозволяє логічно групувати таблиці різних модулів у одній базі.
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Invoice> Invoices => Set<Invoice>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Всі таблиці за замовчуванням у схемі "sales"
modelBuilder.HasDefaultSchema("sales");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
-- Таблиці отримують схему:
CREATE TABLE [sales].[Orders] (...);
CREATE TABLE [sales].[Invoices] (...);
CREATE TABLE [sales].[Customers] (...);
public class HrEmployeeConfiguration : IEntityTypeConfiguration<Employee>
{
public void Configure(EntityTypeBuilder<Employee> builder)
{
// Перевизначити схему для конкретної таблиці
builder.ToTable("Employees", schema: "hr");
builder.HasKey(e => e.Id);
// ...
}
}
-- Ця таблиця у схемі "hr", а не "sales"
CREATE TABLE [hr].[Employees] (...);
// DbContext для модуля Product Catalog
public class CatalogDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("catalog");
}
}
// DbContext для модуля Orders
public class OrdersDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("orders");
}
}
// DbContext для HR
public class HrDbContext : DbContext
{
public DbSet<Employee> Employees => Set<Employee>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("hr");
}
}
Всі три DbContext — в одній фізичній базі, але у різних схемах. Це дозволяє:
hr схемою)Зберемо всі концепції разом у реальному сценарії — мінімальний e-commerce з правильно налаштованою схемою:
public class EcommerceDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("shop");
// Глобальна послідовність для Order Number
modelBuilder.HasSequence<long>("order_number_seq", schema: "shop")
.StartsAt(100001)
.IncrementsBy(1);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(EcommerceDbContext).Assembly);
}
}
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products", schema: "shop",
tb => tb.HasComment("Каталог товарів магазину"));
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).IsRequired().HasMaxLength(300)
.HasComment("Повна назва товару для відображення покупцю");
builder.Property(p => p.Sku).IsRequired().HasMaxLength(50).IsUnicode(false)
.HasComment("Stock Keeping Unit — внутрішній артикул");
builder.Property(p => p.Price).HasPrecision(12, 2).IsRequired()
.HasComment("Ціна у гривнях (UAH) включно з ПДВ");
builder.Property(p => p.Stock).IsRequired()
.HasComment("Кількість одиниць на складі. Не може бути від'ємним.");
// Бізнес-правила як Check Constraints
builder.ToTable(tb =>
{
tb.HasCheckConstraint("CK_Products_Price", "[Price] >= 0");
tb.HasCheckConstraint("CK_Products_Stock", "[Stock] >= 0");
tb.HasCheckConstraint("CK_Products_Sku_NotEmpty", "LEN(TRIM([Sku])) > 0");
});
// Alternate Key на SKU (для FK з OrderItem)
builder.HasAlternateKey(p => p.Sku)
.HasName("AK_Products_Sku");
// Індекси
builder.HasIndex(p => p.CategoryId)
.HasDatabaseName("IX_Products_CategoryId");
builder.HasIndex(p => new { p.IsActive, p.Price })
.HasFilter("[IsActive] = 1")
.HasDatabaseName("IX_Products_Active_Price");
builder.HasIndex(p => p.Name)
.HasDatabaseName("IX_Products_Name"); // для пошуку за назвою
}
}
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.ToTable("Customers", schema: "shop");
builder.HasKey(c => c.Id);
builder.Property(c => c.Email)
.IsRequired().HasMaxLength(320)
.UseCollation("SQL_Latin1_General_CP1_CI_AS"); // CI для email
builder.Property(c => c.PhoneNumber).HasMaxLength(20);
// Фільтровані унікальні індекси (soft delete friendly)
builder.HasIndex(c => c.Email)
.IsUnique()
.HasFilter("[IsDeleted] = 0")
.HasDatabaseName("UX_Customers_Email_Active");
// Check Constraints
builder.ToTable(tb =>
{
tb.HasCheckConstraint("CK_Customers_Email",
"LEN([Email]) >= 5 AND [Email] LIKE '%@%.%'");
});
}
}
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders", schema: "shop",
tb => tb.HasComment("Замовлення покупців"));
builder.HasKey(o => o.Id);
// OrderNumber з послідовності
builder.Property(o => o.OrderNumber)
.HasDefaultValueSql("NEXT VALUE FOR [shop].[order_number_seq]")
.ValueGeneratedOnAdd()
.HasComment("Унікальний номер замовлення для покупця");
builder.Property(o => o.Status)
.IsRequired().HasMaxLength(20)
.HasConversion<string>();
builder.Property(o => o.TotalAmount).HasPrecision(14, 2);
builder.Property(o => o.TaxAmount).HasPrecision(14, 2);
// Check Constraints
builder.ToTable(tb =>
{
tb.HasCheckConstraint("CK_Orders_TotalAmount", "[TotalAmount] >= 0");
tb.HasCheckConstraint("CK_Orders_TaxAmount", "[TaxAmount] >= 0");
tb.HasCheckConstraint("CK_Orders_Status",
"[Status] IN ('Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled', 'Refunded')");
});
// FK до Customer
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
// Індекси
builder.HasIndex(o => o.CustomerId)
.HasDatabaseName("IX_Orders_CustomerId");
builder.HasIndex(o => new { o.Status, o.PlacedAt })
.HasDatabaseName("IX_Orders_Status_PlacedAt");
builder.HasIndex(o => o.OrderNumber)
.IsUnique()
.HasDatabaseName("UX_Orders_OrderNumber");
}
}
Завдання 1.1: Check Constraints для бронювання
HotelReservation (Id, CheckInDate, CheckOutDate, GuestCount, RoomType, PricePerNight, TotalPrice). Додайте обмеження:
CheckOutDate > CheckInDateGuestCount BETWEEN 1 AND 10PricePerNight > 0TotalPrice приблизно дорівнює PricePerNight * DATEDIFF(day, CheckInDate, CheckOutDate) — але з урахуванням можливих знижок: TotalPrice >= PricePerNight * DATEDIFF(day, CheckInDate, CheckOutDate) * 0.5RoomType IN ('Standard', 'Deluxe', 'Suite', 'Presidential')Завдання 1.2: Sequence для Invoice Number
Реалізуйте InvoiceNumber для Invoice, що генерується як INV-2024-000001 (рік + 6-значний номер з послідовності). Підказка: HasDefaultValueSql не підтримує конкатенацію при звичайному використанні — розгляньте computed column або генерацію у C# через ValueGenerator.
Завдання 1.3: Схеми для Bounded Contexts
Розбийте один AppDbContext на три: IdentityDbContext (схема identity), CatalogDbContext (схема catalog), FinanceDbContext (схема finance). Напишіть три міграції для кожного контексту. Як реєструвати кілька DbContext у DI?
Завдання 2.1: Комплексні Check Constraints
Для Employee (Id, FirstName, LastName, Salary, HireDate, TerminationDate?, Role, ManagerId?) додайте:
Salary > 0TerminationDate IS NULL OR TerminationDate > HireDateRole = 'Manager' — ManagerId IS NULL (менеджер не може мати іншого менеджера)Role != 'Manager' — ManagerId IS NOT NULL (у не-менеджерів має бути менеджер)Остання умова — чи можна реалізувати через один CHECK CONSTRAINT? Чи потрібні два? Напишіть тест, що верифікує ці правила.
Завдання 2.2: Collation для пошуку
Є Document (Id, Title, Content, AuthorName). Реалізуйте:
Title і AuthorName: Ukrainian CI AS (пошук без урахування регістру і акцентів)Content: стандартне Latin collation (технічний контент)Напишіть запит пошуку: WHERE Title LIKE @pattern OR AuthorName LIKE @pattern. Переконайтеся, що пошук за "іван" знаходить документи з "Іван" і "ІВАН".
Завдання 3.1: Повна схема для SaaS-платформи
Реалізуйте схему для SaaS з multi-tenancy через схеми БД:
platform: Tenant (Id, Name, PlanId, CreatedAt), Plan (Id, Name, MaxUsers, MaxStorage){tenantSlug}: кожен тенант має власну схему БДПитання для дослідження:
TenantId в кожній таблиці (row-level security) — порівняйте підходиЦя стаття охопила всі аспекти обмежень та схеми бази даних в EF Core:
Частина 1 — Індекси:
HasIndex, IsUnique, складені індекси (правило лівого префіксу)HasFilter) — ідеальні для soft deleteIncludeProperties) — уникаємо bookmark lookupЧастина 2 — Обмеження та схема:
HasCheckConstraint — інваріанти бізнес-логіки на рівні базиHasSequence, UseHiLo, UseSequence для TPCUseCollation — правила порівняння рядків (CI vs CS, accents)HasComment — документація схеми для DBAHasDefaultSchema і ToTable(schema:) — логічне розбиття на модуліНаступна стаття — Seed Data (стаття 14) — розкриє HasData(), міграційний seeding та стратегії початкового наповнення бази.
Індекси, Обмеження та Схема (Частина 1)
Глибокий розбір індексів в EF Core — HasIndex, унікальні індекси, складені, фільтровані, covering індекси. Як B-tree індексує дані, коли індекс допомагає, а коли шкодить. Primary Key vs Alternate Key vs Unique Index.
Seed Data — Початкові Дані (Частина 1)
Стратегії наповнення бази початковими даними в EF Core — HasData() через Fluent API, обмеження і практичні нюанси, Alternate Key у seed data, маппінг Shadow Properties, Data Seeding через Initialization сервіс.