SQL 表达式作为映射属性

映射类上的属性可以链接到 SQL 表达式,这些表达式可以在查询中使用。

使用混合属性

将相对简单的 SQL 表达式链接到类的最简单、最灵活的方法是使用所谓的“混合属性”,在 混合属性 部分进行了描述。混合属性提供了一个在 Python 层级和 SQL 表达式层级上都工作的表达式。例如,在下面我们映射一个名为 User 的类,它包含属性 firstnamelastname,并且包含一个混合属性,它将为我们提供 fullname,它是这两个属性的字符串连接。

from sqlalchemy.ext.hybrid import hybrid_property


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        return self.firstname + " " + self.lastname

上面,fullname 属性在实例和类级别上都被解释,因此它可以从实例

some_user = session.scalars(select(User).limit(1)).first()
print(some_user.fullname)

以及在查询中使用

some_user = session.scalars(
    select(User).where(User.fullname == "John Smith").limit(1)
).first()

字符串连接示例是一个简单的示例,其中 Python 表达式可以在实例和类级别上双重使用。通常,SQL 表达式必须与 Python 表达式区分开来,这可以通过使用 hybrid_property.expression() 来实现。下面我们说明一个在混合属性中需要存在条件的情况,在 Python 中使用 if 语句,并在 SQL 表达式中使用 case() 结构

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql import case


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        if self.firstname is not None:
            return self.firstname + " " + self.lastname
        else:
            return self.lastname

    @fullname.expression
    def fullname(cls):
        return case(
            (cls.firstname != None, cls.firstname + " " + cls.lastname),
            else_=cls.lastname,
        )

使用 column_property

column_property() 函数可用于以类似于常规映射的 Column 的方式映射 SQL 表达式。使用此技术,属性会在加载时与所有其他列映射属性一起加载。在某些情况下,这比使用混合属性更具优势,因为该值可以在对象加载时与父行一起加载,特别是如果表达式是连接到其他表(通常是作为相关子查询)以访问通常不会在已加载对象上可用的数据的表达式。

使用 column_property() 映射 SQL 表达式的缺点包括表达式必须与为整个类发出的 SELECT 语句兼容,并且在从声明式 mixin 中使用 column_property() 时,也会出现一些配置上的怪癖。

我们的“fullname”示例可以使用 column_property() 表示如下

from sqlalchemy.orm import column_property


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))
    fullname = column_property(firstname + " " + lastname)

相关子查询也可以使用。下面我们使用 select() 结构创建一个 ScalarSelect,它表示一个面向列的 SELECT 语句,它将特定 User 可用的 Address 对象的数量链接在一起。

from sqlalchemy.orm import column_property
from sqlalchemy import select, func
from sqlalchemy import Column, Integer, String, ForeignKey

from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user.id"))


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    address_count = column_property(
        select(func.count(Address.id))
        .where(Address.user_id == id)
        .correlate_except(Address)
        .scalar_subquery()
    )

在上面的示例中,我们定义了一个类似于以下内容的 ScalarSelect() 结构

stmt = (
    select(func.count(Address.id))
    .where(Address.user_id == id)
    .correlate_except(Address)
    .scalar_subquery()
)

上面,我们首先使用 select() 创建一个 Select 结构,然后使用 Select.scalar_subquery() 方法将其转换为 标量子查询,表示我们希望在列表达式上下文中使用此 Select 语句。

Select 本身中,我们选择 Address.id 行的计数,其中 Address.user_id 列等于 id,在 User 类的上下文中,它是名为 idColumn(请注意,id 也是 Python 内置函数的名称,这不是我们想要使用的 - 如果我们不在 User 类定义中,我们会使用 User.id)。

Select.correlate_except() 方法表示此 select() 的 FROM 子句中的每个元素都可以从 FROM 列表中省略(即,与针对 User 的外层 SELECT 语句相关联),除了对应于 Address 的那个元素。这并不是严格必要的,但是它可以防止 AddressUserAddress 表之间存在大量的连接的情况下,并且针对 Address 的 SELECT 语句是嵌套的,意外地从 FROM 列表中省略。

对于引用从多对多关系链接的列的 column_property(),使用 and_() 将关联表的字段连接到关系中的两个表。

from sqlalchemy import and_


