SQLAlchemy模型设计中的一些核心概念。这种设计(继承 MappedAsDataclass和 DeclarativeBase)是SQLAlchemy现代声明式模型的一种强大且类型安全的写法。
下面是一个例子:
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) -------------------------------------------------------------------------- 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 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) password_salt: Mapped[str | None] = mapped_column(String(255), default=None) avatar: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) interface_language: Mapped[str | None] = mapped_column(String(255), default=None) interface_theme: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) timezone: Mapped[str | None] = mapped_column(String(255), default=None) last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) last_login_ip: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) last_active_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.current_timestamp(), nullable=False, init=False ) status: Mapped[str] = mapped_column( String(16), server_default=sa.text("'active'::character varying"), default="active" ) initialized_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.current_timestamp(), nullable=False, init=False ) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.current_timestamp(), nullable=False, init=False, onupdate=func.current_timestamp() ) role: TenantAccountRole | None = field(default=None, init=False) _current_tenant: "Tenant | None" = field(default=None, init=False)
为什么是 MappedAsDataclass和 DeclarativeBase?
这个 TypeBase类被设计为项目中所有数据模型(如 Account)的总基类。它结合了两个父类的功能,为子类提供了强大的能力:
基类 |
核心作用 |
带来的好处 |
|
数据库映射:这是SQLAlchemy声明式系统的核心。它负责将Python类与数据库表关联起来(通过 |
提供了ORM功能,让你能用Python对象操作数据库表。 |
|
数据类行为:这是一个混入类(Mixin),它让模型类具备类似Python |
简化了对象初始化,支持更清晰、更现代的类型注解(如 |
简单来说,DeclarativeBase负责“数据库的事”,而 MappedAsDataclass负责“Python对象的事”。两者结合,创造出了一个既强大又易用的模型基类。
DeclarativeBase(SQLAlchemy 2.0)
作用:
- 提供声明式 ORM 基类
- 管理表结构、元数据、注册机制
- 支持 Mapped 类型注解
# 1. 表注册和元数据管理 __tablename__ = "accounts" # 定义表名 metadata = metadata # 使用统一的元数据 # 2. ORM 查询能力 account = db.session.query(Account).filter_by(email="test@example.com").first() # 3. 关系映射 # 可以定义 relationships, backrefs 等 # 4. 类型注解支持 id: Mapped[str] # SQLAlchemy 2.0 的类型注解
如果不继承 DeclarativeBase:
无法使用声明式 ORM 需要手动创建 Table 对象
无法使用 Mapped 类型注解
失去 ORM 的便利性
MappedAsDataclass(SQLAlchemy 2.0)
作用:
- 让模型像 Python dataclass 一样工作
- 自动生成 __init__、__repr__ 等方法
- 支持 field() 和 init=False 等 dataclass 特性
# 1. 自动生成 __init__ 方法 account = Account( name="张三", email="zhangsan@example.com", password="hashed_password" ) # 不需要手动写 __init__ # 2. 支持 dataclass 的 field() 函数 role: TenantAccountRole | None = field(default=None, init=False) # init=False 表示这个字段不会出现在 __init__ 参数中 # 3. 支持 init=False 在 mapped_column 中 id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4()), init=False) # init=False 表示创建对象时不需要传入 id,会自动生成 # 4. 自动生成 __repr__ 等方法 print(account) # 会有默认的字符串表示
🔍 Account 类如何利用父类特性
Account类继承 TypeBase后,其字段定义大量使用了由父类带来的能力:
1. 类型注解与列映射
name: Mapped[str] = mapped_column(String(255))
Mapped[str]:这是由DeclarativeBase系统识别的类型注解,指明name属性是映射到数据库的列,且类型为str。mapped_column(String(255)):mapped_column是 SQLAlchemy 2.0 的现代写法,替代了传统的Column,用于定义列的细节(如数据类型String(255))。
2. 数据类行为控制
init=False:这是MappedAsDataclass提供的功能。表示该字段不包含在自动生成的__init__构造函数参数中。例如id和created_at通常由数据库自动生成,所以创建Account对象时不需要传入。default=None:这也是数据类特性。为字段设置默认值。创建Account对象时,如果没提供password的值,它就会是None。
3. 数据库端默认值
last_active_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), init=False)
server_default:由DeclarativeBase系统处理,指定在数据库层面的默认值(如func.current_timestamp())。这与 Python 层面的default是不同的。
⚖️ 对比:不继承父类的传统写法
如果不使用 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:作为具体的 “产品”(用户账户模型),使用统一的“零件”是天经地义、也是最可靠的选择。