SqlAlchemy 2.0 中文文档(三十一)(2)https://developer.aliyun.com/article/1562898
与 Dataclasses 或其他类型敏感的属性系统结合
Python dataclasses 集成的示例在将 ORM 映射应用于现有数据类(传统数据类用法)中提出了一个问题;Python dataclasses 期望一个明确的类型,它将用于构建类,并且每个赋值语句中给定的值是重要的。也就是说,一个如下所示的类必须要准确地声明才能被 dataclasses 接受:
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 还有另一个专门用于 dataclasses 的插件,这也可能影响我们的操作。
上述类实际上会通过 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 映射类。
基于 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]]
。类型默认始终被视为可选,即使对于主键和非空列也是如此。原因是因为虽然数据库列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
)的行为混淆,后者将根据注释生成与可选与必需属性相匹配的构造函数。
不具有显式类型的列
包含 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 block中适当导入:
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
类允许在类级函数中声明声明性映射属性,并且在使用声明性混合时特别有用。对于这些函数,函数的返回类型应该使用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"]
与数据类或其他类型敏感的属性系统结合
Python 数据类集成示例中的将 ORM 映射应用到现有数据类(旧数据类使用)存在一个问题;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 中文文档(三十一)(4)https://developer.aliyun.com/article/1562901