sprawdź ograniczenie nie działa?

23

Mam następującą tabelę.

create table test (
   id smallint unsigned AUTO_INCREMENT,
   age tinyint not null,
   primary key(id),
   check (age<20)
);

Problem polega na tym, że CHECKograniczenie nie działa w kolumnie wieku. Na przykład, gdy wstawię 222 w polu wieku, MySQL to akceptuje.

ALH
źródło

Odpowiedzi:

16

Potrzebne są dwa wyzwalacze, aby wyłapać nieprawidłowy wiek

  • PRZED WPROWADZENIEM
  • PRZED AKTUALIZACJĄ

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 !!!

RolandoMySQLDBA
źródło
19

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 ...

gbn
źródło
13

Oprócz fajnego rozwiązania wyzwalającego autorstwa @Rolando istnieje jeszcze jedno obejście tego problemu w MySQL (dopóki CHECKograniczenia nie zostaną zaimplementowane).

Jak emulować niektóre CHECKograniczenia 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_allowedtabeli, aby uniknąć przypadkowego dodania lub usunięcia wierszy.

Ta sztuczka nie będzie działać z FLOATkolumnami typu danych, niestety (zbyt wiele wartości pomiędzy 0.0i 20.0).


Jak emulować arbitralne CHECKograniczenia 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 VIRTUALi GENERATEDodpowiednio - które mogą być utrwalone, tj przechowywane w tabela - nazywają je PERSISTENTi STOREDodpowiednio - możemy z nich korzystać, aby uprościć powyższe rozwiązanie, a nawet lepiej, przedłużyć go naśladować / egzekwować arbitralnych CHECKograniczeń ):

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 CHECKograniczeń.

Następnie dodajemy kolumnę obliczeniową, która ocenia do TRUE/ FALSE/ UNKNOWN, dokładnie tak, jak CHECKwiązałoby to ograniczenie - ale ta kolumna ma FOREIGN KEYograniczenie do naszej tabeli zakotwiczenia. Jeśli warunek / kolumna ma wartość FALSEdla niektórych wierszy, wiersze są odrzucane z powodu FK.

Jeśli warunek / kolumna ma wartość TRUElub UNKNOWN( NULL), wiersze nie są odrzucane, dokładnie tak jak powinno być w przypadku CHECKograniczeń:

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 PERSISTENTzamiast STORED. W wersji 10.2 STOREDdodano 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 CHECKograniczeń (co jest powszechne w wielu projektach), musimy po prostu dodać obliczoną kolumnę i klucz obcy dla każdego z nich. Potrzebujemy tylko jednej truthtabeli 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ż CHECKograniczenia 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.

ypercubeᵀᴹ
źródło
0

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 .

Vlad Mihalcea
źródło