Освен грешките, които сървърът генерира в различни ситуации, тази възможност я има и програмистът. Той може да генерира предупреждения и грешки. В предна статия обяснихме как се прихващат грешки от т.нар. Handler-и. Потребителски-дефинираните грешки също се прихващат от тях. Грешки и предупреждения от съхранени процедури, тригери и събития се генерират посредством генерирането на сигнал. Сигналът е специален обект, който носи информация за типа на грешката (SQLSTATE) и някакво съобщение.
Синтаксис:
SIGNAL <SQLSTATE или condition_name>
< condition_information_item_name>;
След ключовата дума SIGNAL следва определен SQLSTATE(виж статията Грешки при съхранени…) или т.нар. condition, който можете и сами да декларирате. Condition представлява дефинирана от потребителя грешка, която си има определено име. За нея можем да създадем handler, с който да я прихванем след като я генерираме чрез сигнал. Аналогично в програмирането е: генерираме си наше изключение и пишем catch блок, с който да го прихванем. Писането на condition-и прави процедурата/тригерът доста по четим. Например: SQLSTATE-ът, който връща сървъра при опит за изтриване на несъществуваща таблица е 1051. Ако решим да си направим hanler за него, то той би изглеждал така:
use school_sport_clubs; drop procedure testCondition; delimiter | create procedure testCondition() begin DECLARE CONTINUE HANDLER FOR 1051 BEGIN SELECT 'No such table'; END; drop table tbmc; end; | delimiter ;
Извикваме процедурата и резултатът е:
Нека сега да създадем condition за този SQLSTATE, който да има по-значещо име:
use school_sport_clubs; drop procedure if exists testConditionWithCondition; delimiter | create procedure testConditionWithCondition() begin DECLARE no_such_table CONDITION for 1051; DECLARE CONTINUE HANDLER FOR no_such_table BEGIN SELECT 'No such table'; END; drop table tbmc; end; | delimiter ;
Така с no_such_table всъщност дадохме по-значещо име на 1051. Резултатът, разбира се, е същият.
Да се върнем на синтаксиса на SIGNAL. След като уточнихме какъв ще бъде типа на сигнала – дали чрез SQLSTATE или чрез condition_name. След това се подава информация чрез използване на SET и някоя от дефинираните в mysql condition_information_item. А те са:
- SUBCLASS_ORIGIN
- MESSAGE_TEXT
- MYSQL_ERRNO
- CONSTRAINT_CATALOG
- CONSTRAINT_SCHEMA
- CONSTRAINT_NAME
- CATALOG_NAME
- SCHEMA_NAME
- TABLE_NAME
- COLUMN_NAME
- CURSOR_NAME
Нека дадем пример, който илюстрира казаното по-горе:
drop procedure if exists signalTest; delimiter | CREATE PROCEDURE signalTest(IN testVar INT) BEGIN DECLARE testCondition CONDITION FOR SQLSTATE '45000'; IF testVar = 0 THEN SIGNAL SQLSTATE '01000'; # warning ELSEIF testVar = 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro from case 1 - signal with handled SQLSTATE.'; ELSEIF testVar = 2 THEN SIGNAL testCondition SET MESSAGE_TEXT = 'Error from case 2 - signal with handled condition.'; ELSE SIGNAL SQLSTATE '01000' SET MESSAGE_TEXT = 'Warning! It will not be terminated. The procedure will continue.', MYSQL_ERRNO = 1000; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'After the warning, the error reset the message and it apper on the screen!', MYSQL_ERRNO = 1001; END IF; END ; | DELIMITER ;
Стандартното съобщение за потребителска грешка (user-define error) е 45000.
Ако подадете 0: Възниква WARNING. Той не терминира процедурата.
Ако подадете 1: Генерира се сигнал с SQLSTATE: ‘45000’. Терминира процедурата и извежда съобщението.
Ако подадете 2: Генерира се сигнал със създадения по-горе condition и се извежда съобщението.
Всички останали случаи: Ще се генерира WARNING. От предупреждението не се терминира процедурата, но стигне ли се до грешката, процедурата се терминира. Съобщението за грешката и номерът и, зададени по-рано се заменят от тези, които грешката решава да сложи.
Може да пробвате вътре да си дефинирате handler за SQLSTATE ‚45000‘ или за testCondition, но не го правете и за двете, защото второто е просто по-потребителско ориентирано име на първото. Например:
drop procedure if exists signalTest; delimiter | CREATE PROCEDURE signalTest(IN testVar INT) BEGIN DECLARE testCondition CONDITION FOR SQLSTATE '45000'; DECLARE EXIT handler for testCondition SELECT 'Hi, from handler for testCondition'; IF testVar = 0 THEN SIGNAL SQLSTATE '01000'; # warning ELSEIF testVar = 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro from case 1 - signal with handled SQLSTATE.'; ELSEIF testVar = 2 THEN SIGNAL testCondition SET MESSAGE_TEXT = 'Error from case 2 - signal with handled condition.'; ELSE SIGNAL SQLSTATE '01000' SET MESSAGE_TEXT = 'Warning! It will not be terminated. The procedure will continue.', MYSQL_ERRNO = 1000; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'After the warning, the error reset the message and it apper on the screen!', MYSQL_ERRNO = 1001; END IF; END ; | DELIMITER ; CALL signalTest(1); CALL signalTest(2);
И за двете резултатът е:
Съобщението за грешка вече не присъства, защото грешката е прихваната от Handler-a. И процедурата е завършила с нормален EXIT, а не принудително както по-горе.
Нека да реализираме един пример. Искаме да валидираме входа на таблица coaches- егн на треньорите не може да бъде по-малко от 10 символа. Ако е така, то въвеждането да се прекрати. За целта ще създадем тригер, който се активира преди INSERT:
DROP TRIGGER if exists before_coaches_insert; delimiter | CREATE TRIGGER before_coaches_insert BEFORE INSERT ON coaches FOR EACH ROW BEGIN IF(CHAR_LENGTH(NEW.egn) < 10) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'The egn must be 10 characters.'; end if; END; | delimiter ;
Тестваме с невалидно EGN:
INSERT INTO `school_sport_clubs`.`coaches` (`name`, `egn`, `month_salary`, `hour_salary`) VALUES ('Ivan Iordanov Petrov', '7452', '1500', '14');
Резултатът е:
Съответно записът не е въведен в таблицата. Но пък може да бъде въведен при UPDATE, затова може да направите и такъв тригер.
С разгледаното по -горе ние всъщност симулирахме ограниченито check, за което казахме че в MySQL не работи, но съществува синтактично. На практика чрез сигнал в тригера можем да спрем вкарването на некоректни данни.
Реализиране на повече от един тригер за един и същи trigger event в един и същи action time:
Споменахме, че след версия 5.7.2 на MySQL вече може да се правят повече от 1 тригери за един и същи trigger event и един и същи action time. За да се случи това, трябва да се въведе последователност на тригерите- кой след кой ще се извиква. За целта въвеждаме двете ключови думи FOLLOWS и PRECEDES след FOR EACH ROW. Останалата част от синтаксиса не се променя. Само уточняваме след или преди кой друг тригер да се изпълни новия. Нека да направим още един тригер, който да се изпълни след before_coaches_insert.
DROP TRIGGER if exists before_coaches_insert2; delimiter | CREATE TRIGGER before_coaches_insert2 BEFORE INSERT ON coaches FOR EACH ROW FOLLOWS before_coaches_insert BEGIN IF(CHAR_LENGTH(NEW.name) = 0) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'The name is empty.'; end if; END; | delimiter ;
Доста излишно е да се създадат два тригера, които да сработват един след друг, защото спокойно може да обедините заявките си в тялото на единия. Така или иначе то може да съдържа множество заявки между BEGIN и END.
Даниел Джолев