第 8 章 映射类(Model)的定义与记录添加
从一个生活场景说起
前 7 章我们用的是 Core 风格:表是 Table 对象,查出来的是一行行"裸数据"(Row)。这有点像拿 Excel 表格管理员工——能干活,但每条记录只是一堆格子,没有"人"的感觉。
ORM 的想法是:把每一行数据变成一个活生生的 Python 对象。
- 表 → 一个 Python 类(class);
- 表中的一行 → 类的一个实例(对象);
- 表中的一列 → 对象的一个属性。
这就像把 Excel 表格升级成了一套员工档案系统:每条记录不再是格子,而是一张有姓名、有年龄、能直接拿在手上操作的"档案卡"。你要查张三?直接拿到"张三"这张卡,看 zhangsan.name、改 zhangsan.age 就行,SQL 由 SQLAlchemy 在幕后替你写好。
术语解释 —— ORM(Object-Relational Mapping,对象关系映射):"对象"指 Python 对象,"关系"指关系型数据库的表,"映射"就是两者之间的一一对应。ORM 让你操作对象,SQL 自动生成。
核心概念
SQLAlchemy 2.0 的新式写法
2.0 推荐使用 Declarative + Mapped[] 类型标注:
DeclarativeBase:所有模型类的"祖师爷"基类,继承了它就具备了映射能力;Mapped[int]:Python 层面的类型标注,告诉 IDE 和类型检查器"这个属性是 int";mapped_column(...):数据库层面的列定义,指定 SQL 类型、主键、约束。
两者分工:Mapped[] 管 Python,mapped_column() 管数据库。
Session:ORM 世界的"办事窗口"
Core 风格用 engine.connect() 直接和数据库对话;ORM 风格则通过 Session(会话)——它是对象与数据库之间的"办事窗口":你把对象交给它(add),它帮你翻译成 INSERT;你确认提交(commit),数据落库。
定义映射类(逐行讲解)
from sqlalchemy import create_engine, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
"""所有模型类的基类:继承它,普通类就变成"映射类" """
class User(Base):
__tablename__ = "user" # 对应数据库里的表名
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
age: Mapped[int | None] # int | None 表示"可以是整数也可以是空"
def __repr__(self):
return f"<User id={self.id} name={self.name}>"
engine = create_engine(
"mysql+pymysql://root:123456@localhost:3306/demo_db?charset=utf8mb4",
echo=True,
)
Base.metadata.create_all(engine) # 通过基类的 metadata 建表几个细节:
__tablename__:告诉 SQLAlchemy 这个类对应数据库里的哪张表;Mapped[int | None]:竖线表示"或",即该属性允许为None,映射到数据库就是"允许 NULL";只写Mapped[int]则默认非空;__repr__:不是必须的,但有了它print(user)会打印出可读的信息而不是<...object at 0x...>,调试体验天差地别;Base.metadata:基类自带了第 2 章学过的 MetaData"档案柜",所有继承 Base 的类都会自动登记进去。
添加记录
from sqlalchemy.orm import Session
with Session(engine) as session:
user = User(name="张三", age=20) # ① 创建一个普通 Python 对象(此时与数据库无关)
session.add(user) # ② 交给 Session,进入"待保存"状态
session.commit() # ③ 提交:真正执行 INSERT
print(user.id) # ④ 提交后自动生成的主键已回填到对象上
# 批量添加
with Session(engine) as session:
session.add_all([
User(name="李四", age=22),
User(name="王五", age=25),
])
session.commit()体会这个流程的精妙之处:你全程没写一句 SQL。commit() 时 SQLAlchemy 发现"这个 User 对象是新的",自动生成 INSERT INTO user (name, age) VALUES (...) 并执行,然后把 MySQL 分配的自增 id 填回 user.id。
关键知识点
| 概念 | 说明 |
|---|---|
DeclarativeBase | 2.0 的模型基类 |
Mapped[int] | 类型标注,声明该属性映射到表的列 |
mapped_column() | 定义列的细节(类型、主键、约束) |
Session.add() | 把对象标记为待插入 |
session.commit() | 提交事务,持久化所有变更 |
深入理解:id 是什么时候产生的?
一个常见困惑:User(name="张三") 创建后立刻打印 user.id,结果是 None。
因为主键是 MySQL 在 INSERT 时才分配的。对象的 id 要经历:创建(None)→ add(还是 None)→ INSERT 执行(MySQL 分配 id)→ SQLAlchemy 回填到对象(有值了)。commit() 内部会先执行 INSERT,所以 commit 之后 user.id 必然可用。
进阶内容
1. 双写法的配合。 Mapped[str] 和 mapped_column(String(50)) 看似重复,实则各司其职:
String(50):告诉 MySQL 建VARCHAR(50)列;Mapped[str]:告诉 IDE/mypy 这个属性是字符串,写代码时有自动补全和类型检查。
2. 给字段加默认值和时间戳。
from datetime import datetime
from sqlalchemy import func
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50))
created_at: Mapped[datetime] = mapped_column(server_default=func.now())server_default=func.now() 让 MySQL 在插入时自动填当前时间(DEFAULT CURRENT_TIMESTAMP)。
3. Core 与 ORM 的关系。 Core(第 1~7 章)和 ORM 不是两个割裂的世界:映射类的底层仍然是一张 Table(可以通过 User.__table__ 访问),ORM 语句的底层仍然是 Core 的 select()。理解了 Core,ORM 就没有黑魔法。
注意事项
Mapped[str]与mapped_column(String(50))一起使用时,String(50)控制数据库层,Mapped[str]控制 Python 层类型检查。MySQL 中字符串必须指定长度。- 对象在
commit()之前没有主键 id。 - MySQL 的自增主键在 INSERT 执行后即可获得,commit 后通过
user.id访问。 - 插入中文报错
Incorrect string value时,检查数据库/表/连接串是否都是utf8mb4。 - 所有映射类必须继承同一个
Base,否则Base.metadata.create_all()找不到漏网的类,表就不会被创建。
本章小结
- ORM = 类对应表、对象对应行、属性对应列;
- 2.0 新风格:继承
DeclarativeBase,用Mapped[]+mapped_column()声明字段; - 添加记录三步走:建对象 →
session.add()→session.commit(); - 主键 id 由 MySQL 在 INSERT 时分配,commit 后回填到对象。