Задача: создавать ID сертификатов в формате год (2 знака), месяц (2 знака), порядковый номер в текущем месяце с обнулением 1 числа каждого месяца (4 знака). Например, для 19 июня 2018 года первый сертификат будет 18060001, следующий 18060002.
Решения два:
- составной ключ
год-месяц(триггер) +номер(AUTO_INCREMENT) - один ключ, полностью по триггеру
Первому решению я доверяю больше, не будут ошибки вставки, когда два запроса придут одновременно.
Во втором случае в момент работы триггера может возникнуть ситуация, когда SELECT уже отработал, но до фактической вставки данные уже изменились, и ID уже занят. Тогда возникнет ошибка первичного ключа. Это чисто теоретически.
Таблица:
CREATE TABLE `api_certs` (
`ID` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'ID сертификата: год (2 знака), месяц (2 знака), порядковый номер в текущем месяце с обнулением 1 числа каждого месяца (4 знака), f.e. 18060001',
`DATE_CREATED` DATETIME NOT NULL DEFAULT '1000-01-01 00:00:00' COMMENT 'Дата создания',
PRIMARY KEY (`ID`)
) ENGINE=INNODB;Дата создания выбрана не '0000-00-00 00:00:00', так как с версии MySQL 5.7 возникнет ошибка Invalid default value for 'DATE_CREATED'
Триггер:
-- Удаляем старый триггер, если он есть
DROP TRIGGER IF EXISTS `api_certs_before_insert`;
-- Временно меняем разделитель выражений
DELIMITER $$
-- Создаем триггер
CREATE TRIGGER `api_certs_before_insert` BEFORE INSERT ON `api_certs`
FOR EACH ROW BEGIN
-- Префикс "год-месяц"
SET @init_id_prefix=DATE_FORMAT(NOW(), "%y%m");
-- Следующий номер, нумерация не с 0, а с 1
SET @init_id=@init_id_prefix * 10000 + 1;
-- LIKE префикс нужен для исключения ошибки, см объяснение ниже
SET @init_id_prefix=CONCAT(@init_id_prefix, '%');
-- Выбираем следующий номер в этом временном периоде, либо @init_id
SELECT IFNULL(MAX(ID) + 1, @init_id) INTO @newID FROM `api_certs` WHERE `ID` >= @init_id AND `ID` LIKE @init_id_prefix;
-- Перезаписываем ID вычисленным значением
SET NEW.ID = @newID;
END;
$$
-- Возвращаем разделитель выражений
DELIMITER ;Конструкция ID LIKE @init_id_prefix необходима для защиты от ошибки смены времени на сервере:
- дата: июнь 2018 года => префикс
1806 - поменяли дату сервера вперед на 1 месяц => префикс
1807 - произошла вставка: 1807001
- вернули дату сервера назад => префикс
1806 - без условия
ID LIKEследующий номер будет 1807002, а не 1806001 (правильный вариант)
Проверим разные варианты вставки:
INSERT INTO `api_certs` VALUES
(DEFAULT, DEFAULT),
(DEFAULT, DEFAULT);
INSERT INTO `api_certs` VALUES (DEFAULT, DEFAULT);mysql> SELECT * FROM `api_certs`;
+----------+---------------------+
| ID | DATE_CREATED |
+----------+---------------------+
| 18060001 | 1000-01-01 00:00:00 |
| 18060002 | 1000-01-01 00:00:00 |
| 18060003 | 1000-01-01 00:00:00 |
+----------+---------------------+