次の表があります。
create table test (
id smallint unsigned AUTO_INCREMENT,
age tinyint not null,
primary key(id),
check (age<20)
);
問題は、CHECK
制約が年齢列で機能しないことです。たとえば、年齢フィールドに222を挿入すると、MySQLはそれを受け入れます。
必要なのは、無効な年齢条件をキャッチする2つのトリガーです
以下は、本の第11章、ページ254〜256のMySQLトリガーのjerry-riggedエラートラップメソッドに基づいています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>
自動インクリメント値が無駄になったり失われたりしないことにも注意してください。
試してみる !!!
MySQLではCHECK制約は実装されていません。から CREATE TABLE
CHECK句は解析されますが、すべてのストレージエンジンによって無視されます。セクション12.1.17「CREATE TABLE構文」を参照してください。構文句を受け入れるが無視する理由は、互換性のため、他のSQLサーバーからのコードの移植を容易にし、参照付きのテーブルを作成するアプリケーションを実行するためです。セクション1.8.5「MySQLと標準SQLの違い」を参照してください。
報告されたバグ でもほぼ8年間です...
@RolandoによるNiceトリガーソリューションのほかに、MySQLでこの問題の別の回避策があります(CHECK
制約が実装されるまで)。
MySQLでCHECK
制約をエミュレートする方法
したがって、参照整合性制約を優先し、トリガーを回避したい場合(テーブルに両方が存在する場合の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
の間の値が多すぎます)。
MySQL(5.7)およびMariaDB(5.2から10.1まで)で任意のCHECK
制約をエミュレートする方法
MariaDBは5.2バージョンに計算カラムを追加した (GAリリース:2010-11-10)および 5.7のMySQL ( 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
テーブルが1つだけ必要です。 1つの行が挿入され、すべての書き込みアクセスが削除されます。
ただし、最新のMariaDBでは、CHECK
制約が実装されている バージョン10.2.1でこれらのアクロバットをすべて実行する必要はありません(アルファリリース:2016- 2004年7月)!
現在の10.2.2バージョンはまだベータ版ですが、この機能はMariaDB 10.2シリーズの最初の安定版リリースで利用できるようです。
この記事 で説明したように、バージョン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;
MySQLバージョン8.0.16より前のデータベーストリガーを使用したCHECK制約のエミュレーションの詳細については、 この記事 を確認してください。