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

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

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[]类型应用于属性idname等,因为它们将被@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))

在上面,idnameother_name的最终类级数据类型将被内省为Mapped[Optional[int]]Mapped[Optional[str]]Mapped[Optional[str]]。类型默认始终被视为可选,即使对于主键和非空列也是如此。原因是因为虽然数据库列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)的行为混淆,后者将根据注释生成与可选与必需属性相匹配的构造函数。

不具有显式类型的列

包含 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=Truerelationship.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[]类型应用于属性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 中文文档(三十一)(4)https://developer.aliyun.com/article/1562901

相关文章
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(二十九)(3)
SqlAlchemy 2.0 中文文档(二十九)
28 4
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(二十九)(2)
SqlAlchemy 2.0 中文文档(二十九)
28 7
|
1月前
|
SQL 存储 关系型数据库
SqlAlchemy 2.0 中文文档(二十九)(1)
SqlAlchemy 2.0 中文文档(二十九)
31 4
|
1月前
|
存储 SQL 测试技术
SqlAlchemy 2.0 中文文档(三十一)(1)
SqlAlchemy 2.0 中文文档(三十一)
17 1
|
1月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(三十一)(2)
SqlAlchemy 2.0 中文文档(三十一)
17 1
|
1月前
|
JSON 测试技术 数据格式
SqlAlchemy 2.0 中文文档(三十一)(4)
SqlAlchemy 2.0 中文文档(三十一)
21 1
|
1月前
|
SQL 存储 关系型数据库
SqlAlchemy 2.0 中文文档(三十四)(4)
SqlAlchemy 2.0 中文文档(三十四)
24 1
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(三十四)(5)
SqlAlchemy 2.0 中文文档(三十四)
18 0
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(二十九)(4)
SqlAlchemy 2.0 中文文档(二十九)
28 4
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(二十九)(5)
SqlAlchemy 2.0 中文文档(二十九)
17 1