SqlAlchemy 2.0 中文文档(三十一)(2)

简介: SqlAlchemy 2.0 中文文档(三十一)

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[]类型应用于属性idname等,因为它们会被@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-stubssqlalchemy2-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 类的 idname 属性。这包括 User 实例将使用 int 类型的 idstr 类型的 name。它还包括当访问 User.idUser.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 类的形式定义,而不是动态类。
  • idname 属性是以 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))

在上面,idnameother_name 这些最终的类级别数据类型将被解析为 Mapped[Optional[int]]Mapped[Optional[str]]Mapped[Optional[str]]。这些类型默认情况下总是被认为是 Optional 的,即使对于主键和非空列也是如此。原因是因为虽然数据库列idname不能为 NULL,但 Python 属性 idname 在没有显式构造函数的情况下肯定可以是 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 插件将接受上述的 intstrOptional[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

相关文章
|
4月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(三十七)(2)
SqlAlchemy 2.0 中文文档(三十七)
42 2
|
4月前
|
存储 SQL 测试技术
SqlAlchemy 2.0 中文文档(三十一)(1)
SqlAlchemy 2.0 中文文档(三十一)
54 1
|
4月前
|
JSON 测试技术 数据格式
SqlAlchemy 2.0 中文文档(三十一)(4)
SqlAlchemy 2.0 中文文档(三十一)
38 1
|
4月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(三十一)(3)
SqlAlchemy 2.0 中文文档(三十一)
28 1
|
4月前
|
SQL API 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(三十二)(3)
SqlAlchemy 2.0 中文文档(三十二)
33 1
|
4月前
|
SQL 存储 缓存
SqlAlchemy 2.0 中文文档(三十七)(4)
SqlAlchemy 2.0 中文文档(三十七)
49 1
|
4月前
|
SQL 存储 缓存
SqlAlchemy 2.0 中文文档(三十七)(3)
SqlAlchemy 2.0 中文文档(三十七)
33 1
|
4月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(三十七)(5)
SqlAlchemy 2.0 中文文档(三十七)
21 1
|
4月前
|
SQL 存储 关系型数据库
SqlAlchemy 2.0 中文文档(三十四)(4)
SqlAlchemy 2.0 中文文档(三十四)
45 1
|
4月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(三十四)(5)
SqlAlchemy 2.0 中文文档(三十四)
41 0