跳到主要内容

3.1 增:插入数据

CRUD 的第一课。本节学会:单条插入、批量插入、拿回自增主键、处理唯一约束冲突。

本章公共代码

第 3 章所有示例基于同一个模型,先建好 models.py

# models.py
from datetime import datetime
from typing import Optional

from sqlalchemy import String, create_engine, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker


class Base(DeclarativeBase):
pass


class User(Base):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
email: Mapped[str] = mapped_column(String(120), unique=True)
age: Mapped[Optional[int]]
city: Mapped[Optional[str]] = mapped_column(String(50))
created_at: Mapped[datetime] = mapped_column(server_default=func.now())

def __repr__(self) -> str:
return f"User(id={self.id}, name={self.name!r}, age={self.age}, city={self.city!r})"


engine = create_engine("sqlite:///crud.db", echo=True)
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(engine)

之后的示例文件统一这样开头:

from models import User, SessionLocal

一、插入一条数据:三步走

from models import User, SessionLocal

with SessionLocal() as session:
# ① 创建对象
user = User(name="张三", email="zhangsan@example.com", age=25, city="北京")
# ② 加入会话
session.add(user)
# ③ 提交
session.commit()

print(f"插入成功,分配到的 id = {user.id}")

对应生成的 SQL(echo 日志里能看到):

INSERT INTO users (name, email, age, city) VALUES (?, ?, ?, ?)

commit 之后发生了什么?

  • 数据真正写入数据库
  • user.id 自动被填上数据库分配的自增主键——不需要你做任何事
  • created_at 这类 server_default 字段也由数据库填好了

💡 如果 commit 后访问 user.created_at,SQLAlchemy 会自动发一条 SELECT 把数据库生成的值取回来(因为 commit 后对象会"过期",首次访问属性时自动刷新)。

二、批量插入:add_all

with SessionLocal() as session:
users = [
User(name="李四", email="lisi@example.com", age=30, city="上海"),
User(name="王五", email="wangwu@example.com", age=17, city="北京"),
User(name="赵六", email="zhaoliu@example.com", age=42, city="深圳"),
User(name="孙七", email="sunqi@example.com", age=28, city="上海"),
]
session.add_all(users)
session.commit()

一次 commit 提交多条是常规做法:所有 INSERT 在同一个事务里,要么全部成功、要么全部失败,而且比逐条 commit 快得多。

# ❌ 慢且不原子:每条一个事务
for u in users:
session.add(u)
session.commit()

# ✅ 快且原子:一个事务搞定
session.add_all(users)
session.commit()

三、超大批量:bulk insert

要灌入几万条数据(导数据、造测试数据)时,走 ORM 对象会比较慢。用 Core 风格的 insert() 直接批量执行:

from sqlalchemy import insert
from models import User, SessionLocal

data = [
{"name": f"用户{i}", "email": f"user{i}@example.com", "age": 20 + i % 30}
for i in range(10000)
]

with SessionLocal() as session:
session.execute(insert(User), data) # 一次性批量 INSERT
session.commit()
方式特点适用
add_all + ORM 对象走完整 ORM 流程(默认值、事件、拿回主键)日常业务,几十几百条
session.execute(insert(User), 字典列表)跳过对象管理,速度快数倍数据导入,上万条

四、处理唯一约束冲突

email 列有 unique=True,插入重复邮箱会抛异常:

from sqlalchemy.exc import IntegrityError
from models import User, SessionLocal

with SessionLocal() as session:
user = User(name="张三二号", email="zhangsan@example.com") # 邮箱已存在!
session.add(user)
try:
session.commit()
except IntegrityError:
session.rollback() # 必须回滚,否则这个 session 无法继续使用
print("邮箱已被注册!")

两个要点:

  1. 捕获的是 IntegrityError(完整性错误:唯一约束、非空约束、外键约束违反都抛它)
  2. except 里必须 rollback()——commit 失败后 Session 处于"故障"状态,不回滚的话后续任何操作都会报 PendingRollbackError

更友好的做法:先查后插

from sqlalchemy import select

with SessionLocal() as session:
email = "zhangsan@example.com"
exists = session.scalar(select(User).where(User.email == email))
if exists:
print("邮箱已被注册")
else:
session.add(User(name="张三", email=email))
session.commit()

📌 注意"先查后插"在高并发下仍有极小概率撞车(两个请求同时通过了检查),所以唯一约束 + 捕获 IntegrityError 是最后防线,两者都要有

五、插入时的默认值回顾

user = User(name="张三", email="zs@example.com")
# age、city 没传 → NULL(因为是 Optional)
# created_at 没传 → 数据库自动填当前时间(server_default)
# id 没传 → 数据库自增分配

不要手动给 id 赋值,交给数据库自增管理。

六、常见疑问

Q:add 之后、commit 之前,能查到这条数据吗?同一个 session 里可以(SQLAlchemy 查询前会自动 flush 待办的 INSERT);其他 session / 其他程序看不到,因为事务还没提交。

Q:怎么在 commit 前就拿到自增 id? session.flush()——SQL 发出去了(id 已分配),但事务未提交,还能反悔。

Q:忘了 commit 会怎样? with 块结束时 session 关闭,未提交的改动全部丢弃。新手"为什么数据没存进去"的第一大原因就是忘了 commit。

📝 本节小结

  • 插入三步:User(...)session.add()session.commit()
  • commit 后自增 id 自动回填到对象上
  • 多条数据放同一个事务里一次 commit:更快、更安全
  • 超大批量用 session.execute(insert(User), 字典列表)
  • 唯一约束冲突抛 IntegrityError,捕获后必须 rollback

下一节,学 CRUD 里最丰富的"查" → 3.2 查询数据