跳到主要内容

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.pymodels/base.py 里,其他模型文件导入它。

📌 老教程里的 Base = declarative_base() 是 1.x 写法,效果相同,但 2.0 推荐用继承 DeclarativeBase 的写法,类型检查器支持更好。

三、__tablename__:表名

class User(Base):
__tablename__ = "users"

必填。惯例是:类名用单数大驼峰(User),表名用复数小写下划线(users)

类名表名
Userusers
BlogPostblog_posts
OrderItemorder_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 类型数据库类型(自动推导)
intINTEGER
strVARCHAR
floatFLOAT
boolBOOLEAN
datetimeDATETIME
dateDATE
DecimalNUMERIC
bytesBLOB

可空列: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=0default=datetime.now
server_default=数据库侧默认值(写进建表 SQL)server_default=func.now()
comment=列注释comment="用户昵称"

default vs server_default 的区别

default=0server_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_allcreate_all(数据清空重来)
  • 正规解决法:用 Alembic 做数据库迁移 → 5.3 节

📝 本节小结

  • 模型四要素:继承 Base__tablename__Mapped[类型]mapped_column(细节)
  • Mapped[str] = 必填列,Mapped[Optional[str]] = 可空列
  • 常用列参数:primary_keyuniqueindexdefaultserver_default
  • create_all 只建新表,不改旧表——改表结构要靠 Alembic
  • 每个模型写 __repr__,调试幸福感翻倍

下一节,学习操作数据的总入口 → 2.3 Session:与数据库对话的窗口