作为映射属性的 SQL 表达式

优质
小牛编辑
128浏览
2023-12-01

映射类上的属性可以链接到SQL表达式,SQL表达式可用于查询。

使用杂交种

将相对简单的SQL表达式链接到类的最简单和最灵活的方法是使用一个在本节中描述的所谓的“混合属性”。 混合属性 . 混合提供了一个同时在Python和SQL表达式级别工作的表达式。例如,下面我们映射一个类 User ,包含属性 firstnamelastname 包括一个混合动力车 fullname ,这是两个字符串的串联:

from sqlalchemy.ext.hybrid import hybrid_property

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

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

上面, fullname 属性在实例和类级别都被解释,以便它可以从实例中使用::

some_user = session.query(User).first()
print(some_user.fullname)

以及在查询中可用:

some_user = session.query(User).filter(User.fullname == "John Smith").first()

字符串连接示例是一个简单的示例,其中python表达式可以在实例和类级别上实现双重目的。通常,必须将SQL表达式与Python表达式区分开来,后者可以使用 hybrid_property.expression() . 下面我们将说明在混合动力系统中需要存在条件的情况,使用 if python和 case() SQL表达式的构造::

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

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    firstname = Column(String(50))
    lastname = 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() 函数可用于以类似于定期映射的方式映射SQL表达式 Column . 使用此技术,将在加载时与所有其他列映射属性一起加载属性。在某些情况下,这是使用混合的一个优势,因为值可以与对象的父行同时预先加载,特别是当表达式链接到其他表(通常作为相关子查询)以访问通常在已加载的对象上不可用的数据时。

使用的缺点 column_property() 因为SQL表达式包括表达式必须与为类整体发出的select语句兼容,并且在使用 column_property() 来自声明性混合。

我们的“全名”示例可以用 column_property() 如下:

from sqlalchemy.orm import column_property

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

也可以使用相关子查询。下面我们使用 select() 构造以创建 ScalarSelect ,表示面向列的SELECT语句,该语句将 Address 可用于特定对象的对象 User ::

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

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

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

class User(Base):
    __tablename__ = 'user'
    id = 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 构造,然后将其转换为 scalar subquery 使用 Select.scalar_subquery() 方法,表明我们打算使用此 Select 列表达式上下文中的语句。

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

这个 Select.correlate_except() 方法指示此的FROM子句中的每个元素 select() 可以从FROM列表中省略(即,与封闭的SELECT语句相关联 User ),但对应于 Address 。这不是严格必要的,但可以防止 Address 之间的长字符串连接的情况下,不会意外地从From列表中遗漏 UserAddress SELECT语句所针对的表 Address 是嵌套的。

如果导入问题阻止 column_property() 从与类一起内联定义开始,可以在两个类都配置之后将其分配给类。当使用利用 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()
)

使用不使用的映射样式时 declarative_base() ,例如 registry.mapped() 装饰师, Mapper.add_property() 方法可以在基础 Mapper 对象,该对象可以使用 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()
    )
)

对于一个 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()
    )

在映射时从列属性合成

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

class File(Base):
    __tablename__ = 'file'

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

File 类通常用于表达式中,将属性指定给 filenamepath 可以直接使用。使用 ColumnProperty.expression 属性仅在使用 ColumnProperty 直接在映射定义中:

q = session.query(File.path).filter(File.filename == 'foo.txt')

使用普通描述符

如果SQL查询比 column_property()hybrid_property 必须发出can-provide,可以使用作为属性访问的常规python函数,假定表达式只需要在已加载的实例上可用。这个函数是用python自己的 @property decorator将其标记为只读属性。在函数中, object_session() 用于定位 Session 对应于当前对象,该对象随后用于发出查询::

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

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    firstname = Column(String(50))
    lastname = 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表达式

使用时 Session.query() 我们不仅可以指定映射的实体,还可以指定特殊的SQL表达式。假设一个班 A 具有整数属性 .x.y ,我们可以查询 A 对象,以及 .x.y 如下:

q = session.query(A, A.x + A.y)

上面的查询返回窗体的元组 (A object, integer) .

存在可应用临时 A.x + A.y 返回的表达式 A 对象,而不是作为单独的元组项;这是 with_expression() 查询选项与 query_expression() 属性映射。类被映射为包含一个占位符属性,其中可以应用任何特定的SQL表达式::

from sqlalchemy.orm import query_expression

class A(Base):
    __tablename__ = 'a'
    id = Column(Integer, primary_key=True)
    x = Column(Integer)
    y = Column(Integer)

    expr = query_expression()

然后我们可以查询类型为的对象 A ,应用要填充到的任意SQL表达式 A.expr ::

from sqlalchemy.orm import with_expression
q = session.query(A).options(
    with_expression(A.expr, A.x + A.y))

这个 query_expression() 地图绘制有以下注意事项:

  • 在一个物体上 query_expression() 未用于填充属性,对象实例上的属性将具有值 None ,除非 query_expression.default_expr 参数设置为备用SQL表达式。

  • 查询表达式值 不填充已加载的对象 . 也就是说,这将 不工作 ::

    obj = session.query(A).first()
    
    obj = session.query(A).options(with_expression(A.expr, some_expr)).first()

    要确保重新加载属性,请使用 Query.populate_existing() ::

    obj = session.query(A).populate_existing().options(
        with_expression(A.expr, some_expr)).first()
  • 查询表达式值 对象过期时不刷新 . 一旦对象过期,可以通过 Session.expire() 或通过“到期”提交行为 Session.commit() ,该值将从属性中移除并将返回 None 在后续访问时。只有运行一个新的 Query 接触对象,其中包括一个新的 with_expression() 指令将属性设置为非无值。

  • 当前映射的属性 不能 应用于查询的其他部分,如WHERE子句、ORDER BY子句,并使用特殊表达式;即,这不起作用:

    # wont work
    q = session.query(A).options(
        with_expression(A.expr, A.x + A.y)
    ).filter(A.expr > 5).order_by(A.expr)

    这个 A.expr 表达式将在上面的WHERE子句和ORDER BY子句中解析为空。要在整个查询中使用表达式,请指定一个变量并使用该变量:

    a_expr = A.x + A.y
    q = session.query(A).options(
        with_expression(A.expr, a_expr)
    ).filter(a_expr > 5).order_by(a_expr)

1.2 新版功能.