チェック制約が機能しませんか?


23

次の表があります。

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

問題はCHECK、年齢の列で制約が機能しないことです。たとえば、年齢フィールドに222を挿入すると、MySQLはそれを受け入れます。

回答:


16

必要なのは、無効な年齢条件をキャッチする2つのトリガーです

  • 挿入する前に
  • 更新前

以下は、MySQLストアドプロシージャプログラミングの小見出し「Validating Data with Triggers」の第11章、ページ254〜256のMySQL Triggersに対するjerry-riggedエラートラップメソッドに基づいています

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>

また、自動インクリメント値が無駄になったり失われたりしないことに注意してください。

試してみる !!!


19

CHECK制約はMySQLに実装されていません。CREATE TABLEから

CHECK句は解析されますが、すべてのストレージエンジンによって無視されます。セクション12.1.17「CREATE TABLE構文」を参照してください。構文句を受け入れるが無視する理由は、互換性のためであり、他のSQLサーバーからのコードの移植を容易にし、参照を持つテーブルを作成するアプリケーションを実行するためです。セクション1.8.5「MySQLの標準SQLとの違い」を参照してください。

また、ほぼ8年間バグ報告されています...


13

@Rolandoによる優れたトリガーソリューションに加えて、MySQLでこの問題を回避する方法があります(CHECK制約が実装されるまで)。

CHECKMySQLでいくつかの制約をエミュレートする方法

したがって、参照整合性制約を優先し、トリガーを回避する場合(テーブルに両方がある場合の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)。


CHECKMySQL(5.7)およびMariaDB(5.2から10.1まで)で任意の制約をエミュレートする方法

以来MariaDBは、それらの5.2バージョンで計算カラムを追加:(GAリリース2010-11-10と)5.7でのMySQL(GAリリース:2015年10月21日) -彼らはそれらを呼び出しているVIRTUALGENERATED、それぞれ-に格納されている、すなわち、永続化することができます表-それらPERSISTENTSTOREDそれぞれ呼び出し、それぞれを使用して上記のソリューションを簡素化し、さらに改善して、任意のCHECK制約をエミュレート/強制することができます):

上記のように、ヘルプテーブルが必要になりますが、今回は「アンカー」テーブルとして機能する単一の行があります。さらに良いことに、このテーブルは任意の数のCHECK制約に使用できます。

次に、制約とまったく同じようにTRUE/ FALSE/のいずれかに評価される計算列を追加しますが、この列にはアンカーテーブルへの制約があります。一部の行について条件/列が評価される場合、FKのために行は拒否されます。UNKNOWNCHECKFOREIGN KEYFALSE

条件/列の評価が(TRUEまたは)の場合、制約で発生するはずのとおり、行は拒否されません。UNKNOWNNULLCHECK

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データベースに必要なテーブルは1つだけです。1行挿入し、すべての書き込みアクセスを削除する必要があります。


ただし、最新のMariaDB では、バージョン10.2.1(アルファリリース:2016-Jul-04)で制約が実装さているため、これらのすべてのアクロバットを実行する必要はありませんCHECK

現在の10.2.2バージョンはまだベータ版ですが、この機能はMariaDB 10.2シリーズの最初の安定版リリースで利用できるようです。


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;

8.0.16より前のMySQLバージョンのデータベーストリガーを使用したCHECK制約のエミュレートの詳細については、この記事をご覧ください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.