返回

MySQL中upcID为NULL时生成唯一ID的四种方案

mysql

MySQL 中 upcID 为 NULL 时生成唯一 ID 的方案

最近有个项目,需要用 UPC 条形码的值来索引库存商品。库存表的主键、唯一列是 upcID,这个列 不是 自增的。扫描条形码添加商品时,直接用条形码的 GS1 编号作为 ID。现在有个需求:要能添加 没有 条形码的商品,同时生成一个以 '99991' 开头的 12 位数字作为 "伪造" 的 UPC 制造商部分。生成的 ID 还需要加上 7 位数的唯一部分,最终的 ID 看起来像这样: '999910000001'。生成好ID插入表里后, 就能打印条形码标签贴到这些商品上。

琢磨了一下,想用一个插入前触发器(before insert trigger)来实现:如果插入的 ID 是 NULL,就生成一个编号,然后跟表里的数据比对,如果重复了就递增,直到生成一个唯一的,再用这个唯一的 ID 进行插入。 大致思路就是文章开头的这段不太成熟的代码。

所以有没有啥好办法能实现? 下面就聊聊怎么搞定这个问题。

问题根源

问题的核心在于:如何在插入数据时,为没有条形码的商品自动生成一个唯一的 upcID,同时还要保证这个 ID 符合特定的格式要求(以 '99991' 开头,总共 12 位),且不能与已有的 ID 冲突。直接用自增ID不行, 因为要符合'99991'开头这个格式.

解决方案

方案一: 使用触发器 + 循环

这就是最开始想到的办法。插入数据前触发, 判断如果 upcID 是 NULL, 就开始循环生成ID。

原理:

  1. 触发器(Trigger): 在插入操作 之前 触发一段自定义的 SQL 代码。
  2. 循环检查: 生成一个初始的 upcID,然后在一个循环里检查这个 ID 是否已存在。如果存在,就增加 ID 的值,再次检查,直到找到一个唯一的。

代码示例:

DELIMITER $

CREATE TRIGGER generate_upc_id BEFORE INSERT ON your_table
FOR EACH ROW
BEGIN
    DECLARE next_upc BIGINT;
    DECLARE upc_exists INT;

    IF NEW.upcID IS NULL THEN
        SET next_upc = 999910000000;

        loop_label: LOOP
            SELECT COUNT(*) INTO upc_exists FROM your_table WHERE upcID = next_upc;
            
            IF upc_exists = 0 THEN
                SET NEW.upcID = next_upc;
                LEAVE loop_label;
            ELSE
                SET next_upc = next_upc + 1;
            END IF;
        END LOOP;
    END IF;
END$

DELIMITER ;

-- 替换 your_table 为你的表名

代码解释:

  1. DELIMITER $$: 更改语句分隔符为 $$,因为触发器定义里会用到分号 ;
  2. CREATE TRIGGER generate_upc_id ...: 创建一个名为 generate_upc_id 的触发器,在表your_table的每次INSERT前(BEFORE)触发。
  3. FOR EACH ROW: 触发器对每一行都执行。
  4. DECLARE next_upc BIGINT;: 声明一个变量来存储将要生成的 UPC。
  5. DECLARE upc_exists INT;: 声明一个变量来看ID是否已存在。
  6. IF NEW.upcID IS NULL THEN: 检查即将插入的 upcID 是否为 NULL。
  7. SET next_upc = 999910000000;: 如果是 NULL,设定初始upc。
  8. loop_label: LOOP ... END LOOP;: 开始一个循环,loop_label 是循环的标签。
  9. SELECT COUNT(*) INTO upc_exists ...: 查询表中是否存在当前 next_upc
  10. IF upc_exists = 0 THEN ... END IF: 如果不存在,就用它!然后LEAVE loop_label; 退出循环。
  11. SET next_upc = next_upc + 1;: 否则自增一下,继续尝试.
  12. DELIMITER ;: 把语句分隔符改回分号 ;

安全建议:

  • 在高并发插入的情况下,这个触发器可能会有性能问题,因为循环检查会消耗一定资源。如果有大量并发插入,可能会导致锁等待。
  • 确保触发器内的 SQL 逻辑尽量高效,避免全表扫描。

进阶使用技巧:

为了提升性能,可以在你的 upcID 列上创建一个索引:

CREATE INDEX idx_upcID ON your_table (upcID);

这将加快触发器中 SELECT COUNT(*) 的查询速度。

方案二:使用辅助表 + 触发器

用一个单独的表来记录已生成的ID, 这样比每次都查整个数据表快多了。

原理:

  1. 辅助表: 创建一个专门用来存储已分配的 upcID 的表。这个表可以非常简单,只有一个自增主键和一个存储 upcID 的列。
  2. 触发器: 在插入库存数据之前,先向辅助表插入一条记录,获取自增 ID,然后用这个 ID 加上 '99991' 的前缀,生成最终的 upcID

代码示例:

-- 1. 创建辅助表
CREATE TABLE upc_id_generator (
  id INT AUTO_INCREMENT PRIMARY KEY,
  used_upc BIGINT
);

-- 2. 创建触发器
DELIMITER $

