bigywater.tech
Flask · 84

Model 与 Table 映射

写一个 Model 类,数据库里到底会生成什么?default 与 server_default 有什么不同?没写主键会怎样?已有的表怎么映射?这篇用真实的 PostgreSQL 逐项对照 SQLAlchemy 生成的 DDL 与数据库里实际的结构。

server_default
才会进入 DDL;Python 端的 default 绕过 ORM 就不生效
定义类时
没有主键的模型在类定义那一刻就报 ArgumentError
Mapped[Optional[...]]
2.0 风格注解下 Optional 才是可空,旧式 Column 默认可空

表名怎么来

实测 · 本机执行输出
   {'UserProfile': 'user_profile', 'HTTPRequestLog': 'http_request_log', 'Custom': 'my_custom'}

上一篇:Flask 请求与数据库事务边界;下一篇:Lazy Loading 与 Eager Loading。

没有主键

实测 · 本机执行输出
  没有主键的模型 → ArgumentError: Mapper Mapper[NoPk(nopk)] could not assemble any primary key columns for mapped table 'nopk'

类型与 DDL

实测 · 本机执行输出
  SQLAlchemy 生成的 CREATE TABLE:
    CREATE TABLE product (
    	id SERIAL NOT NULL, 
    	name VARCHAR(30) NOT NULL, 
    	note TEXT, 
    	price NUMERIC(8, 2), 
    	active BOOLEAN DEFAULT true, 
    	created TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), 
    	meta JSON, 
    	kind kind, 
    	stock INTEGER NOT NULL CHECK (stock >= 0), 
    	PRIMARY KEY (id), 
    	UNIQUE (name)
    )
  PG 里实际的列:
    id integer null=NO default=nextval('product_id_seq'::regclass)
    name character varying null=NO default=-
    note text null=YES default=-
    price numeric null=YES default=-
    active boolean null=YES default=true
    created timestamp without time zone null=YES default=now()
    meta json null=YES default=-
    kind USER-DEFINED null=YES default=-
    stock integer null=NO default=-
  PG 里的索引: ix_product_name_active, product_name_key, product_pkey

default 与 server_default

实测 · 本机执行输出
  ORM 发出的 INSERT(只显示列名部分): INSERT INTO product (name, note, price, active, created, kind, stock) VALUES (…)
  结果行: p1|0.00|t|t|0
raw||t|t|1
  直接用 SQL 插入(绕过 ORM)时,只有 server_default 起作用: raw||t|t|1

约束

实测 · 本机执行输出
  stock=-1(CheckConstraint)  → IntegrityError / 底层 CheckViolation
  重名 p1(unique)              → IntegrityError / 底层 UniqueViolation

Mapped[] 注解

实测 · 本机执行输出
   CREATE TABLE acct (
   	id SERIAL NOT NULL, 
   	name VARCHAR(30) NOT NULL, 
   	nick VARCHAR, 
   	age INTEGER NOT NULL, 
   	PRIMARY KEY (id)
   )

模型类上的东西

实测 · 本机执行输出
  Product.name 的类型: InstrumentedAttribute | Product.__table__.name: product | 列: ['id', 'name', 'note', 'price', 'active'] ...
  mapper 的映射属性数: 9 | 主键: ['id']
  实例的 p.name: tmp | 没赋值的 p.note: None | 没赋值的 p.price(default 还没生效): None

反射已有的表

实测 · 本机执行输出
  反射出的表 legacy 的列: [('id', 'INTEGER', False), ('title', 'TEXT', False), ('score', 'INTEGER', True)]
  db.reflect() 是不是也把 product 反射进来: True

分步演示

Model 与 Table 映射
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)
    • 没有验证:SQLite 与 MySQL 上同样模型生成的 DDL、继承映射(单表、联表)、复合主键与复合外键、列的 onupdate、混入类(mixin)与 __abstract__、Flask-SQLAlchemy 的 db.Table 与 BINDS 一起使用
    京ICP备2026067502号-1京公网安备11010502063487号