Skip to content

第 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),数据落库。

定义映射类(逐行讲解) ​

python
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 的类都会自动登记进去。

添加记录 ​

python
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。

关键知识点 ​

概念说明
DeclarativeBase2.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. 给字段加默认值和时间戳。

python
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 后回填到对象。