CREATE TRIGGER generate_upc_id_with_helper BEFORE INSERT ON your_table
FOR EACH ROW
BEGIN
    DECLARE next_id INT;

    IF NEW.upcID IS NULL THEN
        -- 插入辅助表, 获取自增 ID
        INSERT INTO upc_id_generator (used_upc) VALUES (NULL);
        SET next_id = LAST_INSERT_ID();
        
        -- 生成完整的 upcID
        SET NEW.upcID = 999910000000 + next_id;
        
         -- 更新辅助表,标记这个 upcID 已经被用了。不是必要的步骤, 看自己需求
        UPDATE upc_id_generator SET used_upc = NEW.upcID WHERE id = next_id;

    END IF;
END$

DELIMITER ;

解释:

  • 首先,创建一个 upc_id_generator表,它有一个自增的id列和存储最终upcID的used_upc列。
  • 在触发器里,先往 upc_id_generator表里插入一条空数据,然后通过LAST_INSERT_ID() 获取刚刚插入的自增ID。
  • 然后就简单了,直接拼接'99991'和自增ID, 得到最终的 upcID
  • 为了追踪哪个 upcID 真正被用掉, 可以选择更新 upc_id_generator表里的 used_upc列.

安全建议:

  • 这种方法比方案一更安全,因为它避免了在高并发环境下可能的竞争条件,使用辅助表的自增 ID 保证了唯一性。
  • 注意控制辅助表的增长,定期清理不需要的记录,以避免浪费存储空间. 如果不想追踪,完全可以不用used_upc这一列。

进阶技巧:

  • 如果确定 upc_id_generator 表永远不会有非常多的数据(远少于INT最大值), 可以定期 TRUNCATE TABLE upc_id_generator; 清空并重置自增ID,避免ID过大. 当然,得先保证这段时间不会生成新的 upcID.

方案三:应用程序层生成

如果不想在数据库层面做太多事情, 直接在应用代码里生成 upcID 也完全OK.

原理:

  1. 获取最大 ID: 从数据库中查询当前最大的 upcID
  2. 生成新 ID: 在应用代码中,将最大 ID 加 1,然后拼接上前缀 '99991'。
  3. 插入数据: 将生成的 upcID 与其他数据一起插入到数据库中。

代码示例 (Python):

import mysql.connector

def generate_next_upc(cursor):
    """
    生成下一个唯一的 upcID.
    
    Args:
        cursor: 数据库游标
    
    Returns:
        下一个可用的 upcID (str)
    """
    cursor.execute("SELECT MAX(upcID) FROM your_table")  #查询最大的upc
    result = cursor.fetchone()

    if result[0] is None:  
        # 如果表中没有数据,从初始值开始
        next_upc = 999910000000
    else:
       # 提取出数字部分
        max_upc_num = int(str(result[0])[5:]) if str(result[0]).startswith('99991') else 0
        next_upc = 999910000000 + (max_upc_num + 1)  # 最大ID数字部分加+1.

    return str(next_upc)

# 使用示例:
mydb = mysql.connector.connect(
  host="your_host",
  user="your_user",
  password="your_password",
  database="your_database"
)

cursor = mydb.cursor()

# 生成新的 upcID
new_upc = generate_next_upc(cursor)

# 假设你有一些商品数据要插入
sql = "INSERT INTO your_table (upcID, product_name, ...) VALUES (%s, %s, ...)"
val = (new_upc, "Product without barcode", ...) 

cursor.execute(sql, val)
mydb.commit()
print(cursor.rowcount, "record inserted.")
cursor.close()
mydb.close()

代码解释:

  1. generate_next_upc(cursor): 函数负责生成ID。
  2. SELECT MAX(upcID) ...: 从数据库里查出最大的upcID
  3. if result[0] is None: 如果表里没有数据,就用初始值 999910000000
  4. max_upc_num = ...: 如果表里有数据,提取出 upcID 的数字部分(去掉开头的 '99991' )。 如果 最大ID不是'99991'开头, 说明有问题,从0开始。
  5. next_upc = ...: 在最大的数字部分上加 1,然后拼接'99991'。
  6. 其余代码为链接mysql并插入数据。

安全建议:

  • 在高并发环境下,这种方法可能会有ID重复的风险. 因为多个客户端可能同时查到同一个最大 ID, 导致生成一样的新 ID。 解决办法
    • 使用数据库事务, 并在事务中查询最大ID和插入数据。
    • 加锁。 查询最大ID时加锁(SELECT ... FOR UPDATE),阻止其他事务同时读取。
    • 利用数据库的 unique约束. 插入时如果冲突, 捕获异常,重新生成ID。
  • 要考虑网络延迟之类的,尽量减少数据库的查询次数。

方案四:使用UUID

直接用UUID.虽然不完全符合以"99991"开头的需求, 但是绝对唯一!

原理 :
直接生成 UUID, 插入。简单粗暴!

代码示例:

INSERT INTO your_table (upcID, ...) values (UUID(), ...);

如果要保留部分 “99991” 特性,可以在UUID前拼接。 但这不保证唯一性,还需要对这个字段创建unique index, 让mysql保证唯一。

INSERT INTO your_table (upcID, ...) values (CONCAT('99991', REPLACE(UUID(),'-','')), ...);
ALTER TABLE your_table ADD UNIQUE INDEX upcID_unique (upcID);

这种方式的优点在于简单快捷,但UUID毕竟不是纯数字, 长度也更长。 如果追求格式统一, 可以用触发器把 UUID 转换成纯数字,并截取成需要的长度. 比较繁琐,这里不细说了.
###总结
几种方法各有优劣. 根据实际情况, 仔细权衡后选一种最适合自己的!