Menu

Ograniczenia CHECK w SQLite: walidacja danych na poziomie tabeli

Jak używać ograniczeń CHECK w SQLite, aby wymuszać reguły dla wartości w kolumnach: sprawdzenia jednej kolumny, kilku kolumn, nazwane ograniczenia i pułapki.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Ograniczenie CHECK to reguła, którą musi spełniać każdy wiersz

Ograniczenie CHECK to wyrażenie logiczne przypisane do tabeli. SQLite oblicza je przy każdym INSERT i UPDATE, a jeśli wyrażenie okaże się fałszywe, operacja się nie udaje. To sposób na wpisanie reguły biznesowej ("cena nie może być ujemna", "status musi być jedną z tych trzech wartości") prosto w schemat.

Pierwsze dwa wiersze trafiają do tabeli. Trzeci zgłasza CHECK constraint failed i zostaje odrzucony: tabela nigdy go nie zobaczy. Ograniczenie wymusza regułę dla każdego zapisującego, niezależnie od tego, czy to twoja aplikacja, skrypt migracji, czy ktoś grzebiący w CLI.

Poziom kolumny a poziom tabeli

CHECK możesz zapisać w dwóch miejscach: po definicji kolumny (poziom kolumny) albo po wszystkich kolumnach (poziom tabeli). Działają tak samo, a różnica polega na tym, co czyta się naturalniej.

Pierwsza rezerwacja się wstawia. Druga się nie udaje, bo koniec jest przed początkiem. Reguły dla jednej kolumny lepiej czytają się na poziomie kolumny, a wszystko, co porównuje dwie lub więcej kolumn, na poziomie tabeli.

Ograniczanie wartości do listy

Częste zastosowanie to zmuszenie kolumny do przyjmowania jednej z ustalonego zbioru wartości. SQLite nie ma natywnego typu enum, więc idiomem jest CHECK ... IN (...):

Trzeci wiersz się nie udaje, bo 'pending' nie ma na liście dozwolonych wartości. Jeśli kiedyś trzeba będzie dodać nowy status, konieczna będzie przebudowa tabeli (więcej o tym niżej), więc zastanów się chwilę, zanim zablokujesz listę. Ale dla naprawdę stałych słowników, takich jak nazwy ról czy stany zamówień, to dokładnie to ograniczenie, którego potrzebujesz.

Nadawanie nazw ograniczeniom

Domyślnie ograniczenie jest anonimowe. Komunikat błędu mówi tylko "CHECK constraint failed" razem z wyrażeniem, co wystarcza, gdy tabela ma jeden CHECK, i wprowadza w błąd, gdy ma ich pięć. Dodaj nazwę przez CONSTRAINT:

Teraz komunikat o błędzie zawiera nazwę ograniczenia, więc od razu wiesz, która reguła została złamana. Nadanie nazwy kosztuje kilka dodatkowych znaków i zwraca się już przy pierwszej awarii na produkcji.

CHECK i NULL: pułapka

CHECK przechodzi, gdy wyrażenie jest prawdziwe lub NULL. Nie przechodzi tylko przy jawnym fałszu. Brzmi to dziwnie, dopóki nie przypomnisz sobie, że prawie każde porównanie z NULL daje NULL, a nie prawdę ani fałsz.

Wiersz z NULL wchodzi bez problemu: NULL >= 0 to NULL, a nie fałsz, więc CHECK nie zgłasza błędu. Jeśli naprawdę chcesz zabronić zarówno liczb ujemnych, jak i brakujących wartości, połącz NOT NULL z CHECK:

Teraz wstawienie nie udaje się na ograniczeniu NOT NULL, zanim CHECK w ogóle zostanie sprawdzony. Oba ograniczenia współpracują: NOT NULL pilnuje obecności wartości, a CHECK jej postaci.

Przydatne wbudowane funkcje w CHECK

Wyrażenie może korzystać z większości wbudowanych funkcji SQLite. Kilka, które pojawiają się często:

Trzy błędy: zła postać adresu e-mail, za krótka nazwa użytkownika i kod kraju małymi literami. LIKE obsługuje proste wzorce, a length(), upper(), lower() i arytmetyka też są dozwolone. Pilnuj tylko, żeby wyrażenie było deterministyczne: użycie w CHECK czegoś takiego jak random() czy current_timestamp tworzy reguły, które mogą dawać różne wyniki dla różnych wierszy, a rzadko o to chodzi.

