3.3 定义数据模型
本章目标:用 SQLAlchemy 2.0 语法把「表结构」定义成 Python 类,并在数据库中创建出真实的表。
1. 声明基类:DeclarativeBase
所有模型类都要继承同一个「声明基类」。SQLAlchemy 2.0 的写法:
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
Base 会登记所有继承它的模型类,稍后建表就靠这份登记信息。
2. 定义第一个模型
from datetime import datetime
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(100), unique=True)
age: Mapped[int | None] # 可空字段,简单类型可省略 mapped_column
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
def __repr__(self):
return f"<User id={self.id} username={self.username}>"
逐个拆解:
__tablename__
这个类对应数据库里哪张表。惯例:类名单数大写(User),表名复数小写(users)。
Mapped[类型] 和 mapped_column()
Mapped[int]:声明字段的 Python 类型。SQLAlchemy 自动推断数据库类型(int → INTEGER,str → VARCHAR,datetime → DATETIME...)Mapped[int | None]:类型带| None表示该列允许 NULL;不带则 NOT NULL。可空性由类型注解决定,非常直观mapped_column(...):补充列的其他属性。字段没有额外属性时可以整个省略(如上面的age)
常用列属性速查
| 参数 | 含义 |
|---|---|
primary_key=True | 主键。整数主键默认自增 |
unique=True | 唯一约束(用户名、邮箱不许重复) |
index=True | 建索引,加速按该列的查询 |
default=值 | Python 层默认值(插入时由程序填) |
server_default=func.now() | 数据库层默认值(建表语句里的 DEFAULT,这里是自动填入当前时间) |
String(50) | 指定 VARCHAR 长度。MySQL 必须指定,SQLite 无所谓——为了可移植性,字符串一律写上长度 |
Text | 长文本(文章正文等),不限长度 |
nullable=False | 显式声明非空(通常用类型注解表达,不用写它) |
更多类型示例
from sqlalchemy import String, Text, Numeric
from decimal import Decimal
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
description: Mapped[str | None] = mapped_column(Text) # 长文本
price: Mapped[Decimal] = mapped_column(Numeric(10, 2)) # 金额用 Decimal,别用 float!
stock: Mapped[int] = mapped_column(default=0)
为什么金额不用 float? 浮点数有精度误差(0.1 + 0.2 != 0.3),涉及钱一律用
Numeric/Decimal。
3. 建表:create_all
模型只是「图纸」,要在数据库里真正建出表:
from sqlalchemy import create_engine
engine = create_engine("sqlite:///./app.db", echo=True)
# 根据 Base 登记的所有模型,创建对应的表(已存在的表会跳过)
Base.metadata.create_all(engine)
运行后观察 echo 输出,你会看到生成的建表 SQL:
CREATE TABLE users (
id INTEGER NOT NULL,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
age INTEGER,
is_active BOOLEAN NOT NULL,
created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
PRIMARY KEY (id),
UNIQUE (email)
)
对照你的模型定义看这段 SQL,每一行都能对应上——这是检验你是否真正理解模型定义的好方法。
create_all 的重要限制(新手必看)
create_all 只创建不存在的表,不会修改已存在的表。也就是说:
你给 User 加了一个
phone字段,再跑create_all——数据库里的 users 表不会多出 phone 列!
学习阶段的粗暴解决法:删掉 app.db 文件重新建(数据会丢)。正式项目的解决法:数据库迁移工具 Alembic(第五部分第 3 章)。
4. 查看数据库里的表
推荐装一个可视化工具,随时查看表结构和数据,学习效率翻倍:
- DB Browser for SQLite(免费):https://sqlitebrowser.org/ ,直接打开
app.db文件 - VS Code 扩展 SQLite Viewer:在编辑器里直接看
- MySQL 用户可以用 DBeaver(免费全能)或 Navicat
5. 本章完整代码
保存为 models_demo.py 并运行,确认能成功建表:
from datetime import datetime
from sqlalchemy import String, create_engine, 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(100), unique=True)
age: Mapped[int | None]
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
def __repr__(self):
return f"<User id={self.id} username={self.username}>"
engine = create_engine("sqlite:///./app.db", echo=True)
if __name__ == "__main__":
Base.metadata.create_all(engine)
print("建表完成!")
本章小结
- 继承
DeclarativeBase得到 Base,所有模型继承 Base Mapped[类型]声明字段,| None表示可空,mapped_column()补充主键/唯一/默认值等属性- 字符串列写明长度
String(50);金额用Numeric;长文本用Text Base.metadata.create_all(engine)建表,但不能改表——改表要靠 Alembic
下一章:3.4 增删改查(CRUD) —— 往表里放数据。