Wszystkie wpisy

Więzy integralności w SQL - Primary Key, Foreign Key, CASCADE, UNIQUE, CHECK

Mechanizmy obronne bazy danych - Primary Key (INT IDENTITY kontra GUID), Foreign Key i integralność referencyjna, trzy strategie ON DELETE (CASCADE/SET NULL/NO ACTION), constraints UNIQUE i CHECK jako walidacja na poziomie bazy. Dwudziesta czwarta część kursu C#.


W poprzednim wpisie SQL w praktyce - DDL i DML poznałeś fundamenty języka SQL. Teraz zejdziemy głębiej w mechanizmy obronne bazy danych - więzy integralności (constraints), czyli reguły, które silnik bazy wymusza automatycznie, odrzucając zapytania naruszające spójność danych.

Więzy integralności są w tabeli tym, czym walidacja formularza w UI - ale na znacznie niższym poziomie. Nawet gdy aplikacja ma bug i próbuje zapisać cenę -100 zł albo wskazać nieistniejącego użytkownika, baza mówi “nie”. W tej części poznasz cztery główne typy constraints (PK, FK, UNIQUE, CHECK), trzy strategie kaskadowego zachowania przy usuwaniu (CASCADE/SET NULL/NO ACTION), oraz kluczowy wybór projektowy: INT IDENTITY kontra GUID jako typ Primary Key.

1. Primary Key - fundament identyfikacji #

Klucz Główny to unikalny identyfikator wiersza. Baza używa go do:

  • Szybkiego wyszukiwania konkretnego rekordu (WHERE Id = 42)
  • Wymuszania relacji przez Foreign Keys w innych tabelach
  • Automatycznego tworzenia indeksu klastrowego (SQL Server) - fizyczne uporządkowanie danych na dysku

Trzy wymagane cechy PK #

CechaWymóg
UNIQUEŻadne dwa wiersze nie mogą mieć tej samej wartości
NOT NULLKażdy wiersz musi mieć wartość (NULL zabroniony)
Jeden na tabelęDokładnie jeden PK; może być złożony z wielu kolumn

Konwencja: pojedyncza kolumna Id (INT, BIGINT lub UNIQUEIDENTIFIER) - łatwo współpracuje z ORM (Entity Framework Core rozpoznaje ją automatycznie).

Dwa typy PK w produkcji #

Wybór A: INT IDENTITY (lub BIGINT)

CREATE TABLE Users (
    Id INT IDENTITY(1,1) PRIMARY KEY,   -- 1, 2, 3, 4...
    Name NVARCHAR(50)
);
ZaletyWady
Mały rozmiar (4 B), szybkie sortowaniePrzewidywalność - konkurencja z /order/502 w URL
Sekwencyjny → efektywny indeks klastrowyKonflikty przy łączeniu baz z wielu regionów
Naturalna kolejność chronologicznaNie da się generować po stronie klienta

Wybór B: GUID / UNIQUEIDENTIFIER

CREATE TABLE Users (
    Id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
    Name NVARCHAR(50)
);

W C#:

public class User
{
    public Guid Id { get; set; } = Guid.NewGuid();   // generowane przed wysłaniem do bazy
    public string Name { get; set; }
}
ZaletyWady
Globalna unikalność - zero kolizji między bazami16 B (4× większy niż INT)
Można generować w aplikacji przed INSERTLosowy → fragmentacja indeksu klastrowego
Trudne do zgadnięcia (/order/6F9619FF-8B86-D011-...)Wolniejsze sortowanie, JOINy

Reguła kciuka:

  • Aplikacja monolityczna, jedna baza → INT IDENTITY (prostota, wydajność)
  • Rozproszony system z wieloma bazami, publiczne URL wymagające nieprzewidywalności, generowanie ID w aplikacji przed zapisem → GUID

Dla większości aplikacji biznesowych INT wystarcza. GUID sięgaj tylko gdy realnie potrzebujesz globalnej unikalności.

2. Foreign Key - integralność referencyjna #

Foreign Key łączy dwa wiersze - w tej samej lub innej tabeli. Kolumna “dziecka” zawiera wartość Primary Key “rodzica”. FK to nie tylko relacja - to gwarancja spójności:

Bez FK aplikacja może swobodnie tworzyć orphan records (osierocone rekordy):

-- Bez FK - baza pozwala
INSERT INTO Orders (UserId, Product) VALUES (999, 'Laptop');
-- Order wskazuje na nieistniejącego usera. Później aplikacja próbuje
-- wyświetlić order i wyświetla 'null' zamiast nazwy klienta. Bug.

Z FK baza odrzuca takie próby:

CREATE TABLE Orders (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    UserId INT NOT NULL,
    Product NVARCHAR(100),

    CONSTRAINT FK_Orders_Users
        FOREIGN KEY (UserId) REFERENCES Users(Id)
);

INSERT INTO Orders (UserId, Product) VALUES (999, 'Laptop');
-- ❌ ERROR: The INSERT statement conflicted with the FOREIGN KEY constraint...

