SqlAlchemy 2.0 中文文档(三十一)(1)https://developer.aliyun.com/article/1562897
使用 @declared_attr 和声明性混合类
declared_attr
类允许在类级别函数中声明声明性映射的属性,并且在使用声明性混合类时特别有用。对于这些函数,函数的返回类型应使用Mapped[]
构造或指示函数返回的确切对象类型进行注释。此外,“mixin”类(即不以declarative_base()
类扩展,也不使用诸如registry.mapped()
之类的方法映射的类)应该被装饰上 declarative_mixin()
装饰器,这为 Mypy 插件提供了一个提示,指明特定的类意图充当声明性混合类:
from sqlalchemy.orm import declarative_mixin, declared_attr @declarative_mixin class HasUpdatedAt: @declared_attr def updated_at(cls) -> Column[DateTime]: # uses Column return Column(DateTime) @declarative_mixin class HasCompany: @declared_attr def company_id(cls) -> Mapped[int]: # uses Mapped return Column(ForeignKey("company.id")) @declared_attr def company(cls) -> Mapped["Company"]: return relationship("Company") class Employee(HasUpdatedAt, HasCompany, Base): __tablename__ = "employee" id = Column(Integer, primary_key=True) name = Column(String)
注意方法HasCompany.company
的实际返回类型与注释之间的不匹配。Mypy 插件将所有@declared_attr
函数转换为简单的带注释的属性,以避免这种复杂性:
# what Mypy sees class HasCompany: company_id: Mapped[int] company: Mapped["Company"]
与数据类或其他类型敏感的属性系统相结合
在 将 ORM 映射应用到现有数据类(遗留数据类用法) 中的 Python 数据类集成示例存在一个问题;Python 数据类期望明确的类型,它将用于构建类,并且每个赋值语句中给定的值都是重要的。也就是说,必须确切地声明以下类才能被数据类接受:
mapper_registry: registry = registry() @mapper_registry.mapped @dataclass class User: __table__ = Table( "user", mapper_registry.metadata, Column("id", Integer, primary_key=True), Column("name", String(50)), Column("fullname", String(50)), Column("nickname", String(12)), ) id: int = field(init=False) name: Optional[str] = None fullname: Optional[str] = None nickname: Optional[str] = None addresses: List[Address] = field(default_factory=list) __mapper_args__ = { # type: ignore "properties": {"addresses": relationship("Address")} }
我们不能将我们的Mapped[]
类型应用于属性id
、name
等,因为它们会被@dataclass
装饰器拒绝。此外,Mypy 还有另一个专门用于数据类的插件,这也可能妨碍我们的操作。
上述类实际上会顺利通过 Mypy 的类型检查;我们唯一缺少的是在User
上的属性可用于 SQL 表达式,例如:
stmt = select(User.name).where(User.id.in_([1, 2, 3]))
为了提供一个解决方法,Mypy 插件具有一个额外的功能,我们可以指定一个额外的属性 _mypy_mapped_attrs
,它是一个包含类级对象或它们的字符串名称的列表。这个属性可以在 TYPE_CHECKING
变量内部是条件性的:
@mapper_registry.mapped @dataclass class User: __table__ = Table( "user", mapper_registry.metadata, Column("id", Integer, primary_key=True), Column("name", String(50)), Column("fullname", String(50)), Column("nickname", String(12)), ) id: int = field(init=False) name: Optional[str] = None fullname: Optional[str] nickname: Optional[str] addresses: List[Address] = field(default_factory=list) if TYPE_CHECKING: _mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses] __mapper_args__ = { # type: ignore "properties": {"addresses": relationship("Address")} }
使用上述方法,列在 _mypy_mapped_attrs
中的属性将应用 Mapped
类型信息,以便在类绑定上下文中使用 User
类时,它将表现为一个 SQLAlchemy 映射类。
安装
对于 仅适用于 SQLAlchemy 2.0:不应安装存根,而应完全卸载像 sqlalchemy-stubs 和 sqlalchemy2-stubs 这样的包。
Mypy 包本身是一个依赖项。
可以使用 pip 使用 “mypy” extras 钩子安装 Mypy:
pip install sqlalchemy[mypy]
插件本身配置如 配置 mypy 使用插件 中所述,使用 sqlalchemy.ext.mypy.plugin
模块名称,例如在 setup.cfg
中:
[mypy] plugins = sqlalchemy.ext.mypy.plugin
插件的功能
Mypy 插件的主要目的是拦截和修改 SQLAlchemy 声明式映射 的静态定义,以使其与它们在被 Mapper
对象 仪器化 后的结构相匹配。这使得类结构本身以及使用类的代码对 Mypy 工具有意义,否则根据当前声明式映射的功能,这将不是情况。该插件类似于为像 dataclasses 这样的库所需的类似插件,这些插件在运行时动态地修改类。
要涵盖此类情况经常发生的主要领域,请考虑以下 ORM 映射,使用 User
类的典型示例:
from sqlalchemy import Column, Integer, String, select from sqlalchemy.orm import declarative_base # "Base" is a class that is created dynamically from the # declarative_base() function Base = declarative_base() class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) # "some_user" is an instance of the User class, which # accepts "id" and "name" kwargs based on the mapping some_user = User(id=5, name="user") # it has an attribute called .name that's a string print(f"Username: {some_user.name}") # a select() construct makes use of SQL expressions derived from the # User class itself select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
上面,Mypy 扩展可以执行的步骤包括:
- 对由
declarative_base()
生成的Base
动态类进行解释,以便继承它的类被知道是已映射的。它还可以适应 使用装饰器进行声明式映射(无声明式基类) 中描述的类装饰器方法。 - 对于在声明式“内联”样式中定义的 ORM 映射属性的类型推断,例如上面示例中
User
类的id
和name
属性。这包括User
实例将使用int
类型的id
和str
类型的name
。它还包括当访问User.id
和User.name
类级属性时,正如它们在上面的select()
语句中那样,它们与 SQL 表达式行为兼容,这是从InstrumentedAttribute
属性描述符类派生的。 - 将
__init__()
方法应用于尚未包含显式构造函数的映射类,该构造函数接受特定类型的关键字参数,用于检测到的所有映射属性。
当 Mypy 插件处理上述文件时,结果的静态类定义和传递给 Mypy 工具的 Python 代码等效于以下内容:
from sqlalchemy import Column, Integer, String, select from sqlalchemy.orm import Mapped from sqlalchemy.orm.decl_api import DeclarativeMeta class Base(metaclass=DeclarativeMeta): __abstract__ = True class User(Base): __tablename__ = "user" id: Mapped[Optional[int]] = Mapped._special_method( Column(Integer, primary_key=True) ) name: Mapped[Optional[str]] = Mapped._special_method(Column(String)) def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ... some_user = User(id=5, name="user") print(f"Username: {some_user.name}") select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
以上已经采取的关键步骤包括:
Base
类现在明确地以DeclarativeMeta
类的形式定义,而不是动态类。id
和name
属性是以Mapped
类的术语定义的,该类表示在类与实例级别上表现出不同行为的 Python 描述符。Mapped
类现在是用于所有 ORM 映射属性的InstrumentedAttribute
类的基类。Mapped
被定义为针对任意 Python 类型的通用类,这意味着Mapped
的特定出现与特定的 Python 类型相关联,例如上面的Mapped[Optional[int]]
和Mapped[Optional[str]]
。- 声明式映射属性分配的右侧 已移除,因为这类似于
Mapper
类通常会执行的操作,即它将这些属性替换为InstrumentedAttribute
的具体实例。原始表达式移到一个函数调用中,这将允许它仍然被类型检查而不与表达式的左侧发生冲突。对于 Mypy 来说,左侧的类型注释足以理解属性的行为。 - 为
User.__init__()
方法添加了类型存根,其中包括正确的关键字和数据类型。
使用方法
以下各小节将讨论迄今为止已考虑到的个别用例的 pep-484 符合性。
基于 TypeEngine 的列的自省
对于包含显式数据类型的映射列,当它们作为内联属性映射时,映射类型将被自动解析:
class MyClass(Base): # ... id = Column(Integer, primary_key=True) name = Column("employee_name", String(50), nullable=False) other_name = Column(String(50))
在上面,id
、name
和 other_name
这些最终的类级别数据类型将被解析为 Mapped[Optional[int]]
、Mapped[Optional[str]]
和 Mapped[Optional[str]]
。这些类型默认情况下总是被认为是 Optional
的,即使对于主键和非空列也是如此。原因是因为虽然数据库列id
和name
不能为 NULL,但 Python 属性 id
和 name
在没有显式构造函数的情况下肯定可以是 None
:
>>> m1 = MyClass() >>> m1.id None
上述列的类型可以被显式地声明,提供了更清晰的自我文档说明以及能够控制哪些类型是可选的两个优点:
class MyClass(Base): # ... id: int = Column(Integer, primary_key=True) name: str = Column("employee_name", String(50), nullable=False) other_name: Optional[str] = Column(String(50))
Mypy 插件将接受上述的 int
、str
和 Optional[str]
,并将它们转换为包含在 Mapped[]
类型周围的类型。Mapped[]
结构也可以被显式使用:
from sqlalchemy.orm import Mapped class MyClass(Base): # ... id: Mapped[int] = Column(Integer, primary_key=True) name: Mapped[str] = Column("employee_name", String(50), nullable=False) other_name: Mapped[Optional[str]] = Column(String(50))
当类型是非可选时,这意味着从 MyClass
实例访问的属性将被视为非 None:
mc = MyClass(...) # will pass mypy --strict name: str = mc.name
对于可选属性,Mypy 认为类型必须包括 None,否则为 Optional
:
mc = MyClass(...) # will pass mypy --strict other_name: Optional[str] = mc.name
无论映射属性是否被标记为 Optional
,生成的 __init__()
方法仍然将所有关键字视为可选的。这再次与 SQLAlchemy ORM 实际创建构造函数时的行为相匹配,不应与诸如 Python dataclasses
之类的验证系统的行为混淆,后者将生成一个与注释匹配的构造函数,以确定可选 vs. 必需属性的注解。
没有明确类型的列
包含 ForeignKey
修改器的列在 SQLAlchemy 声明式映射中不需要指定数据类型。对于这种类型的属性,Mypy 插件将通知用户需要发送一个显式类型:
# .. other imports from sqlalchemy.sql.schema import ForeignKey Base = declarative_base() class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) user_id = Column(ForeignKey("user.id"))
插件将如下传递消息:
$ mypy test3.py --strict test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from ORM mapped expression assigned to attribute 'user_id'; please specify a Python type or Mapped[<python type>] on the left hand side. Found 1 error in 1 file (checked 1 source file)
要解决此问题,请为 Address.user_id
列应用显式类型注释:
class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) user_id: int = Column(ForeignKey("user.id"))
使用命令式表格映射列
在命令式表格风格中,Column
定义位于一个与映射属性本身分离的 Table
结构内。Mypy 插件不考虑这个 Table
,而是支持可以显式声明属性,必须使用 Mapped
类来标识它们为映射属性:
class MyClass(Base): __table__ = Table( "mytable", Base.metadata, Column(Integer, primary_key=True), Column("employee_name", String(50), nullable=False), Column(String(50)), ) id: Mapped[int] name: Mapped[str] other_name: Mapped[Optional[str]]
上述Mapped
注释被视为映射列,并将包含在默认构造函数中,以及在类级别和实例级别为MyClass
提供正确的类型配置文件。
映射关系
该插件对使用类型推断来检测关系的类型有限支持。对于所有这些无法检测到类型的情况,它都将发出一个信息丰富的错误消息,在所有情况下,可以明确提供适当的类型,要么使用Mapped
类,要么选择在内联声明中省略它。该插件还需要确定关系是指向集合还是标量,并且为此依赖于relationship.uselist
和/或relationship.collection_class
参数的显式值。如果这些参数都不存在,则需要明确的类型,以及如果relationship()
的目标类型是字符串或可调用对象而不是类,则也需要明确的类型:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) user_id: int = Column(ForeignKey("user.id")) user = relationship(User)
上述映射将产生以下错误:
test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or collection for ORM mapped expression assigned to attribute 'user' if both 'uselist' and 'collection_class' arguments are absent from the relationship(); please specify a type annotation on the left hand side. Found 1 error in 1 file (checked 1 source file)
错误可以通过使用relationship(User, uselist=False)
或者提供类型来解决,例如标量User
对象:
class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) user_id: int = Column(ForeignKey("user.id")) user: User = relationship(User)
对于集合,类似的模式也适用,如果没有uselist=True
或者relationship.collection_class
,可以使用集合注释,如List
。在注释中使用类的字符串名称也是完全合适的,这是由 pep-484 支持的,确保在适当的时候在TYPE_CHECKING 块中导入该类:
from typing import TYPE_CHECKING, List from .mymodel import Base if TYPE_CHECKING: # if the target of the relationship is in another module # that cannot normally be imported at runtime from .myaddressmodel import Address class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) addresses: List["Address"] = relationship("Address")
与列相似,Mapped
类也可以显式地应用:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user") class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) user_id: int = Column(ForeignKey("user.id")) user: Mapped[User] = relationship(User, back_populates="addresses")
使用 @declared_attr 和声明性混合
declared_attr
类允许在类级函数中声明 Declarative 映射属性,并且在使用声明性混入时特别有用。对于这些函数,函数的返回类型应该使用Mapped[]
构造进行注释,或者指示函数返回的对象的确切类型。此外,未被映射的“混入”类(即不从declarative_base()
类继承,也不使用诸如registry.mapped()
之类的方法进行映射)应该用declarative_mixin()
装饰器进行修饰,这为 Mypy 插件提供了一个提示,表明特定类意图作为声明性混入:
from sqlalchemy.orm import declarative_mixin, declared_attr @declarative_mixin class HasUpdatedAt: @declared_attr def updated_at(cls) -> Column[DateTime]: # uses Column return Column(DateTime) @declarative_mixin class HasCompany: @declared_attr def company_id(cls) -> Mapped[int]: # uses Mapped return Column(ForeignKey("company.id")) @declared_attr def company(cls) -> Mapped["Company"]: return relationship("Company") class Employee(HasUpdatedAt, HasCompany, Base): __tablename__ = "employee" id = Column(Integer, primary_key=True) name = Column(String)
注意像HasCompany.company
这样的方法的实际返回类型与注释的不匹配。Mypy 插件将所有@declared_attr
函数转换为简单的注释属性,以避免这种复杂性:
# what Mypy sees class HasCompany: company_id: Mapped[int] company: Mapped["Company"]
SqlAlchemy 2.0 中文文档(三十一)(3)https://developer.aliyun.com/article/1562900