Odpowiedzi:
Potrzebne są dwa wyzwalacze, aby wyłapać nieprawidłowy wiek
Poniższa metoda oparta jest na bardzo rygorystycznej metodzie pułapkowania błędów dla wyzwalaczy MySQL z rozdziału 11, strony 254-256 książki MySQL Programowanie procedur składowanych w podtytule „Sprawdzanie poprawności danych za pomocą wyzwalaczy” :
drop table mytable;
create table mytable (
id smallint unsigned AUTO_INCREMENT,
age tinyint not null,
primary key(id)
);
DELIMITER $$
CREATE TRIGGER checkage_bi BEFORE INSERT ON mytable FOR EACH ROW
BEGIN
DECLARE dummy,baddata INT;
SET baddata = 0;
IF NEW.age > 20 THEN
SET baddata = 1;
END IF;
IF NEW.age < 1 THEN
SET baddata = 1;
END IF;
IF baddata = 1 THEN
SELECT CONCAT('Cannot Insert This Because Age ',NEW.age,' is Invalid')
INTO dummy FROM information_schema.tables;
END IF;
END; $$
CREATE TRIGGER checkage_bu BEFORE UPDATE ON mytable FOR EACH ROW
BEGIN
DECLARE dummy,baddata INT;
SET baddata = 0;
IF NEW.age > 20 THEN
SET baddata = 1;
END IF;
IF NEW.age < 1 THEN
SET baddata = 1;
END IF;
IF baddata = 1 THEN
SELECT CONCAT('Cannot Update This Because Age ',NEW.age,' is Invalid')
INTO dummy FROM information_schema.tables;
END IF;
END; $$
DELIMITER ;
insert into mytable (age) values (10);
insert into mytable (age) values (15);
insert into mytable (age) values (20);
insert into mytable (age) values (25);
insert into mytable (age) values (35);
select * from mytable;
insert into mytable (age) values (5);
select * from mytable;
Oto wynik:
mysql> drop table mytable;
Query OK, 0 rows affected (0.03 sec)
mysql> create table mytable (
-> id smallint unsigned AUTO_INCREMENT,
-> age tinyint not null,
-> primary key(id)
-> );
Query OK, 0 rows affected (0.06 sec)
mysql> DELIMITER $$
mysql> CREATE TRIGGER checkage_bi BEFORE INSERT ON mytable FOR EACH ROW
-> BEGIN
-> DECLARE dummy,baddata INT;
-> SET baddata = 0;
-> IF NEW.age > 20 THEN
-> SET baddata = 1;
-> END IF;
-> IF NEW.age < 1 THEN
-> SET baddata = 1;
-> END IF;
-> IF baddata = 1 THEN
-> SELECT CONCAT('Cannot Insert This Because Age ',NEW.age,' is Invalid')
-> INTO dummy FROM information_schema.tables;
-> END IF;
-> END; $$
Query OK, 0 rows affected (0.08 sec)
mysql> CREATE TRIGGER checkage_bu BEFORE UPDATE ON mytable FOR EACH ROW
-> BEGIN
-> DECLARE dummy,baddata INT;
-> SET baddata = 0;
-> IF NEW.age > 20 THEN
-> SET baddata = 1;
-> END IF;
-> IF NEW.age < 1 THEN
-> SET baddata = 1;
-> END IF;
-> IF baddata = 1 THEN
-> SELECT CONCAT('Cannot Update This Because Age ',NEW.age,' is Invalid')
-> INTO dummy FROM information_schema.tables;
-> END IF;
-> END; $$
Query OK, 0 rows affected (0.07 sec)
mysql> DELIMITER ;
mysql> insert into mytable (age) values (10);
Query OK, 1 row affected (0.06 sec)
mysql> insert into mytable (age) values (15);
Query OK, 1 row affected (0.05 sec)
mysql> insert into mytable (age) values (20);
Query OK, 1 row affected (0.04 sec)
mysql> insert into mytable (age) values (25);
ERROR 1172 (42000): Result consisted of more than one row
mysql> insert into mytable (age) values (35);
ERROR 1172 (42000): Result consisted of more than one row
mysql> select * from mytable;
+----+-----+
| id | age |
+----+-----+
| 1 | 10 |
| 2 | 15 |
| 3 | 20 |
+----+-----+
3 rows in set (0.00 sec)
mysql> insert into mytable (age) values (5);
Query OK, 1 row affected (0.07 sec)
mysql> select * from mytable;
+----+-----+
| id | age |
+----+-----+
| 1 | 10 |
| 2 | 15 |
| 3 | 20 |
| 4 | 5 |
+----+-----+
4 rows in set (0.00 sec)
mysql>
Należy również pamiętać, że wartości automatycznego przyrostu nie są marnowane ani tracone.
Spróbuj !!!
Ograniczenia CHECK nie są zaimplementowane w MySQL. Z menu UTWÓRZ
Klauzula CHECK jest analizowana, ale jest ignorowana przez wszystkie silniki pamięci masowej. Patrz Rozdział 12.1.17, „TWORZENIE Składni TABELI”. Powodem akceptowania, ale ignorowania klauzul składniowych jest kompatybilność, aby ułatwić przenoszenie kodu z innych serwerów SQL i uruchamianie aplikacji, które tworzą tabele z referencjami. Zobacz Sekcja 1.8.5, „Różnice MySQL od standardowego SQL”.
To także zgłoszony błąd przez prawie 8 lat ...
Oprócz fajnego rozwiązania wyzwalającego autorstwa @Rolando istnieje jeszcze jedno obejście tego problemu w MySQL (dopóki CHECK
ograniczenia nie zostaną zaimplementowane).
Jak emulować niektóre CHECK
ograniczenia w MySQL
Tak więc, jeśli wolisz ograniczenia spójności referencyjnej i chcesz uniknąć wyzwalaczy (z powodu problemów w MySQL, gdy masz oba w swoich tabelach), możesz użyć innej małej tabeli referencyjnej:
CREATE TABLE age_allowed
( age TINYINT UNSIGNED NOT NULL
, PRIMARY KEY (age)
) ENGINE = InnoDB ;
Wypełnij go 20 rzędami:
INSERT INTO age_allowed
(age)
VALUES
(0), (1), (2), (3), ..., (19) ;
Wtedy twój stół będzie:
CREATE TABLE test
( id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT
, age TINYINT UNSIGNED NOT NULL
, PRIMARY KEY (id)
, CONSTRAINT age_allowed__in__test
FOREIGN KEY (age)
REFERENCES age_allowed (age)
) ENGINE = InnoDB ;
Musisz usunąć dostęp do zapisu w age_allowed
tabeli, aby uniknąć przypadkowego dodania lub usunięcia wierszy.
Ta sztuczka nie będzie działać z FLOAT
kolumnami typu danych, niestety (zbyt wiele wartości pomiędzy 0.0
i 20.0
).
Jak emulować arbitralne CHECK
ograniczenia w MySQL (5.7) i MariaDB (od 5.2 do 10.1)
Od MariaDB dodania kolumny obliczane w wersji 5.2 (GA wydanie: 2010-11-10 ) oraz MySQL 5.7 (GA wydanie: 2015-10-21 ) - który nazywają je VIRTUAL
i GENERATED
odpowiednio - które mogą być utrwalone, tj przechowywane w tabela - nazywają je PERSISTENT
i STORED
odpowiednio - możemy z nich korzystać, aby uprościć powyższe rozwiązanie, a nawet lepiej, przedłużyć go naśladować / egzekwować arbitralnych CHECK
ograniczeń ):
Jak wyżej, potrzebujemy tabeli pomocy, ale tym razem z jednym rzędem, który będzie działał jako tabela „kotwicy”. Co więcej, ta tabela może być używana do dowolnej liczby CHECK
ograniczeń.
Następnie dodajemy kolumnę obliczeniową, która ocenia do TRUE
/ FALSE
/ UNKNOWN
, dokładnie tak, jak CHECK
wiązałoby to ograniczenie - ale ta kolumna ma FOREIGN KEY
ograniczenie do naszej tabeli zakotwiczenia. Jeśli warunek / kolumna ma wartość FALSE
dla niektórych wierszy, wiersze są odrzucane z powodu FK.
Jeśli warunek / kolumna ma wartość TRUE
lub UNKNOWN
( NULL
), wiersze nie są odrzucane, dokładnie tak jak powinno być w przypadku CHECK
ograniczeń:
CREATE TABLE truth
( t BIT NOT NULL,
PRIMARY KEY (t)
) ENGINE = InnoDB ;
-- Put a single row:
INSERT INTO truth (t)
VALUES (TRUE) ;
-- Then your table would be:
-- (notice the change to `FLOAT`, to prove that we don't need)
-- (to restrict the solution to a small type)
CREATE TABLE test
( id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
age FLOAT NOT NULL,
age_is_allowed BIT -- GENERATED ALWAYS
AS (age >= 0 AND age < 20) -- our CHECK constraint
STORED,
PRIMARY KEY (id),
CONSTRAINT check_age_must_be_non_negative_and_less_than_20
FOREIGN KEY (age_is_allowed)
REFERENCES truth (t)
) ENGINE = InnoDB ;
Przykład dotyczy wersji MySQL 5.7. W MariaDB (wersje 5.2+ do 10.1) wystarczy zmodyfikować składnię i zadeklarować kolumnę jako PERSISTENT
zamiast STORED
. W wersji 10.2 STORED
dodano również słowo kluczowe, więc powyższy przykład działa w obu wersjach (MySQL i MariaDB) dla najnowszych wersji.
Jeśli chcemy wymusić wiele CHECK
ograniczeń (co jest powszechne w wielu projektach), musimy po prostu dodać obliczoną kolumnę i klucz obcy dla każdego z nich. Potrzebujemy tylko jednej truth
tabeli w bazie danych. Powinien mieć wstawiony jeden wiersz, a następnie usunięty cały dostęp do zapisu.
W najnowszej wersji MariaDB nie musimy jednak wykonywać tych wszystkich akrobacji, ponieważ CHECK
ograniczenia zostały zaimplementowane w wersji 10.2.1 (wydanie alfa: 2016-lipiec-04)!
Obecna wersja 10.2.2 jest wciąż wersją beta, ale wydaje się, że funkcja będzie dostępna w pierwszym stabilnym wydaniu serii MariaDB 10.2.
Jak wyjaśniłem w tym artykule , począwszy od wersji 8.0.16, MySQL dodał obsługę niestandardowych ograniczeń CHECK:
ALTER TABLE topic
ADD CONSTRAINT post_content_check
CHECK (
CASE
WHEN DTYPE = 'Post'
THEN
CASE
WHEN content IS NOT NULL
THEN 1
ELSE 0
END
ELSE 1
END = 1
);
ALTER TABLE topic
ADD CONSTRAINT announcement_validUntil_check
CHECK (
CASE
WHEN DTYPE = 'Announcement'
THEN
CASE
WHEN validUntil IS NOT NULL
THEN 1
ELSE 0
END
ELSE 1
END = 1
);
Wcześniej było to dostępne tylko przy użyciu wyzwalaczy PRZED WSTAWIENIEM i PRZED AKTUALIZACJĄ:
CREATE
TRIGGER post_content_check BEFORE INSERT
ON topic
FOR EACH ROW
BEGIN
IF NEW.DTYPE = 'Post'
THEN
IF NEW.content IS NULL
THEN
signal sqlstate '45000'
set message_text = 'Post content cannot be NULL';
END IF;
END IF;
END;
CREATE
TRIGGER post_content_update_check BEFORE UPDATE
ON topic
FOR EACH ROW
BEGIN
IF NEW.DTYPE = 'Post'
THEN
IF NEW.content IS NULL
THEN
signal sqlstate '45000'
set message_text = 'Post content cannot be NULL';
END IF;
END IF;
END;
CREATE
TRIGGER announcement_validUntil_check BEFORE INSERT
ON topic
FOR EACH ROW
BEGIN
IF NEW.DTYPE = 'Announcement'
THEN
IF NEW.validUntil IS NULL
THEN
signal sqlstate '45000'
set message_text = 'Announcement validUntil cannot be NULL';
END IF;
END IF;
END;
CREATE
TRIGGER announcement_validUntil_update_check BEFORE UPDATE
ON topic
FOR EACH ROW
BEGIN
IF NEW.DTYPE = 'Announcement'
THEN
IF NEW.validUntil IS NULL
THEN
signal sqlstate '45000'
set message_text = 'Announcement validUntil cannot be NULL';
END IF;
END IF;
END;
Aby uzyskać więcej informacji na temat emulacji ograniczeń CHECK za pomocą wyzwalaczy bazy danych dla wersji MySQL wcześniejszych niż 8.0.16, zapoznaj się z tym artykułem .