MySQL中upcID为NULL时生成唯一ID的四种方案
2025-03-10 03:04:29
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。
原理:
- 触发器(Trigger): 在插入操作 之前 触发一段自定义的 SQL 代码。
- 循环检查: 生成一个初始的
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 为你的表名
代码解释:
DELIMITER $$
: 更改语句分隔符为$$
,因为触发器定义里会用到分号;
。CREATE TRIGGER generate_upc_id ...
: 创建一个名为generate_upc_id
的触发器,在表your_table
的每次INSERT
前(BEFORE
)触发。FOR EACH ROW
: 触发器对每一行都执行。DECLARE next_upc BIGINT;
: 声明一个变量来存储将要生成的 UPC。DECLARE upc_exists INT;
: 声明一个变量来看ID是否已存在。IF NEW.upcID IS NULL THEN
: 检查即将插入的upcID
是否为 NULL。SET next_upc = 999910000000;
: 如果是 NULL,设定初始upc。loop_label: LOOP ... END LOOP;
: 开始一个循环,loop_label
是循环的标签。SELECT COUNT(*) INTO upc_exists ...
: 查询表中是否存在当前next_upc
。IF upc_exists = 0 THEN ... END IF
: 如果不存在,就用它!然后LEAVE loop_label;
退出循环。SET next_upc = next_upc + 1;
: 否则自增一下,继续尝试.DELIMITER ;
: 把语句分隔符改回分号;
。
安全建议:
- 在高并发插入的情况下,这个触发器可能会有性能问题,因为循环检查会消耗一定资源。如果有大量并发插入,可能会导致锁等待。
- 确保触发器内的 SQL 逻辑尽量高效,避免全表扫描。
进阶使用技巧:
为了提升性能,可以在你的 upcID
列上创建一个索引:
CREATE INDEX idx_upcID ON your_table (upcID);
这将加快触发器中 SELECT COUNT(*)
的查询速度。
方案二:使用辅助表 + 触发器
用一个单独的表来记录已生成的ID, 这样比每次都查整个数据表快多了。
原理:
- 辅助表: 创建一个专门用来存储已分配的
upcID
的表。这个表可以非常简单,只有一个自增主键和一个存储upcID
的列。 - 触发器: 在插入库存数据之前,先向辅助表插入一条记录,获取自增 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.
原理:
- 获取最大 ID: 从数据库中查询当前最大的
upcID
。 - 生成新 ID: 在应用代码中,将最大 ID 加 1,然后拼接上前缀 '99991'。
- 插入数据: 将生成的
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()
代码解释:
generate_next_upc(cursor)
: 函数负责生成ID。SELECT MAX(upcID) ...
: 从数据库里查出最大的upcID
。if result[0] is None:
如果表里没有数据,就用初始值999910000000
。max_upc_num = ...
: 如果表里有数据,提取出upcID
的数字部分(去掉开头的 '99991' )。 如果 最大ID不是'99991'开头, 说明有问题,从0开始。next_upc = ...
: 在最大的数字部分上加 1,然后拼接'99991'。- 其余代码为链接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 转换成纯数字,并截取成需要的长度. 比较繁琐,这里不细说了.
###总结
几种方法各有优劣. 根据实际情况, 仔细权衡后选一种最适合自己的!