首先,如果以前曾经问过这件事,我很抱歉 - 事实上我确定它有,但我找不到它/找不到要搜索的东西。
我需要根据公司名称生成唯一的快速参考ID。例如:
Company Name Reference Smiths Joinery smit0001 Smith and Jones Consulting smit0002 Smithsons Carpets smit0003
这些都将存储在MySQL表的varchar列中。数据将被收集,转义和插入,如'HTML - > PHP - > MySQL的”。 ID应该采用上面描述的格式,四个字母,然后是四个数字(最初至少 - 当我到达smit9999
时,它只会溢出到5位数字。)
我可以处理从公司名称生成4个字母,我将简单地逐步浏览名称,直到我收集了4个字母字符,然后strtolower()
- 但我需要获取下一个可用的数字。
最好/最简单的方法是什么,以便消除重复的可能性?
目前我在想:
$fourLetters = 'smit';
$query = "SELECT `company_ref`
FROM `companies`
WHERE
`company_ref` LIKE '$fourLetters%'
ORDER BY `company_ref` DESC
LIMIT 1";
$last = mysqli_fetch_assoc(mysqli_query($link, $query));
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1;
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT);
但是,如果两个用户尝试输入同时导致相同ID的公司名称,我可以看到这会导致问题。我将在列上使用唯一索引,因此它不会导致数据库中出现重复,但仍会导致问题。
当我进行插入时,有没有人能想到让MySQL为我工作的方法,而不是事先在PHP中计算它?
请注意,实际代码将是OO并将处理错误等 - 我只是想知道是否有更好的方法来执行此特定任务,它更多地是关于SQL而不是其他任何内容。
修改
我认为@ EmmanuelN建议使用MySQL触发器可能是解决这个问题的方法,但是:
what happens if two rows are inserted at the same time that result in the trigger running simultaneously, and produce the same reference? Is there any way to lock the trigger (or a UDF) in such a way that it can only have one concurrent instance?
。或者我会接受任何其他建议的解决此问题的方法。
答案 0 :(得分:7)
如果您使用的是MyISAM,则可以在文本字段+自动增量字段上创建复合主键。 MySQL将自动处理递增数字。它们是单独的字段,但您可以获得相同的效果。
CREATE TABLE example (
company_name varchar(100),
key_prefix char(4) not null,
key_increment int unsigned auto_increment,
primary key co_key (key_prefix,key_increment)
) ENGINE=MYISAM;
当您在表格中插入时,key_increment
字段将根据基于key_prefix
的最高值递增。因此,插入key_prefix
“smit”将从key_inrement
中的1开头,key_prefix
“jone”将以key_inrement
中的1开头,等等。
优点:
缺点:
答案 1 :(得分:3)
这个带有触发器和表的解决方案如何保持company_ref的唯一性。进行了更正 - 如果您希望每个唯一的4char序列的编号从1开始,则参考表必须是MyISAM。
DROP TABLE IF EXISTS company;
CREATE TABLE company (
company_name varchar(100) DEFAULT NULL,
company_ref char(8) DEFAULT NULL
) ENGINE=InnoDB
DELIMITER ;;
CREATE TRIGGER company_reference BEFORE INSERT ON company
FOR EACH ROW BEGIN
INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL;
SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0'));
END ;;
DELIMITER ;
DROP TABLE IF EXISTS reference;
CREATE TABLE reference (
company_ref char(4) NOT NULL DEFAULT '',
numeric_ref int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (company_ref, numeric_ref)
) ENGINE=MyISAM;
为了完整性,这里有一个触发器,如果公司名称被更改,它将创建一个新的引用。
DROP TRIGGER IF EXISTS company_reference_up;
DELIMITER ;;
CREATE TRIGGER company_reference_up BEFORE UPDATE ON company
FOR EACH ROW BEGIN
IF NEW.company_name <> OLD.company_name THEN
DELETE FROM reference WHERE company_ref=SUBSTRING(LOWER(OLD.company_ref), 1, 4) AND numeric_ref=SUBSTRING(OLD.company_ref, 5, 4);
INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL;
SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0'));
END IF;
END;
;;
DELIMITER ;
答案 2 :(得分:3)
鉴于您正在使用InnoDB,为什么不使用显式事务来获取独占行锁定并阻止其他连接在您基于它设置新ID之前读取同一行?
(当然,在触发器中进行计算会使锁定的时间更短。)
mysqli_query($link, "BEGIN TRANSACTION");
$query = "SELECT `company_ref`
FROM `companies`
WHERE
`company_ref` LIKE '$fourLetters%'
ORDER BY `company_ref` DESC
LIMIT 1
FOR UPDATE";
$last = mysqli_fetch_assoc(mysqli_query($link, $query));
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1;
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT);
mysqli_query($link, "INSERT INTO companies . . . (new row using $newref)");
mysqli_commit($link);
编辑:只是为了100%确定我手动运行测试以确认第二个事务将在等待而不是原始锁定行之后返回新插入的行。
Edit2:还测试了没有返回初始行的情况(你认为没有初始行可以锁定),这也是有效的。
答案 3 :(得分:1)
答案 4 :(得分:0)
避免引用列重复值的最简单方法是添加唯一约束。因此,如果多个进程尝试设置为相同的值,MySQL将拒绝第二次尝试并抛出错误。
ALTER TABLE table_name ADD UNIQUE KEY (`company_ref`);
如果我遇到你的情况,我会处理应用层内的公司参考ID生成,如果设置不正确,触发器会变得混乱。
答案 5 :(得分:0)
适用于InnoDB的hacky版本。
在事务中使用两个插入替换插入到companies
:
INSERT INTO __keys
VALUES (LEFT(LOWER('Smiths Joinery'),4), LAST_INSERT_ID(1))
ON DUPLICATE KEY UPDATE
num = LAST_INSERT_ID(num+1);
INSERT INTO __companies (comp_name, reference)
VALUES ('Smiths Joinery',
CONCAT(LEFT(LOWER(comp_name),4), LPAD(LAST_INSERT_ID(), 4, '0')));
其中:
CREATE TABLE `__keys` (
`prefix` char(4) NOT NULL,
`num` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`prefix`)
) ENGINE=InnoDB COLLATE latin1_general_ci;
CREATE TABLE `__companies` (
`comp_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`comp_name` varchar(45) NOT NULL,
`reference` char(8) NOT NULL,
PRIMARY KEY (`comp_id`)
) ENGINE=InnoDB COLLATE latin1_general_ci;
注意:
latin1_general_ci
可以替换为utf8_general_ci
,LEFT(LOWER('Smiths Joinery'),4)
最好成为PHP中的一个函数