Ich habe die folgende Tabelle.
create table test (
id smallint unsigned AUTO_INCREMENT,
age tinyint not null,
primary key(id),
check (age<20)
);
Das Problem ist, dass die Einschränkung CHECK
in der Altersspalte nicht funktioniert. Wenn ich beispielsweise 222 für das Altersfeld einfüge, akzeptiert MySQL dies.
Was Sie brauchen, sind zwei Auslöser, um die ungültige Altersbedingung zu erkennen
Das Folgende basiert auf einer von Jerry manipulierten Fehlerüberwachungsmethode für MySQL-Trigger aus Kapitel 11, Seiten 254-256 des Buches MySQL Stored Procedure Programming unter der Überschrift 'Daten mit Triggern validieren' :
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;
Hier ist das Ergebnis:
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>
Bitte beachten Sie auch, dass automatische Inkrementierungswerte nicht verschwendet werden oder verloren gehen.
Versuche es !!!
CHECK-Einschränkungen sind in MySQL nicht implementiert. Von TABELLE ERSTELLEN
Die CHECK-Klausel wird analysiert, aber von allen Speicher-Engines ignoriert. Siehe Abschnitt 12.1.17, „CREATE TABLE-Syntax“. Der Grund für das Akzeptieren, aber Ignorieren von Syntaxklauseln liegt in der Kompatibilität, um das Portieren von Code von anderen SQL-Servern zu vereinfachen und Anwendungen auszuführen, die Tabellen mit Referenzen erstellen. Siehe Abschnitt 1.8.5, „MySQL-Unterschiede zu Standard-SQL“.
Es ist auch ein gemeldeter Fehler seit fast 8 Jahren ...
Neben der Nice-Trigger-Lösung von @Rolando gibt es eine weitere Problemumgehung für dieses Problem in MySQL (bis CHECK
-Einschränkungen implementiert sind).
So emulieren Sie einige CHECK
Einschränkungen in MySQL
Wenn Sie also Einschränkungen der referenziellen Integrität bevorzugen und Trigger vermeiden möchten (aufgrund der Probleme in MySQL, wenn Sie beide in Ihren Tabellen haben), können Sie eine andere kleine Referenztabelle verwenden:
CREATE TABLE age_allowed
( age TINYINT UNSIGNED NOT NULL
, PRIMARY KEY (age)
) ENGINE = InnoDB ;
Füllen Sie es mit 20 Zeilen:
INSERT INTO age_allowed
(age)
VALUES
(0), (1), (2), (3), ..., (19) ;
Dann wäre Ihr Tisch:
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 ;
Sie müssen den Schreibzugriff auf die Tabelle age_allowed
entfernen, um ein versehentliches Hinzufügen oder Entfernen von Zeilen zu vermeiden.
Dieser Trick funktioniert leider nicht mit FLOAT
Datentypspalten (zu viele Werte zwischen 0.0
und 20.0
).
So emulieren Sie beliebige CHECK
Einschränkungen in MySQL (5.7) und MariaDB (von 5.2 bis 10.1)
Seit MariaDB hat in ihrer 5.2-Version berechnete Spalten hinzugefügt (GA-Version: 2010-11-10 ) und MySQL in 5.7 (GA release: 2015-10-21 ) - die sie VIRTUAL
bzw. GENERATED
nennen - das kann beibehalten werden, dh in der Tabelle gespeichert werden - sie nennen sie PERSISTENT
bzw. STORED
- wir können sie verwenden, um die obige Lösung zu vereinfachen und noch besser Erweitern Sie es, um beliebige CHECK
Einschränkungen zu emulieren/durchzusetzen ):
Wie oben benötigen wir eine Hilfetabelle, diesmal jedoch mit einer einzelnen Zeile, die als "Ankertabelle" fungiert. Noch besser ist, dass diese Tabelle für eine beliebige Anzahl von CHECK
Einschränkungen verwendet werden kann.
Wir fügen dann eine berechnete Spalte hinzu, die entweder TRUE
/FALSE
/UNKNOWN
ergibt, genau wie eine CHECK
-Einschränkung - aber diese Spalte hat eine FOREIGN KEY
Einschränkung unserer Ankertabelle. Wenn die Bedingung/Spalte für einige Zeilen FALSE
ergibt, werden die Zeilen aufgrund der FK abgelehnt.
Wenn die Bedingung/Spalte TRUE
oder UNKNOWN
(NULL
) ergibt, werden die Zeilen nicht zurückgewiesen, genau wie es mit CHECK
Einschränkungen geschehen sollte:
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 ;
Das Beispiel ist für die MySQL 5.7-Version. In MariaDB (Versionen 5.2+ bis 10.1) müssen wir nur die Syntax ändern und die Spalte als PERSISTENT
anstelle von STORED
deklarieren. In Version 10.2 wurde auch das Schlüsselwort STORED
hinzugefügt, sodass das obige Beispiel in beiden Varianten (MySQL und MariaDB) für die neuesten Versionen funktioniert.
Wenn wir viele CHECK
Einschränkungen erzwingen möchten (was in vielen Designs üblich ist), müssen wir nur eine berechnete Spalte und einen Fremdschlüssel für jede von ihnen hinzufügen. Wir brauchen nur eine truth
Tabelle in der Datenbank. Es sollte eine Zeile eingefügt und dann der gesamte Schreibzugriff entfernt werden.
In der neuesten MariaDB müssen wir jedoch nicht mehr alle diese Akrobaten ausführen, da CHECK
Einschränkungen wurden implementiert in Version 10.2.1 (Alpha-Version: 04.07.2016)!
Die aktuelle 10.2.2-Version ist noch eine Beta-Version, aber es scheint, dass die Funktion in der ersten stabilen Version der MariaDB 10.2-Serie verfügbar sein wird.
Wie ich in diesem Artikel erklärt habe, hat MySQL ab Version 8.0.16 Unterstützung für benutzerdefinierte CHECK-Einschränkungen hinzugefügt:
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
);
Bisher war dies nur mit den Triggern BEFORE INSERT und BEFORE UPDATE verfügbar:
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;
Weitere Informationen zum Emulieren von CHECK-Einschränkungen mithilfe von Datenbank-Triggern für MySQL-Versionen vor 8.0.16 finden Sie unter dieser Artikel .