2.2 定义模型:把 Python 类变成数据表
模型(Model)是 ORM 的灵魂:一个类对应一张表,一个对象对应一行数据。本节把模型定义的每个零件拆开讲清楚。
一、最小完整示例
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
四个零件:Base 基类、__tablename__、Mapped[...] 注解、mapped_column()。逐个来看。
二、Base 基类:模型的"注册中心"
class Base(DeclarativeBase):
pass
- 所有模型类都必须继承同一个
Base Base内部维护着一份metadata(元数据),记录了所有登记过的表结构Base.metadata.create_all(engine)就是拿着这份记录去数据库建表
整个项目只定义一次 Base,通常单独放在 database.py 或 models/base.py 里,其他模型文件导入它。
📌 老教程里的
Base = declarative_base()是 1.x 写法,效果相同,但 2.0 推荐用继承DeclarativeBase的写法,类型检查器支持更好。
三、__tablename__:表名
class User(Base):
__tablename__ = "users"
必填。惯例是:类名用单数大驼峰(User),表名用复数小写下划线(users)。
| 类名 | 表名 |
|---|---|
User | users |
BlogPost | blog_posts |
OrderItem | order_items |
四、Mapped[类型]:声明列的 Python 类型
id: Mapped[int]
name: Mapped[str]
price: Mapped[float]
is_active: Mapped[bool]
created_at: Mapped[datetime]
Mapped[X] 告诉 SQLAlchemy:"这是一个映射到数据库的列,Python 类型是 X"。SQLAlchemy 会自动推导对应的数据库类型:
| Python 类型 | 数据库类型(自动推导) |
|---|---|
int | INTEGER |
str | VARCHAR |
float | FLOAT |
bool | BOOLEAN |
datetime | DATETIME |
date | DATE |
Decimal | NUMERIC |
bytes | BLOB |
可空列:Optional
数据库的列分两种:NOT NULL(必填) 和 NULL(可空)。用类型注解表达:
from typing import Optional
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] # NOT NULL:必须有值
nickname: Mapped[Optional[str]] # NULL:可以不填(值为 None)
# Python 3.10+ 也可以写成:
# nickname: Mapped[str | None]
💡 这是 2.0 语法的优雅之处:可空性直接由类型注解决定,与 Python 类型系统完全一致。IDE 也会据此提醒你"这个值可能是 None"。
五、mapped_column():补充数据库细节
当只有类型注解不够用时(要设主键、长度、默认值、唯一约束……),用 mapped_column() 补充:
from datetime import datetime
from sqlalchemy import String, Text, func
from typing import Optional
class Article(Base):
__tablename__ = "articles"
# 主键 + 自增(整数主键默认自增)
id: Mapped[int] = mapped_column(primary_key=True)
# 指定长度 + 建索引
title: Mapped[str] = mapped_column(String(200), index=True)
# 唯一约束:不允许两篇文章 slug 相同
slug: Mapped[str] = mapped_column(String(200), unique=True)
# 长文本(不限长度)
content: Mapped[str] = mapped_column(Text)
# Python 侧默认值:不传时自动为 0
views: Mapped[int] = mapped_column(default=0)
# 数据库侧默认值:由数据库在 INSERT 时填入当前时间
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
# 可空 + 默认 None
summary: Mapped[Optional[str]] = mapped_column(String(500), default=None)
常用参数速查
| 参数 | 作用 | 示例 |
|---|---|---|
primary_key=True | 设为主键 | mapped_column(primary_key=True) |
String(50) 等类型对象 | 显式指定数据库类型/长度 | mapped_column(String(50)) |
unique=True | 唯一约束 | 邮箱、用户名 |
index=True | 建索引,加速按该列的查询 | 经常被 where 的列 |
nullable= | 显式指定可空性(一般用 Optional 注解代替) | nullable=False |
default= | Python 侧默认值(可以是值或函数) | default=0、default=datetime.now |
server_default= | 数据库侧默认值(写进建表 SQL) | server_default=func.now() |
comment= | 列注释 | comment="用户昵称" |
default vs server_default 的区别
default=0 | server_default=text("0") | |
|---|---|---|
| 谁来填值 | SQLAlchemy 在 INSERT 前填 | 数据库自己填 |
| 建表 SQL 里有吗 | 没有 | 有(DEFAULT 0) |
| 别人绕过 ORM 直接插数据 | 不生效 | 生效 |
新手记结论:普通默认值用 default,"创建时间"这类推荐用 server_default=func.now()。
六、一个"标准配置"的用户模型
把上面所学组合起来,这是实际项目里常见的完整模型:
from datetime import datetime
from typing import Optional
from sqlalchemy import String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
email: Mapped[str] = mapped_column(String(120), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(128))
nickname: Mapped[Optional[str]] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(),
onupdate=func.now(), # 每次 UPDATE 时自动刷新
)
def __repr__(self) -> str:
return f"User(id={self.id}, username={self.username!r})"
💡
__repr__强烈建议每个模型都写:调试时print(user)能看到关键信息,而不是<User object at 0x000001A2B3C4D5E0>。
七、创建对象 ≠ 写入数据库
定义好模型后,实例化就像普通类:
user = User(username="alice", email="alice@example.com", hashed_password="xxx")
print(user.username) # alice
print(user.id) # None ← 注意!还没进数据库,没有 id
此时 user 只是内存里的普通 Python 对象。要写入数据库,需要下一节的主角——Session。
八、建表与删表
# 建表:扫描所有继承 Base 的模型,执行 CREATE TABLE(已存在则跳过)
Base.metadata.create_all(engine)
# 删表:删除所有模型对应的表(数据全没!仅限开发环境)
Base.metadata.drop_all(engine)
⚠️
create_all的重要限制:它只会"创建不存在的表",不会修改已存在的表。你给模型加了一个字段,再跑create_all是没有任何效果的!
- 开发初期的粗暴解决法:
drop_all再create_all(数据清空重来)- 正规解决法:用 Alembic 做数据库迁移 → 5.3 节
📝 本节小结
- 模型四要素:继承
Base、__tablename__、Mapped[类型]、mapped_column(细节) Mapped[str]= 必填列,Mapped[Optional[str]]= 可空列- 常用列参数:
primary_key、unique、index、default、server_default create_all只建新表,不改旧表——改表结构要靠 Alembic- 每个模型写
__repr__,调试幸福感翻倍
下一节,学习操作数据的总入口 → 2.3 Session:与数据库对话的窗口