class Author(Base):
    # ...

    book_count = column_property(
        select(func.count(books.c.id))
        .where(
            and_(
                book_authors.c.author_id == authors.c.id,
                book_authors.c.book_id == books.c.id,
            )
        )
        .scalar_subquery()
    )

向现有的声明式映射类添加 column_property()

如果导入问题阻止 column_property() 在类内联定义,则可以在配置完类和属性后将其分配给类。当使用使用声明式基类的映射时(即由 DeclarativeBase 超类或传统函数(如 declarative_base())生成的),这种属性分配的效果相当于调用 Mapper.add_property() 来添加一个额外的属性。

# only works if a declarative base class is in use
User.address_count = column_property(
    select(func.count(Address.id)).where(Address.user_id == User.id).scalar_subquery()
)

当使用不使用声明式基类的映射风格时,例如 registry.mapped() 装饰器,可以显式地在底层的 Mapper 对象上调用 Mapper.add_property() 方法,该对象可以使用 inspect() 获取。

from sqlalchemy.orm import registry

reg = registry()


@reg.mapped
class User:
    __tablename__ = "user"

    # ... additional mapping directives


# later ...

# works for any kind of mapping
from sqlalchemy import inspect

inspect(User).add_property(
    column_property(
        select(func.count(Address.id))
        .where(Address.user_id == User.id)
        .scalar_subquery()
    )
)

在映射时从列属性进行组合

可以创建将多个 ColumnProperty 对象组合在一起的映射。在 Core 表达式上下文中使用时,ColumnProperty 将被解释为 SQL 表达式,前提是它被现有表达式对象定位;这是通过 Core 检测该对象是否具有 __clause_element__() 方法来完成的,该方法返回一个 SQL 表达式。但是,如果 ColumnProperty 在没有其他 Core SQL 表达式对象作为目标的表达式中用作引导对象,则 ColumnProperty.expression 属性将返回底层的 SQL 表达式,以便它可以用于一致地构建 SQL 表达式。下面,File 类包含一个属性 File.path,它将字符串标记连接到 File.filename 属性,该属性本身是一个 ColumnProperty

class File(Base):
    __tablename__ = "file"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(64))
    extension = mapped_column(String(8))
    filename = column_property(name + "." + extension)
    path = column_property("C:/" + filename.expression)

File 类在表达式中正常使用时,分配给 filenamepath 的属性可以直接使用。只有在映射定义中直接使用 ColumnProperty 时,才需要使用 ColumnProperty.expression 属性。

stmt = select(File.path).where(File.filename == "foo.txt")

column_property() 中使用列延迟

ORM 查询指南使用列延迟限制加载的列 中介绍的列延迟功能,可以在映射时应用于由 column_property() 映射的 SQL 表达式,方法是使用 deferred() 函数代替 column_property()

from sqlalchemy.orm import deferred


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    firstname: Mapped[str] = mapped_column()
    lastname: Mapped[str] = mapped_column()
    fullname: Mapped[str] = deferred(firstname + " " + lastname)

使用普通描述符

column_property()hybrid_property 无法提供的比 SQL 查询更复杂的情况下,可以使用作为属性访问的常规 Python 函数,假设表达式只需要在已加载的实例上可用。该函数用 Python 自己的 @property 装饰器进行装饰,以将其标记为只读属性。在函数内,object_session() 用于定位与当前对象相对应的 Session,然后用于发出查询

from sqlalchemy.orm import object_session
from sqlalchemy import select, func


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @property
    def address_count(self):
        return object_session(self).scalar(
            select(func.count(Address.id)).where(Address.user_id == self.id)
        )

普通描述符方法作为最后的手段很有用,但通常情况下,它的性能不如混合属性和列属性方法,因为它需要在每次访问时发出 SQL 查询。

查询时 SQL 表达式作为映射属性

除了能够在映射类上配置固定的 SQL 表达式外,SQLAlchemy ORM 还包含一个特性,其中对象可以加载任意 SQL 表达式的结果,这些表达式在查询时作为其状态的一部分设置。这种行为可以通过使用 query_expression() 配置 ORM 映射属性,然后在查询时使用 with_expression() 加载器选项来实现。有关映射和使用的示例,请参见部分 在对象上加载任意 SQL 表达式