To jest integralność referencyjna - baza sama pilnuje, by relacje między tabelami były poprawne. Aplikacja może mieć błąd, próbować dowolnych wartości - baza jest ostatnią linią obrony.

Pełny przykład Departamenty ↔ Pracownicy #

-- Tabela rodzic
CREATE TABLE Departamenty (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Nazwa NVARCHAR(100) NOT NULL
);

-- Tabela dziecko z FK
CREATE TABLE Pracownicy (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Imie NVARCHAR(50) NOT NULL,
    Nazwisko NVARCHAR(50) NOT NULL,
    DepartamentId INT NOT NULL,

    CONSTRAINT FK_Pracownicy_Departamenty
        FOREIGN KEY (DepartamentId)
        REFERENCES Departamenty(Id)
);

Silnik gwarantuje:

  • INSERT pracownika z DepartamentId = 5 gdy departament 5 nie istnieje → odrzucone
  • DELETE departamentu 5 gdy istnieją pracownicy z DepartamentId = 5 → odrzucone (domyślnie)
  • UPDATE pracownika ustawiający DepartamentId = 999 gdy 999 nie istnieje → odrzucone

3. Trzy strategie ON DELETE #

Domyślnie próba usunięcia rodzica z istniejącymi dziećmi kończy się błędem (bezpieczna blokada). Możesz to zmienić deklarując zachowanie:

ON DELETE NO ACTION (domyślne) / RESTRICT #

CONSTRAINT FK_Pracownicy_Departamenty
    FOREIGN KEY (DepartamentId) REFERENCES Departamenty(Id)
    ON DELETE NO ACTION

Silnik odrzuca próbę usunięcia rodzica gdy istnieją dzieci. Musisz najpierw ręcznie posprzątać. Bezpieczna decyzja - wymusza świadome podjęcie decyzji.

ON DELETE CASCADE - kaskadowe usuwanie #

CONSTRAINT FK_KoszykZawartosc_Koszyki
    FOREIGN KEY (KoszykId) REFERENCES Koszyki(Id)
    ON DELETE CASCADE

Usunięcie rodzica automatycznie kasuje wszystkie powiązane dzieci.

Dobre użycie:

  • KoszykPozycjaKoszyka (usunąć koszyk = usunąć pozycje)
  • PlikTagiPliku (usunąć plik = usunąć tagi)
  • WpisBlogaKomentarze (usunąć wpis = usunąć komentarze)

Anti-pattern (klasyczna katastrofa):

  • KlientFaktura z CASCADE - usunięcie klienta kasuje historię faktur wymaganą przez US
  • UzytkownikLogAudytowy z CASCADE - usunięcie usera zaciera ślad audytowy
  • KontoTransakcje z CASCADE - usunięcie konta kasuje historię finansową

Reguła: CASCADE tylko dla ściśle podrzędnych danych bez wartości historycznej.

ON DELETE SET NULL #

CONSTRAINT FK_Pracownik_Menedzer
    FOREIGN KEY (MenedzerId) REFERENCES Pracownicy(Id)
    ON DELETE SET NULL

Usunięcie rodzica ustawia FK dzieci na NULL. Warunek: kolumna FK musi zezwalać na NULL (INT NULL, nie INT NOT NULL).

Klasyczne zastosowanie: luźne powiązanie, gdzie dziecko żyje bez rodzica:

  • Pracownik.MenedzerId → Pracownik.Id - menedżer odchodzi z firmy, jego podwładni pracują dalej bez szefa (do czasu nowego przydziału)
  • Artykul.KategoriaId → Kategoria.Id - kategoria usunięta, artykuł zostaje jako “nieskategoryzowany”

Podsumowanie strategii #

StrategiaZachowanie przy usunięciu rodzicaKiedy używać
NO ACTION / RESTRICTBłąd - odrzuca operacjęDomyślne, bezpieczne; wymusza świadome działanie
CASCADEAutomatyczne usunięcie dzieciŚciśle podrzędne dane (koszyk↔pozycje, plik↔tagi)
SET NULLFK dzieci → NULLLuźne powiązania (menedżer↔podwładni)

4. UNIQUE - unikalność bez bycia PK #

Primary Key jest jeden na tabelę. Ale kolumn wymagających unikalności może być kilka. UNIQUE to constraint zapewniający unikalność bez bycia PK:

CREATE TABLE Users (
    Id INT IDENTITY(1,1) PRIMARY KEY,        -- PK
    Email VARCHAR(100) UNIQUE NOT NULL,      -- unikalny email
    Username NVARCHAR(50) UNIQUE NOT NULL,   -- unikalny login
    Pesel CHAR(11) UNIQUE,                   -- unikalny PESEL (może być NULL)
    Name NVARCHAR(100)
);

Próba INSERT drugiego usera z tym samym emailem → błąd bazy.

Różnica PK kontra UNIQUE:

