Wst臋p

Co to s膮 sentinel values w EF Core 8 i jak z nich korzysta膰 馃槉

Przyk艂ad na pocz膮tek

Mamy tabel臋 Customers, kt贸ra wygl膮da tak:

CREATE TABLE [dbo].[Customers](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [nvarchar](max) NOT NULL,
	[Status] [int] NOT NULL,
	[Points] [int] NOT NULL,
	[Created] [datetime2](7) NOT NULL,

-- [... other, not relevant SQL code ...]

ALTER TABLE [dbo].[Customers] ADD  DEFAULT ((1)) FOR [Status]
ALTER TABLE [dbo].[Customers] ADD  DEFAULT ((10)) FOR [Points]
ALTER TABLE [dbo].[Customers] ADD  DEFAULT (getdate()) FOR [Created]
ALTER TABLE [dbo].[Customers] ADD  DEFAULT (N'') FOR [Name]

Bardzo prosta tabela, zaznacz臋 tylko 偶e Status to jest enum w kodzie.
Tabela zawiera informacje o klientach, ka偶dy klient posiada:

  • nazw臋,
  • status (new, verified, deactivated), domy艣lna warto艣膰 to: New oraz
  • liczb臋 punkt贸w, domy艣lna warto艣膰 to 10.

W kodzie konfiguracja pola Points wygl膮da tak:

modelBuilder.Entity<Customer>()
	.Property(x => x.Points)
	.IsRequired()
	.HasDefaultValue(10);

Przyda Nam si臋 to w dalszej cz臋艣ci wpisu. Konfiguracja innych p贸l Nas nie interesuje.

Dodawanie klient贸w

Dodajemy dw贸ch klient贸w:

  • dla pierwszego u偶ywamy domy艣lnych warto艣ci kolumn,
  • dla drugiego chcemy z g贸ry ustali膰 偶e b臋dzie mia艂 ZERO punkt贸w.
AddCustomer(new Customer {Name = "Default values, points: 10"});
AddCustomer(new Customer {Name = "Zero points", Points = 0 });

Co otrzymujemy w wyniku takiego kodu:

Id Name Status Points Created
1 Default values, points: 10 1 10 2024-05-11 05:45:24.9888016
2 Zero points 1 10 2024-05-11 05:45:24.9888016

Ka偶dy klient ma 10 punkt贸w. Przecie偶 jasno wskazali艣my, 偶e ma by膰 0.

Co si臋 sta艂o? 馃

Warto艣ci domy艣lne w konfiguracji EF Core

Warto艣膰 domy艣lna (CLR default) dla default(int) to jest w艂a艣nie 0.
EF Core dzia艂a tak, 偶e musi wiedzie膰 kiedy do bazy wstawi膰 t膮 nasz膮 warto艣膰 domy艣ln膮. Czyli 10 w przypadku punkt贸w. Por贸wnuje w艂a艣nie wstawian膮 warto艣膰 do domy艣lnej warto艣ci tego typu w .NET.

W naszym przypadku chcieli艣my wpisa膰 klientowi 0 punkt贸w, czyli default(int), wi臋c zosta艂a wstawiona nasza warto艣膰 domy艣lna czyli 10.

Sentinel values

I tutaj wchodz膮 do gry sentinel values czyli po polsku warto艣ci wartownicze 馃槉

呕eby m贸c wstawi膰 klientowi 0 punkt贸w trzeba skonfigurowa膰 pole w nast臋puj膮cy spos贸b:

modelBuilder.Entity<Customer>()
	.Property(x => x.Points)
	.IsRequired()
	.HasDefaultValue(10)
	.HasSentinel(-1);

To oznacza 偶e jak b臋dzie 0 to wstaw 0, jak b臋dzie -1 to wstaw warto艣膰 domy艣ln膮, czyli w tym przypadku 10.

Przy takiej konfiguracji do bazy zostan膮 zapisane takie dane:

Id Name Status Points Created
3 Default values, points: 10 1 0 2024-05-11 05:45:24.9888016
4 Zero points 1 0 2024-05-11 05:45:24.9888016

Ka偶dy klient ma 0 punkt贸w. Co tym razem si臋 sta艂o? 馃

Warto艣ci domy艣lne w modelu

Sta艂o si臋 to, 偶e pierwszy klient nie ma ustawionych punkt贸w, czyli default(int) = 0, a 0 mo偶na ju偶 wstawi膰 do tabeli i to si臋 w艂a艣nie sta艂o 馃槉

// TUTAJ JEST POINTS == 0
AddCustomer(new Customer {Name = "Default values, points: 10"});
AddCustomer(new Customer {Name = "Zero points", Points = 0 });

呕eby wszystko dzia艂a艂o tak jak chcemy, to trzeba w modelu zrobi膰 warto艣膰 domy艣ln膮 dla punkt贸w:

public sealed class Customer
{
    [...other code...]

    public int Points { get; init; } = 10; // << Domy艣lne 10 punkt贸w
    
    [...other code...]
}

Dopiero wtedy wszystko b臋dzie w bazie si臋 zapisywa艂o tak jak chcemy.

Id Name Status Points Created
5 Default values, points: 10 1 10 2024-05-11 05:45:24.9888016
6 Zero points 1 0 2024-05-11 05:45:24.9888016

W tym momencie warto艣膰 domy艣lna 10 jest w dw贸ch miejscach: w modelu oraz konfiguracji EF Core. 呕eby nie duplikowa膰 danych mo偶na od razu w modelu da膰 -1, t膮 warto艣膰 ustawion膮 jako wartownik (metoda HasSentinel) i wtedy wszystko b臋dzie dzia艂a膰 jak tego oczekujemy.

// sentinel value configure in OnModelCreating
public int Points { get; init; } = -1;

Drugi spos贸b

Inny sposobem na poradzenie sobie z tym jest zrobienie pola nullowalnego dla naszej w艂a艣ciwo艣ci i tam ogarni臋cie tej warto艣ci domy艣lnej.

private readonly int? _points;

public int Points
{
    get => _points ?? 10;
    init => _points = value;
}

W konfiguracji ju偶 nie trzeba robi膰 .HasSentinel(-1).

Podsumowanie

Jak wida膰 na powy偶szym przyk艂adzie trzeba uwa偶a膰 z warto艣ciami domy艣lnymi dla kolumn. Mo偶e to troch臋 popsu膰 nasze dane. Po pewnym czasie mo偶e si臋 okaza膰, 偶e na produkcji klient ma za du偶o punkt贸w lub za ma艂o ni偶 na prawd臋 powinien mie膰 馃槉.

Kod kt贸rego u偶y艂em we wpisie znajduje si臋 na moin GitHubie: https://github.com/tomaszprasolek/EfCore8_SentinelValues

Linki