SQLAlchemy模型设计中的一些核心概念。这种设计(继承 MappedAsDataclass和 DeclarativeBase)是SQLAlchemy现代声明式模型的一种强大且类型安全的写法。
场景:
比如我们在flask中想要写一个Base 类去继承 MappedAsDataclass, DeclarativeBase,然后让其它的类去继承Base类:
下面是基础类的定义
from sqlalchemy.orm import DeclarativeBase,MappedAsDataclass class TypeBase(MappedAsDataclass, DeclarativeBase): """ This is for adding type, after all finished, rename to Base. """ pass
为什么继承 MappedAsDataclass和 DeclarativeBase?
这个 TypeBase类被设计为项目中所有数据模型(如 Account)的总基类。它结合了两个父类的功能,为子类提供了强大的能力:
基类 |
核心作用 |
带来的好处 |
|
数据库映射:这是SQLAlchemy声明式系统的核心。它负责将Python类与数据库表关联起来(通过 |
提供了ORM功能,让你能用Python对象操作数据库表。 |
|
数据类行为:这是一个混入类(Mixin),它让模型类具备类似Python |
简化了对象初始化,支持更清晰、更现代的类型注解(如 |
简单来说,DeclarativeBase负责“数据库的事”,而 MappedAsDataclass负责“Python对象的事”。两者结合,创造出了一个既强大又易用的模型基类。
关系总结
| 组件 | 作用 | 与 mapped_column 的关系 |
| DeclarativeBase | 提供 ORM 基础设施 | 扫描和处理 mapped_column 返回的对象 |
| MappedAsDataclass | 提供 dataclass 功能 | 与 mapped_column 的 init 参数配合 |
| mapped_column | 创建列定义对象 | 返回 MappedColumn,被 DeclarativeBase 处理 |
| Registry | 管理所有映射类 | 存储 Account 的映射信息 |
| MetaData | 管理数据库模式 | 存储 Account 的表定义 |
继承DeclarativeBase 的完整调用解析
执行 Python 文件(account.py)在下面的举例中有提到Account 类的定义继承了TypeBase类 ↓ from models.base import TypeBase ↓ 1. 加载 models/base.py 模块 ↓ 2. 执行 base.py 顶层代码(按顺序) ↓ a. from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, ... ↓ b. 加载 sqlalchemy.orm 模块 ↓ ┌───────────────────────────────────── │ 在 sqlalchemy.orm 模块中: │ │ 1. 定义元类 DeclarativeAttributeIntercept │ │ 2. 定义 DeclarativeBase 类 │ class DeclarativeBase( │ metaclass=DeclarativeAttributeIntercept │ ): │ # 类体执行 │ # __init_subclass__ 被定义 │ │ 3. 定义 MappedAsDataclass 类 │ │ 4. 定义其他类(Mapped, mapped_column等) └───────────────────────────────────── ↓ c. 回到 base.py 继续执行 metadata = sa.MetaData() ↓ d. 定义 TypeBase 类 class TypeBase(MappedAsDataclass, DeclarativeBase): metadata = metadata ↓ 执行 TypeBase 类体:metadata = metadata ↓ 元类处理 TypeBase 类(DeclarativeAttributeIntercept) ↓ 调用 __init_subclass__ 链 ↓ MappedAsDataclass.__init_subclass__ ↓ DeclarativeBase.__init_subclass__ (TypeBase 直接继承,执行 _setup_declarative_base) ↓ TypeBase 类定义完成,拥有 registry 和 metadata ↓ 3. TypeBase 导入完成,回到 account.py ↓ 4. 继续执行 account.py 的其他导入 from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import String ↓ 5. 定义 Account 类 class Account(UserMixin, TypeBase): __tablename__ = "accounts" id: Mapped[str] = mapped_column(StringUUID, ...) name: Mapped[str] = mapped_column(String(255)) email: Mapped[str] = mapped_column(String(255)) password: Mapped[str | None] = mapped_column(...) password_salt: Mapped[str | None] = mapped_column(...) avatar: Mapped[str] = mapped_column(...) ↓ 6. 执行 Account 类体(按顺序执行字段定义) ↓ 7. 元类处理 Account 类(同样使用 DeclarativeAttributeIntercept) ↓ 8. 调用 __init_subclass__ 链 ↓ MappedAsDataclass.__init_subclass__(Account) ↓ DeclarativeBase.__init_subclass__(Account) (Account 间接继承,执行 _as_declarative) ↓ 扫描 Account.__dict__,创建 Table 和 Mapper ↓ Account 类定义完成,拥有 __table__ 和 __mapper__ ↓ 9. Account 可以使用 ORM 功能
🔍 Account 类继承详细解释
下面是在Flask框架中使用Sqlalchemy定义的Account 类.
#engine.py from flask_sqlalchemy import SQLAlchemy from sqlalchemy import MetaData POSTGRES_INDEXES_NAMING_CONVENTION:dict[str,str] = { "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", "ck": "ck_%(table_name)s_%(column_0_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s", } metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION) db = SQLAlchemy(metadata=metadata) -------------------------------------------------------------------------- #base.py from sqlalchemy.orm import DeclarativeBase,MappedAsDataclass from models.engine import metadata class TypeBase(MappedAsDataclass, DeclarativeBase): """ This is for adding type, after all finished, rename to Base. """ metadata = metadata -------------------------------------------------------------------------- #Account.py class Account(UserMixin, TypeBase): __tablename__ = "accounts" __table_args__ = (sa.PrimaryKeyConstraint("id", name="account_pkey"), sa.Index("account_email_idx", "email")) id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), init=False) name: Mapped[str] = mapped_column(String(255)) email: Mapped[str] = mapped_column(String(255)) password: Mapped[str | None] = mapped_column(String(255), default=None)
⚖️ 对比:不继承父类的传统写法
如果不使用 TypeBase提供的现代组合功能,Account类的定义会变得冗长且缺乏类型提示。
下面是一个简化的对比示例,假设只定义 id, name, email三个字段:
# ✅ 现代写法(继承 TypeBase) from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column from sqlalchemy import String, func class TypeBase(MappedAsDataclass, DeclarativeBase): pass class Account(TypeBase): __tablename__ = "accounts" id: Mapped[str] = mapped_column(primary_key=True, init=False) name: Mapped[str] = mapped_column(String(255)) email: Mapped[str] = mapped_column(String(255)) # 创建对象非常简洁,且有类型检查 new_account = Account(name="张三", email="zhangsan@example.com") ---------------------------------------------------------------------------------------------- # ❌ 传统写法(不继承这些父类) from sqlalchemy import Column, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker # 1. 需要手动创建基类 Base = declarative_base() # 2. 类定义无法使用 Mapped 类型注解,列定义用 Column class Account(Base): __tablename__ = "accounts" # 每个字段都需要用 Column 详细定义,代码冗长 id = Column(String(255), primary_key=True) name = Column(String(255)) email = Column(String(255)) # 3. 如果想有初始化逻辑,需手动写 __init__ def __init__(self, name, email): self.name = name self.email = email # 4. 没有方便的默认 __repr__ 等方法 # 创建对象 new_account = Account(name="张三", email="zhangsan@example.com")
💡 不继承 DeclarativeBase
如果不继承 DeclarativeBase,你将无法使用SQLAlchemy的声明式模型(即通过类定义表结构这种最常用的方式),而必须回归到更底层的命令式映射(Imperative Mapping 或 Classical Mapping)。
这意味着你需要:
- 先手动定义一个
Table对象来描述表结构。 - 再定义一个普通的Python类。
- 最后使用
mapper()函数手动地将两者关联起来
# ✅ 现代写法:继承 DeclarativeBase from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass class User(Base): # 继承DeclarativeBase __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column() -------------------------------------------------------------------------------------- # ❌ 传统写法:不继承 DeclarativeBase (命令式映射) from sqlalchemy import Table, Column, Integer, String, MetaData from sqlalchemy.orm import registry # 1. 手动创建注册表 mapper_registry = registry() metadata = MetaData() # 2. 先像定义普通SQL表一样,用Table构造表 user_table = Table( "users", metadata, Column("id", Integer, primary_key=True), Column("name", String(30)), ) # 3. 定义一个普通的Python类 class User: def __init__(self, name: str): self.name = name # 4. 使用注册表的 map_imperatively 方法手动将类和表关联起来 mapper_registry.map_imperatively(User, user_table)
主要影响:代码变得冗长且不直观,失去了声明式模型的简洁性和可读性。声明式模型将所有信息集中在类定义中,一目了然
💡 不继承 MappedAsDataclass
MappedAsDataclass是一个混入类,它为你提供了类似Python dataclass的便利特性。如果不继承它,你将无法使用SQLAlchemy 2.0风格的现代类型注解,也无法自动获得一些便利方法。
# ✅ 现代写法:继承 MappedAsDataclass from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column class Base(MappedAsDataclass, DeclarativeBase): pass class User(Base): # 继承的Base包含了MappedAsDataclass __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True, init=False) # 使用Mapped注解,init=False控制构造函数 name: Mapped[str] = mapped_column() email: Mapped[str | None] = mapped_column(default=None) # 支持默认值 # 创建对象非常简洁,且有类型检查和自动生成的__init__方法 new_user = User(name="张三") # 只需传name,email使用默认值None
# ❌ 传统写法:不继承 MappedAsDataclass (仅继承DeclarativeBase) from sqlalchemy import Column, Integer, String from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass class User(Base): # 仅继承DeclarativeBase __tablename__ = "users" id = Column(Integer, primary_key=True) # 不使用Mapped注解 name = Column(String(30)) email = Column(String(255), default=None) # 数据库层面的默认值 # 可能需要手动编写__init__方法以获得灵活的构造函数 def __init__(self, name: str, email: str | None = None): self.name = name self.email = email # 创建对象 new_user = User(name="李四")
主要影响:
- 失去现代类型注解:无法使用
Mapped[类型]这种清晰的注解方式,代码的类型提示作用减弱,IDE的智能提示和支持也会变差。 - 失去数据类特性:不会自动生成一个智能的
__init__构造函数。你需要手动编写__init__方法才能实现类似init=False(某些字段不放入构造函数)或灵活的默认值逻辑
总结
可以这样理解这个设计:
TypeBase:是项目自产的、功能强大的 “模型零件”,它融合了数据库映射和现代化数据类两种特性。Account:作为具体的 “产品”(用户账户模型),使用统一的“零件”是天经地义、也是最可靠的选择。