Primary KeyUNIQUE
Ilość w tabeliDokładnie 1Dowolna liczba
NULL❌ Niedozwolone✅ Dozwolone (jeden NULL w SQL Server)
CelIdentyfikacja rekorduWymuszenie unikalności biznesowej
Indeks klastrowyDomyślnie takDomyślnie nie (tylko non-clustered)

Typowe zastosowania UNIQUE:

  • Email użytkownika (można się zalogować tylko jednym kontem na maila)
  • Numer telefonu (jeśli ma być unikalny)
  • Kod kreskowy produktu
  • PESEL, NIP, REGON
  • Alias URL (slug) - /posts/my-post-title - dwa posty nie mogą mieć tego samego slug

5. CHECK - walidacja logiczna #

CHECK wymusza dowolny warunek logiczny na kolumnie lub zestawie kolumn. To ostatnia linia obrony przed niepoprawnymi danymi.

Proste warunki #

CREATE TABLE Produkty (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Nazwa NVARCHAR(100) NOT NULL,
    Cena DECIMAL(10, 2) NOT NULL,

    -- Cena musi być dodatnia
    CONSTRAINT CHK_Cena_Dodatnia CHECK (Cena > 0)
);

INSERT INTO Produkty (Nazwa, Cena) VALUES (N'Laptop', -100);
-- ❌ ERROR: CHK_Cena_Dodatnia

Zakresy i wartości domenowe #

CREATE TABLE Uzytkownicy (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Wiek INT NOT NULL CHECK (Wiek BETWEEN 0 AND 120),
    Status NVARCHAR(20) NOT NULL
        CHECK (Status IN ('Active', 'Paused', 'Deleted')),
    Email VARCHAR(100) NOT NULL
        CHECK (Email LIKE '%@%.%')
);

CHK_Status_Domena blokuje dowolny status inny niż trzy wartości - baza nie pozwoli wstawić 'Fake', 'ACTIVE' (wielkość znaków!), NULL.

Warunki między kolumnami #

CREATE TABLE Zamowienia (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    DataZlozenia DATETIME2 NOT NULL,
    DataDostawy DATETIME2 NULL,

    -- Data dostawy nie może być przed datą złożenia
    CONSTRAINT CHK_Daty_Kolejnosc
        CHECK (DataDostawy IS NULL OR DataDostawy >= DataZlozenia)
);

CHECK sprawdza logiczną spójność między dwoma kolumnami tego samego wiersza.

Wielowarstwowa walidacja #

W dojrzałych aplikacjach walidacja żyje na wielu poziomach:

WarstwaRodzaj walidacjiPrzykład
UI (frontend)Natychmiastowy feedback”Cena musi być liczbą” w formularzu
Aplikacja (backend)Walidacja biznesowa”Cena musi mieszać się w widełkach 1-99999”
CHECK (baza)Constraint na poziomie danychCHECK (Cena > 0)

Każda warstwa łapie inne rodzaje błędów: UI - literówki użytkownika; aplikacja - reguły biznesowe; baza - błędy aplikacji, bezpośrednie połączenia SQL, migracje danych z zewnątrz. Wszystkie trzy są potrzebne.

Podsumowanie tematu #

W tej części kursu poznałeś:

  • Więzy integralności (constraints) - mechanizmy obronne bazy; naruszające zapytania są automatycznie odrzucane, chroniąc spójność danych
  • Primary Key - unikalny identyfikator wiersza; UNIQUE + NOT NULL + jeden na tabelę; wybór INT IDENTITY (mały, sekwencyjny) kontra GUID (globalnie unikalny, dla rozproszonych systemów)
  • Foreign Key - relacja między dwoma wierszami; integralność referencyjna blokuje orphan records; przykład Departamenty↔Pracownicy
  • Trzy strategie ON DELETE - NO ACTION (domyślne, blokada), CASCADE (kaskadowe usuwanie - niebezpieczne dla historii biznesowej), SET NULL (luźne powiązania)
  • UNIQUE - unikalność kolumny bez bycia PK; klasyczne użycia: email, telefon, kod kreskowy, PESEL; tabela różnic PK/UNIQUE
  • CHECK - warunek logiczny na kolumnie lub wielu; walidacja zakresów, wartości domenowych, spójności między kolumnami
  • Wielowarstwowa walidacja - UI + aplikacja + CHECK w bazie; każda warstwa łapie inne błędy

W następnych wpisach zagłębimy się w transakcje (BEGIN/COMMIT/ROLLBACK - jak baza gwarantuje atomowość operacji), indeksy (jak przyspieszać zapytania i jaki koszt tego płacimy), oraz poziomy izolacji transakcji.

Quiz i zadanie poniżej. Zadanie symuluje więzy integralności w C# - DatabaseSimulator z metodą AddOrder egzekwującą FK check (User istnieje?) i CHECK constraint (Price > 0?), rzucającą InvalidOperationException przy naruszeniu. To dokładny wzorzec, jak myśli prawdziwa baza przy każdym INSERT.

Podobne wpisy

🔗 Linkują tu