verificar restrição não funciona?

23

Eu tenho a tabela a seguir.

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

O problema é que a CHECKrestrição não funciona na coluna de idade. Por exemplo, quando insiro 222 no campo de idade, o MySQL aceita.

ALH
fonte

Respostas:

16

Você precisa de dois gatilhos para detectar a condição de idade inválida

  • ANTES DE INSERIR
  • ANTES DE ATUALIZAR

O seguinte é baseado em um método de interceptação de erro fraudulento para os MySQL Triggers do Capítulo 11, páginas 254-256 do livro MySQL Stored Procedure Programming sob o subtítulo 'Validating Data with Triggers' :

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;

Aqui está o resultado:

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>

Observe também que os valores de incremento automático não são desperdiçados ou perdidos.

De uma chance !!!

RolandoMySQLDBA
fonte
19

As restrições CHECK não são implementadas no MySQL. De CREATE TABLE

A cláusula CHECK é analisada, mas ignorada por todos os mecanismos de armazenamento. Consulte a Seção 12.1.17, “Sintaxe CREATE TABLE”. O motivo para aceitar, mas ignorar as cláusulas de sintaxe, é a compatibilidade, facilitar a porta de código de outros servidores SQL e executar aplicativos que criam tabelas com referências. See Secção 1.8.5, “Diferenças do MySQL do SQL padrão”.

Também é um bug relatado há quase 8 anos ...

gbn
fonte
13

Além da boa solução de gatilho do @Rolando, há outra solução alternativa para esse problema no MySQL (até CHECK restrições sejam implementadas).

Como emular algumas CHECKrestrições no MySQL

Portanto, se você prefere restrições de integridade referencial e deseja evitar disparadores (por causa dos problemas no MySQL quando você possui os dois em suas tabelas), você pode usar outra pequena tabela de referência:

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

Preencha com 20 linhas:

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

Então sua mesa seria:

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 ;

Você precisará remover o acesso de gravação à age_allowedtabela para evitar a adição ou remoção acidental de linhas.

FLOATInfelizmente, este truque não funciona com colunas de tipo de dados (muitos valores entre 0.0e 20.0).


Como emular CHECKrestrições arbitrárias no MySQL (5.7) e MariaDB (de 5.2 até 10.1)

Desde que o MariaDB adicionou colunas computadas em sua versão 5.2 (GA release: 2010-11-10 ) e MySQL em 5.7 (GA release: 2015-10-21 ) - que eles os chamam VIRTUALe GENERATEDrespectivamente - que podem ser persistidos, ou seja, armazenados no tabela - eles os chamam PERSISTENTe STOREDrespectivamente - podemos usá-los para simplificar a solução acima e, melhor ainda, estendê-la para emular / impor CHECKrestrições arbitrárias ):

Como acima, precisaremos de uma tabela de ajuda, mas desta vez com uma única linha que atuará como uma tabela "âncora". Melhor ainda, esta tabela pode ser usada para qualquer número de CHECKrestrições.

Em seguida, adicionamos uma coluna computada que é avaliada como TRUE/ FALSE/ UNKNOWN, exatamente como uma CHECKrestrição faria - mas essa coluna tem uma FOREIGN KEYrestrição em nossa tabela âncora. Se a condição / coluna for avaliada FALSEpara algumas linhas, as linhas serão rejeitadas devido ao FK.

Se a condição / coluna for avaliada como TRUEou UNKNOWN( NULL), as linhas não serão rejeitadas, exatamente como deveria acontecer com as CHECKrestrições:

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 ;

O exemplo é para a versão MySQL 5.7. No MariaDB (versões 5.2+ até 10.1), só precisamos modificar a sintaxe e declarar a coluna como em PERSISTENTvez de STORED. Na versão 10.2, a STOREDpalavra - chave também foi adicionada; portanto, o exemplo acima funciona nos dois tipos (MySQL e MariaDB) para as versões mais recentes.

Se queremos impor muitas CHECKrestrições (o que é comum em muitos projetos), basta adicionar uma coluna computada e uma chave estrangeira para cada uma delas. Nós precisamos apenas de uma truthtabela no banco de dados. Ele deve ter uma linha inserida e todo o acesso de gravação removido.


No MariaDB mais recente, no entanto, não precisamos mais executar todas essas acrobacias, pois as CHECKrestrições foram implementadas na versão 10.2.1 (versão alpha: 2016-Jul-04)!

A versão atual 10.2.2 ainda é uma versão beta, mas parece que o recurso estará disponível na primeira versão estável da série MariaDB 10.2.

ypercubeᵀᴹ
fonte
0

Como expliquei neste artigo , a partir da versão 8.0.16, o MySQL adicionou suporte para restrições CHECK personalizadas:

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
);

Anteriormente, isso estava disponível apenas usando os gatilhos BEFORE INSERT e 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;

Para obter mais detalhes sobre a emulação de restrições CHECK usando gatilhos de banco de dados para versões do MySQL anteriores à 8.0.16, consulte este artigo .

Vlad Mihalcea
fonte