CHECK a trigger

Zarówno CHECK, jak i triggery mogą odrzucać złe dane, więc początkujący często zastanawiają się, po co sięgnąć. Praktyczna zasada:

  • CHECK, gdy reguła zależy tylko od zapisywanego wiersza. "Ta kolumna w porównaniu z tamtą kolumną", "ta wartość w zakresie", "ten tekst pasuje do wzorca".
  • Trigger (konkretnie trigger BEFORE INSERT/UPDATE, który wywołuje RAISE), gdy reguła zależy od innych wierszy, innych tabel albo wymaga czegoś bardziej złożonego niż jedno wyrażenie logiczne.

CHECK jest szybszy, prostszy i widoczny w schemacie: każdy, kto czyta CREATE TABLE, widzi regułę. Po trigger sięgaj tylko wtedy, gdy CHECK nie potrafi wyrazić tego, czego potrzebujesz.

CHECK nie da się usunąć przez ALTER

To jedyna niedogodność. SQLite nie ma ALTER TABLE ... DROP CONSTRAINT. Aby usunąć lub zmienić CHECK, przebudowujesz tabelę:

BEGIN;

CREATE TABLE products_new (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);

INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;

COMMIT;

Opakuj całość w transakcję, aby awaria w połowie zostawiła bazę nietkniętą. Jeśli inne tabele mają klucze obce wskazujące na przebudowywaną tabelę, procedura się wydłuża: wyłącz foreign_keys, przebuduj, włącz ponownie i sprawdź ponownie. Omówimy to w dokumencie o migracjach w dalszej części kursu.

Dalej: ograniczenia UNIQUE

CHECK sprawdza postać wartości w wierszu. Kolejne ograniczenie, UNIQUE, sprawdza zależności między wierszami: gwarantuje, że żadne dwa wiersze nie mają tej samej wartości w kolumnie lub zestawie kolumn. O tym na następnej stronie.

Najczęściej zadawane pytania

Czym jest ograniczenie CHECK w SQLite?

Ograniczenie CHECK to wyrażenie logiczne przypisane do tabeli, które musi spełniać każdy wiersz. SQLite sprawdza je przy każdym INSERT lub UPDATE i odrzuca zmianę, jeśli wyrażenie jest fałszywe. To najprostszy sposób na wymuszenie reguły typu 'cena musi być dodatnia' bez pisania kodu w aplikacji.

Czy ograniczenie CHECK w SQLite może odwoływać się do kilku kolumn?

Tak, zapisz je jako ograniczenie na poziomie tabeli, a nie przy jednej kolumnie. Na przykład CHECK (start_date <= end_date) zadeklarowane po liście kolumn może odwoływać się do obu kolumn. Technicznie sprawdzenia na poziomie kolumny też mogą odwoływać się do innych kolumn, ale gdy w grę wchodzi więcej niż jedna kolumna, poziom tabeli czyta się czytelniej.

Dlaczego moje ograniczenie CHECK w SQLite nie działa dla NULL?

CHECK przechodzi, gdy wyrażenie jest prawdziwe lub NULL, a nie przechodzi tylko wtedy, gdy jest jawnie fałszywe. Dlatego CHECK (age >= 0) akceptuje wiek równy NULL, bo NULL >= 0 daje NULL, a nie fałsz. Jeśli chcesz zabronić także NULL, dodaj obok CHECK ograniczenie NOT NULL.

Czy mogę usunąć lub zmienić ograniczenie CHECK w SQLite?

Nie bezpośrednio. SQLite nie obsługuje ALTER TABLE ... DROP CONSTRAINT. Aby zmienić CHECK, możesz edytować sqlite_schema przez PRAGMA writable_schema (zaawansowane i ryzykowne) albo przebudować tabelę: utworzyć nową tabelę z właściwymi ograniczeniami, skopiować dane, usunąć starą tabelę i zmienić nazwę nowej. Nazwane ograniczenia ułatwiają czytanie skryptu przebudowy.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