проверка ограничений не работает?

23

У меня есть следующая таблица.

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

Проблема в том, что CHECKограничение не работает в столбце возраста. Например, когда я вставляю 222 для поля возраста, MySQL принимает его.

ALH
источник

Ответы:

16

Вам нужны два триггера, чтобы поймать недопустимое возрастное состояние

  • ДО ВСТАВКИ
  • ДО ОБНОВЛЕНИЯ

Нижеследующее основано на методе перехвата ошибочных сообщений для триггеров MySQL из главы 11, страницы 254-256 книги « Программирование хранимых процедур MySQL» в подзаголовке «Проверка данных с помощью триггеров» :

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;

Вот результат:

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>

Также обратите внимание, что значения автоинкремента не теряются и не теряются.

Попробуйте!

RolandoMySQLDBA
источник
19

Ограничения CHECK не реализованы в MySQL. От CREATE TABLE

Предложение CHECK анализируется, но игнорируется всеми механизмами хранения. См. Раздел 12.1.17, «Синтаксис CREATE TABLE». Причиной принятия, но игнорирования синтаксических предложений является совместимость, упрощение переноса кода с других серверов SQL и запуск приложений, создающих таблицы со ссылками. См. Раздел 1.8.5, «Отличия MySQL от стандартного SQL».

Это также было зарегистрированной ошибкой в течение почти 8 лет ...

ГБН
источник
13

Помимо приятного триггерного решения @Rolando, в MySQL есть еще один обходной путь этой проблемы (пока не CHECKбудут реализованы ограничения).

Как подражать некоторым CHECK ограничения в MySQL

Итак, если вы предпочитаете ограничения ссылочной целостности и хотите избежать триггеров (из-за проблем в MySQL, когда у вас есть обе таблицы), вы можете использовать другую небольшую справочную таблицу:

CREATE TABLE age_allowed
  ( age TINYINT UNSIGNED NOT NULL
  , PRIMARY KEY (age)
  ) ENGINE = InnoDB ;

Заполните его 20 строками:

INSERT INTO age_allowed
  (age)
VALUES
  (0), (1), (2), (3), ..., (19) ;

Тогда ваш стол будет:

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 ;

Вам придется удалить доступ на запись к age_allowed таблице, чтобы избежать случайного добавления или удаления строк.

Этот трюк FLOAT, к сожалению, не будет работать со столбцами типов данных (слишком много значений между 0.0и 20.0).


Как подражать произвольному CHECK ограничения в MySQL (5.7) и MariaDB (от 5.2 до 10.1)

Поскольку MariaDB добавил вычисляемые столбцы в их версии 5.2 ( версия GA: 2010-11-10 ) и MySQL в версии 5.7 (версия GA: 2015-10-21 ) - как они их называют VIRTUALи GENERATEDсоответственно - их можно сохранить, то есть сохранить в таблица - они их называют PERSISTENTи STOREDсоответственно - мы можем использовать их, чтобы упростить вышеуказанное решение и, что еще лучше, расширить его, чтобы эмулировать / применять произвольные CHECKограничения ):

Как и выше, нам понадобится справочная таблица, но на этот раз с одной строкой, которая будет выступать в роли «якорной» таблицы. Более того, эту таблицу можно использовать для любого количестваCHECK ограничений.

Затем мы добавляем вычисляемый столбец, который оценивает либо TRUE/ FALSE/ UNKNOWN, точно так же, как CHECKограничение - но этот столбец имеет FOREIGN KEYограничение для нашей таблицы привязки. Если условие / столбец оценивается FALSEдля некоторых строк, строки отклоняются из-за FK.

Если условие / столбец оценивается как TRUEили UNKNOWN( NULL), строки не отклоняются, в точности так, как это должно происходить с CHECKограничениями:

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 ;

Пример для версии MySQL 5.7. В MariaDB (версии 5.2+ до 10.1) нам просто нужно изменить синтаксис и объявить столбец PERSISTENTвместо STORED. В версии 10.2 также STOREDбыло добавлено ключевое слово, поэтому приведенный выше пример работает в обоих вариантах (MySQL и MariaDB) для последних версий.

Если мы хотим применить много CHECKограничений (что часто встречается во многих проектах), нам просто нужно добавить вычисляемый столбец и внешний ключ для каждого из них. Нам нужна только одна truthтаблица в базе данных. В него должна быть вставлена ​​одна строка, а затем все права на запись удалены.


Однако в последнем MariaDB нам больше не нужно выполнять всю эту акробатику, так как CHECKограничения были реализованы. в версии 10.2.1 (альфа-версия: 2016-Jul-04)!

Текущая версия 10.2.2 по-прежнему является бета-версией, но, похоже, эта функция будет доступна в первом стабильном выпуске серии MariaDB 10.2.

ypercubeᵀᴹ
источник
0

Как я объяснил в этой статье , начиная с версии 8.0.16, MySQL добавил поддержку пользовательских ограничений 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
);

Ранее это было доступно только с использованием триггеров BEFORE INSERT и BEFORE UPDATE:

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;

Более подробную информацию об эмуляции ограничений CHECK с использованием триггеров базы данных для версий MySQL до 8.0.16 можно найти в этой статье .

Влад Михалча
источник