2011-11-05 56 views
7

首先,我很抱歉如果之前已经问过这个问题 - 事实上我确定它已经存在了,但是我找不到它/无法计算出搜索以找到它。如何根据MySQL中的现有值获取下一个字母数字ID

我需要根据公司名称生成唯一的快速参考ID。因此,例如:

 
Company Name    Reference 
Smiths Joinery    smit0001 
Smith and Jones Consulting smit0002 
Smithsons Carpets   smit0003 

这些都将被存储在一个MySQL表中的VARCHAR列。数据将被收集,转义并插入,如'HTML - > PHP - > MySQL'。身份证的格式应该是上面描述的格式,四个字母,然后是四个数字(最初至少 - 当我到达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中计算它?

需要注意的是实际的代码将面向对象和将处理错误等等 - 我只是在寻找是否有更好的方法来做到这一点特定任务的想法,它是更多关于SQL比什么都重要。

编辑

我觉得@使用MySQL的触发器的EmmanuelN的建议可能是处理这个问题的方法,但是:

  • 我不够好与MySQL,尤其触发器,以让它起作用,并且想要创建,添加和使用触发器的分步示例。
  • 我还不确定这是否会消除生成两个相同ID的可能性。请参阅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

您是否想过在触发器中使用触发器并生成该ID? –

+0

不,但是SQL绝对是我在这类项目中最弱的话题,我真的不知道如何有效地使用触发器 - 你能否给我一个基本的如何做到这一点?如果你能指点我一些相关阅读材料的方向,我会很高兴:-) – DaveRandom

+1

[本文](http://www.sugarcrm.com/forums/f6/developed-auto-generate-ids-guids -within-mysql-2895 /)讲述了在使用mysql的GUID上自动生成,我认为它会给你开始的地方 –

回答

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”插入将与1 key_inrementkey_prefix“琼”将与key_inrement 1日开始启动,等

优点:

  • 你不必与计算的数字做任何事。

缺点:

  • 你确实有跨2列的密钥的分离。
  • 它不适用于InnoDB。
+0

这看起来是一个很好的选择和一个很好的候选人接受的答案 - 大概这样做,我只是'将'alpha'插入'key_prefix'列,'key_increment'生成一个数字部分?当用户输入引用进行搜索时,它会使'SELECT'逻辑*稍微复杂一点,但除了这个小点之外,它可以满足所有需求......非常感谢! – DaveRandom

+1

我的存储引擎是当前的InnoDB(尽管我不特别强调这一点) - 出于兴趣,为什么只能用MyISAM来做到这一点? – DaveRandom

+0

我也想知道 - 从Innodb限制下的mysql手册开始“对于AUTO_INCREMENT列,您必须始终为该表定义索引,并且该索引必须仅包含AUTO_INCREMENT列。在MyISAM表中,AUTO_INCREMENT列可能是多列索引“。 –

1
  1. 确保您在“参考”列上具有唯一的约束。
  2. 以您在示例代码中使用的方式获取当前最大顺序引用。在转换为(int)之前,实际上并不需要修整零,'0001'是一个有效的整数。
  3. 滚动一个循环,然后在里面插入。
  4. 插入后检查受影响的行。您还可以检查SQL状态是否有重复的键错误,但是如果行数为零,则表明由于插入了现有的引用值而导致插入失败。
  5. 如果您有零受影响的行,请递增顺序号,然后再次滚动循环。如果您有非零的受影响的行,则完成并插入一个唯一的标识符。
+0

此方法稍微无效,因为您将重新尝试插入,直到最终避免冲突。尽管如此,您仍然从当前的最大参考值+1开始,除非您有很多插入,否则很可能在第一次尝试中取得成功,并且实际上保证在第二次尝试中取得成功。 – lanzz

+0

这就是我想我最终会做的事,直到布伦特拜斯利的回答,我只是不喜欢它,因为一个漫长的循环试图找到下一个可用的身份证,在大量独立插入,就像你在评论中提到的那样。我知道这是非常多的编程环境,应该永远不会发生,但如果可能的话,我仍然希望避免。 – DaveRandom

+0

布伦特的解决方案确实比我的解决方案更好,我实际上并不知道myisam会跟踪整个主键的自动增量,而不是实际的列。 – lanzz

0

最简单的避免参考列重复值的方法是添加一个唯一约束。所以如果多个进程尝试设置相同的值,MySQL将拒绝第二次尝试并抛出错误。

ALTER TABLE table_name ADD UNIQUE KEY (`company_ref`); 

如果我面对你的情况,我会处理的应用层中的公司参考ID生成,触发会导致混乱,如果没有设置正确。

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 ; 
+0

我会接受布伦特的答案,因为他已经提供了一个很好的解释,为什么这是正确的方法,但如果我可以接受2个答案,你会得到另一个答案,因为它是你的答案的组合,帮助我找到正确的解决方案,因为你提供了一个创建触发器的好例子。我遇到的问题是运行创建查询时的DELIMITER变化 - 当我最初尝试去做时,我总是错过了。你绝对应得的(至少)我给你的+1 ... – DaveRandom

+0

@Dave :-)很酷 - 很高兴它帮助你 –

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:还测试了没有返回初始行的情况(你会认为没有最初的行来放置锁),并且这也起作用。

0

一款适用于InnoDB的黑客版本。

与两个刀片更换嵌入到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中的函数
相关问题