web-dev-qa-db-fra.com

vérifier la contrainte ne fonctionne pas?

J'ai le tableau suivant.

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

Le problème est que la contrainte CHECK ne fonctionne pas sur la colonne age. Par exemple, lorsque j'insère 222 pour le champ d'âge, MySQL l'accepte.

23
ALH

Vous avez besoin de deux déclencheurs pour détecter la condition d'âge non valide

  • AVANT D'INSÉRER
  • AVANT LA MISE À JOUR

Ce qui suit est basé sur une méthode de capture d'erreur truquée pour les déclencheurs MySQL du chapitre 11, pages 254-256 du livre MySQL Stored Procedure Programming sous la sous-rubrique 'Validation des données avec des déclencheurs' :

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;

Voici le résultat:

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>

Veuillez également noter que les valeurs d'incrémentation automatique ne sont ni gaspillées ni perdues.

Essaie !!!

16
RolandoMySQLDBA

Les contraintes CHECK ne sont pas implémentées dans MySQL. De CREATE TABLE

La clause CHECK est analysée mais ignorée par tous les moteurs de stockage. Voir Section 12.1.17, "Syntaxe CREATE TABLE". La raison d'accepter mais d'ignorer les clauses de syntaxe est pour la compatibilité, pour faciliter le portage de code à partir d'autres serveurs SQL et pour exécuter des applications qui créent des tables avec des références. Voir Section 1.8.5, "Différences entre MySQL et SQL standard".

C'est aussi un bug signalé depuis près de 8 ans ...

19
gbn

Outre la solution de déclenchement Nice de @Rolando, il existe une autre solution à ce problème dans MySQL (jusqu'à ce que les contraintes CHECK soient implémentées).

Comment émuler certaines contraintes CHECK dans MySQL

Donc, si vous préférez les contraintes d'intégrité référentielle et que vous souhaitez éviter les déclencheurs (en raison des problèmes dans MySQL lorsque vous avez les deux dans vos tables), vous pouvez utiliser une autre petite table de référence:

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

Remplissez-le avec 20 lignes:

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

Votre table serait alors:

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 ;

Vous devrez supprimer l'accès en écriture au age_allowed table, pour éviter l'ajout ou la suppression accidentelle de lignes.

Cette astuce ne fonctionnera pas avec les colonnes de type de données FLOAT, malheureusement (trop de valeurs entre 0.0 et 20.0).


Comment émuler des contraintes arbitraires CHECK dans MySQL (5.7) et MariaDB (de 5.2 à 10.1)

Depuis MariaDB a ajouté des colonnes calculées dans leur version 5.2 (version GA: 10/11/2010 ) et MySQL en 5.7 (version GA: 21/10/2015 ) - qu'ils appellent VIRTUAL et GENERATED respectivement - qui peuvent être persistants, c'est-à-dire stockés dans la table - ils les appellent respectivement PERSISTENT et STORED - nous pouvons les utiliser pour simplifier la solution ci-dessus et encore mieux, l'étendre pour émuler/appliquer des contraintes arbitraires CHECK):

Comme ci-dessus, nous aurons besoin d'une table d'aide mais avec une seule ligne cette fois qui agira comme une table "d'ancrage". Encore mieux, cette table peut être utilisée pour n'importe quel nombre de contraintes CHECK.

Nous ajoutons ensuite une colonne calculée qui évalue à TRUE/FALSE/UNKNOWN, exactement comme le ferait une contrainte CHECK - mais cette colonne a un FOREIGN KEY contrainte à notre table d'ancrage. Si la condition/colonne est évaluée à FALSE pour certaines lignes, les lignes sont rejetées, en raison du FK.

Si la condition/colonne est évaluée à TRUE ou UNKNOWN (NULL), les lignes ne sont pas rejetées, exactement comme cela devrait se produire avec les contraintes 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 ;

L'exemple concerne la version MySQL 5.7. Dans MariaDB (versions 5.2+ à 10.1), il suffit de modifier la syntaxe et de déclarer la colonne comme PERSISTENT au lieu de STORED. Dans la version 10.2, le mot clé STORED a également été ajouté, donc l'exemple ci-dessus fonctionne dans les deux versions (MySQL et MariaDB) pour les dernières versions.

Si nous voulons appliquer de nombreuses contraintes CHECK (ce qui est courant dans de nombreuses conceptions), il nous suffit d'ajouter une colonne calculée et une clé étrangère pour chacune d'entre elles. Nous n'avons besoin que d'une seule table truth dans la base de données. Il doit avoir une ligne insérée, puis tous les accès en écriture supprimés.


Dans la dernière MariaDB cependant, nous n'avons plus à effectuer toutes ces acrobaties, car CHECK contraintes ont été implémentées dans la version 10.2. 1 (version alpha: 2016-juil-04)!

La version 10.2.2 actuelle est toujours une version bêta mais il semble que la fonctionnalité sera disponible dans la première version stable de la série MariaDB 10.2.

13
ypercubeᵀᴹ

Comme je l'ai expliqué dans cet article , à partir de la version 8.0.16, MySQL a ajouté la prise en charge des contraintes CHECK personnalisées:

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

Auparavant, cela n'était disponible qu'avec les déclencheurs BEFORE INSERT et 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;

Pour plus de détails sur l'émulation des contraintes CHECK à l'aide de déclencheurs de base de données pour les versions de MySQL antérieures à 8.0.16, consultez alors cet article .

0
Vlad Mihalcea