【问题标题】:How to get next alpha-numeric ID based on existing value from MySQL如何根据 MySQL 的现有值获取下一个字母数字 ID
【发布时间】:2011-11-05 15:15:06
【问题描述】:

首先,如果之前有人问过这个问题,我深表歉意 - 我确定确实有,但我找不到/不知道要搜索什么才能找到它。

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

公司名称参考 史密斯细木工 smit0001 史密斯和琼斯咨询公司 smit0002 史密森地毯 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 触发器可能是处理此问题的方法,但是:

  • 我对 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?

或者我愿意接受任何其他建议的方法来解决这个问题。

【问题讨论】:

  • 您是否考虑过使用触发器并在触发器中生成该 id?
  • 不,但就这类项目而言,SQL 绝对是我最薄弱的科目,我真的不知道如何有效地使用触发器 - 你能给我一个基本的介绍吗?完毕?如果你能指出一些相关阅读材料的方向,我会很高兴:-)
  • This article 谈到了使用 mysql 在 GUID 上自动生成,我认为它会给你一个开始的地方
  • 谢谢,我会玩一玩,看看我能想出什么
  • 这似乎会减少生成重复的可能性(通过更快),但不能完全消除它 - 如果同时插入两行导致触发器同时运行会发生什么,并产生相同的参考?有没有办法以只能有一个并发实例的方式锁定触发器(或 UDF)?还是有其他方法来解决这个问题?

标签: php mysql


【解决方案1】:

如果您使用的是 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 开头,等等。

优点:

  • 您无需对数字进行任何操作。

缺点:

  • 您确实有一个分成 2 列的键。
  • 它不适用于 InnoDB。

【讨论】:

  • 这看起来是一个不错的选择,也是公认答案的不错候选 - 大概这样做我只是将INSERT alphas 放入key_prefix 列,而key_increment 生成一个数字部分?当通过用户输入参考进行搜索时,它会使SELECT 逻辑稍微变得更加复杂,但除了那个小点之外,这确实满足了所有要求......非常感谢!
  • 我的存储引擎是当前的 InnoDB(虽然我不是特别喜欢这个)——出于兴趣,为什么只能用 MyISAM 做这个?
  • 我也想知道——来自 Innodb 限制下的 mysql 手册“对于 AUTO_INCREMENT 列,您必须始终为表定义一个索引,并且该索引必须只包含 AUTO_INCREMENT 列。在 MyISAM 表中, AUTO_INCREMENT 列可能是多列索引的一部分。"
  • MyISAM 对修改使用表锁定。因此表锁定,检索最高 ID,递增,然后插入。 InnoDB 使用记录锁定(就像大多数“现代”数据库一样)。在启动时,InnoDB 计算出最高的自动增量,然后将其缓存并用于所有新的插入 ID。
【解决方案2】:

这个解决方案有一个触发器和一个表来保存 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 个答案,你会得到另一个接受,因为它是你的组合帮助我找到问题的正确解决方案的答案,因为您提供了一个很好的触发器创建示例。我遇到的问题是运行创建查询时DELIMITER 的更改——我最初尝试这样做时不知何故错过了这一点。你绝对值得(至少)我给你的 +1...
【解决方案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:还测试了没有返回初始行的情况(您会认为没有初始行可以锁定)并且效果也很好。

【讨论】:

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

    【讨论】:

    • 这种方法有点无效,因为您将重新尝试插入,直到您最终设法避免冲突。尽管如此,你还是从当前的最大参考值 + 1 开始,除非你有很多插入,否则你很有可能在第一次尝试时成功,并且实际上保证在第二次尝试时成功。
    • 在 Brent Baisley 的回答之前,我一直在想我最终会做的事情,我只是不喜欢它,因为很长的循环试图在正如您在评论中提到的那样,发生大量独立插入的事件。我知道这是围绕一个不应该永远不会发生的情况进行编程,但如果可能的话,我仍然想避免。
    • Brent 的解决方案确实比我的好,我实际上不知道 myisam 跟踪整个主键而不是实际列的自动增量。
    • 这是一个我以前不知道的有趣的 MySQL 功能:ON DUPLICATE KEY UPDATE 语法实际上将处理数据库层中的循环。 dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html
    【解决方案5】:

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

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

    如果我遇到你的情况,我会在应用层处理公司参考 ID 生成,如果设置不正确,触发器会变得混乱。

    【讨论】:

      【解决方案6】:

      同样适用于 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 中的函数

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-01-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多