1. 插入数据后拿不到自增 ID,问题到底出在哪
Python 与 MySQL 数据库交互时,获取插入后的自增 ID 是个高频需求:新增用户后要拿 user_id 去建权限记录,新增订单后要拿 order_id 去写日志,新增文章后要拿 article_id 去做重定向。这些场景都指向同一个动作——INSERT 成功后,把数据库生成的那个 AUTO_INCREMENT 值取回来。
但实际操作里,很多人会踩到几类坑:cursor.lastrowid 返回 None,以为驱动坏了;用了 executemany 批量插入,发现只拿到一个 ID;在连接池里多线程跑,担心拿到的 ID 是别人的;或者干脆用 SELECT LAST_INSERT_ID() 又写错会话,拿到上一次的旧值。
这篇聚焦 Python 通过 mysql-connector-python 或 PyMySQL 插入数据后获取 lastrowid 的完整链路,覆盖 cursor.lastrowid、SELECT LAST_INSERT_ID() 与 executemany 场景差异。同时给出一套可复制的 settings.json / config.toml 骨架,以及用 TaoToken 统一 Key 接入配置的方式,让数据库凭据和模型调用凭据都走同一套管理思路。适合正在写后端 CRUD、做数据同步脚本,或者刚接触 MySQL 自增主键的开发者。
2. 前置准备:TaoToken 统一 Key 与数据库凭据管理
在写代码之前,先把凭据管理这件事理清楚。很多教程让你把数据库密码硬编码在 Python 文件里,这在本地跑 demo 没问题,一旦进版本库就是事故。我的做法是:数据库连接参数放配置文件,模型调用凭据走 TaoToken 统一 Key,两者都不进代码。
TaoToken 在这里的角色是统一管理模型调用的 API Key。当你的 Python 脚本除了写 MySQL,还要调用大模型做数据清洗、字段补全、内容摘要时,把模型 Key 收敛到一处会省很多事。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。
先拿到 Key:进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 创建一个 Key。这个 Key 后面会写进配置文件,供脚本读取。
数据库这边,先建库建表。表必须有 AUTO_INCREMENT 主键,这是 lastrowid 能返回有意义值的前提:
CREATE DATABASE IF NOT EXISTS mydatabase DEFAULT CHARSET utf8mb4; USE mydatabase; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, registered_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;注意 ENGINE=InnoDB。MyISAM 虽然也支持 AUTO_INCREMENT,但事务和行锁行为不同,现代项目一律用 InnoDB。
3. 可复制配置:settings.json 与 config.toml 骨架
配置文件的作用是把「会变的东西」抽出来。下面给两份骨架,选你顺手的格式即可。
3.1 settings.json 骨架
{ "mysql": { "host": "127.0.0.1", "port": 3306, "user": "app_user", "password": "REPLACE_WITH_ENV_OR_SECRET", "database": "mydatabase", "charset": "utf8mb4", "autocommit": false, "connection_timeout": 10 }, "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "REPLACE_WITH_YOUR_TAOTOKEN_KEY", "default_model": "claude-sonnet-4-5" } }3.2 config.toml 骨架
[mysql] host = "127.0.0.1" port = 3306 user = "app_user" password = "REPLACE_WITH_ENV_OR_SECRET" database = "mydatabase" charset = "utf8mb4" autocommit = false connection_timeout = 10 [taotoken] base_url = "https://taotoken.net/api" api_key = "REPLACE_WITH_YOUR_TAOTOKEN_KEY" default_model = "claude-sonnet-4-5"读取配置的代码,JSON 用内置 json,TOML 用 tomllib(Python 3.11+):
import json from pathlib import Path def load_settings(path: str = "settings.json") -> dict: with Path(path).open("r", encoding="utf-8") as f: return json.load(f) settings = load_settings() DB_CONFIG = settings["mysql"] TAOTOKEN_CONFIG = settings["taotoken"]注意:配置文件里的 password 和 api_key 建议用环境变量覆盖,例如 os.environ.get("MYSQL_PASSWORD", settings["mysql"]["password"]),这样配置文件可以安全进仓库。
4. 核心链路:cursor.lastrowid 与 SELECT LAST_INSERT_ID()
4.1 安装驱动
pip install mysql-connector-python # 或者用 PyMySQL pip install pymysql两者在 lastrowid 行为上基本一致,下面以 mysql-connector-python 为主,PyMySQL 的差异会单独标注。
4.2 单行插入并获取自增 ID
import mysql.connector def insert_user_and_get_id(db_config: dict, username: str, email: str): last_id = None conn = None cursor = None try: conn = mysql.connector.connect(**db_config) cursor = conn.cursor() sql = "INSERT INTO users (username, email) VALUES (%s, %s)" cursor.execute(sql, (username, email)) conn.commit() last_id = cursor.lastrowid print(f"插入成功,username={username}, id={last_id}") except mysql.connector.Error as err: print(f"插入失败: {err}") if conn and conn.is_connected(): conn.rollback() finally: if cursor: cursor.close() if conn and conn.is_connected(): conn.close() return last_id关键点:cursor.lastrowid 在 cursor.execute() 之后即可读取,不必等 commit。但如果你在 commit 之前又执行了另一条 INSERT,lastrowid 会被覆盖成后一条的 ID。所以拿到 ID 后立刻存变量,别拖。
4.3 用 SELECT LAST_INSERT_ID() 作为对照
cursor.execute("INSERT INTO users (username, email) VALUES (%s, %s)", (username, email)) conn.commit() cursor.execute("SELECT LAST_INSERT_ID()") last_id = cursor.fetchone()[0]LAST_INSERT_ID() 是连接级(session 级)的,不同连接互不干扰,所以并发下也是安全的。但它多一次往返查询,且必须在同一个连接里紧接着执行。cursor.lastrowid 本质上是驱动帮你把这次查询的结果缓存下来了,所以更省事。
4.4 executemany 的差异
这是最容易踩坑的地方。批量插入时:
rows = [("u1", "u1@example.com"), ("u2", "u2@example.com"), ("u3", "u3@example.com")] cursor.executemany("INSERT INTO users (username, email) VALUES (%s, %s)", rows) conn.commit() print(cursor.lastrowid) # 通常只返回第一行的 IDmysql-connector-python 在 executemany 后,lastrowid 一般只反映第一行。如果你需要全部 ID,有几种策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 逐行插入 | 循环 execute,每次取 lastrowid | 行数少,需要精确 ID |
| 应用层生成主键 | 用 UUID 替代自增 | 分布式、需要预知 ID |
| 插入后按条件查回 | 用唯一字段 SELECT 回查 | 有业务唯一键 |
| 依赖连续自增 | 取首 ID 后按行数推算 | 单连接、无并发、innodb_autoinc_lock_mode=0/1 |
最后一种最脆弱,不推荐。生产环境优先用「应用层生成主键」或「逐行插入」。
5. 验证请求:插入后 ID 校验的完整动作
拿到 ID 不代表对。写一个校验函数,用 ID 回查一次,确认数据真的落库且字段匹配:
def verify_user_by_id(db_config: dict, user_id: int): with mysql.connector.connect(**db_config) as conn: with conn.cursor(dictionary=True) as cursor: cursor.execute( "SELECT id, username, email, registered_at FROM users WHERE id = %s", (user_id,) ) row = cursor.fetchone() if row: print(f"校验通过: {row}") else: print(f"校验失败: 未找到 id={user_id}") return row跑一遍完整流程:
if __name__ == "__main__": new_id = insert_user_and_get_id(DB_CONFIG, "alice", "alice@example.com") if new_id: verify_user_by_id(DB_CONFIG, new_id)预期输出类似:
插入成功,username=alice, id=1 校验通过: {'id': 1, 'username': 'alice', 'email': 'alice@example.com', 'registered_at': datetime.datetime(...)}如果校验返回 None,说明 ID 拿到了但数据没落库,八成是 commit 没执行或事务被回滚。
6. 本篇常见错排查
6.1 lastrowid 返回 None
最常见原因:表没有 AUTO_INCREMENT 主键,或者你手动给自增列传了值。检查表结构:
SHOW CREATE TABLE users;确认 id 列带 AUTO_INCREMENT。另外,如果 INSERT 因为唯一键冲突失败,lastrowid 也可能是 None 或旧值,务必先判断 execute 是否抛异常。
6.2 executemany 后只拿到一个 ID
这是驱动行为,不是 bug。需要全部 ID 就改逐行插入,或者改用 UUID 主键。别试图用 lastrowid 加行数推算,并发下必错。
6.3 多线程下 ID 串了
只要每个线程用独立连接,lastrowid 就是连接隔离的,不会串。串的原因通常是多个线程共用一个 connection 或 cursor。连接池场景下,确保从池里借出的连接在同一线程内用完即还。
6.4 PyMySQL 的差异
PyMySQL 同样支持 cursor.lastrowid,行为一致。但 PyMySQL 默认 autocommit=False,忘记 commit 会导致数据不落库,回查时找不到。另外 PyMySQL 连接参数用 charset='utf8mb4',别写成 utf-8。
6.5 时区与 DATETIME 对不上
registered_at 用 DEFAULT CURRENT_TIMESTAMP 时,取回的时间取决于 MySQL 服务器时区。如果 Python 侧显示差 8 小时,在连接参数里加 time_zone='+08:00',或在 MySQL 配置里统一时区。
6.6 连接超时导致插入中断
长事务或网络抖动会让连接断掉,此时 commit 抛异常,lastrowid 不可信。给连接加 connection_timeout,并在 except 里显式 rollback。重试逻辑要放在应用层,别在驱动层硬扛。
7. 把模型调用也接进同一套配置
当你的脚本需要在插入前用大模型生成 username 或 email 模板时,可以直接读同一份配置里的 TaoToken 段:
import json, urllib.request def call_model(prompt: str, cfg: dict) -> str: req = urllib.request.Request( f"{cfg['base_url']}/v1/messages", data=json.dumps({ "model": cfg["default_model"], "max_tokens": 256, "messages": [{"role": "user", "content": prompt}] }).encode(), headers={ "Content-Type": "application/json", "x-api-key": cfg["api_key"], "anthropic-version": "2023-06-01" }, method="POST" ) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read())["content"][0]["text"]这样数据库凭据和模型凭据都在 settings.json 里,换环境只改一份文件。想先验证模型是否通,可以去模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 手动发一条消息确认 Key 有效。如果是要长期跑编码任务或 Agent 流程,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 会更合适。接入细节和参数说明在文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里,Claude Code 相关配置参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
8. 收尾:几个我实际用下来的习惯
第一,lastrowid 拿到后立刻赋值给局部变量,别在中间插任何其他 INSERT。第二,executemany 场景一律不依赖 lastrowid,要么逐行,要么 UUID。第三,校验动作别省,尤其是跨服务写库时,回查一次能挡掉大部分「以为成功其实回滚」的问题。第四,配置文件里的敏感字段用环境变量覆盖,settings.json 只留占位符。第五,连接用完即关,with 语句能省掉一堆 finally 样板代码。
把这些串起来,Python 与 MySQL 的自增 ID 获取链路就稳了。