跳到主要内容

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) —— 往表里放数据。