配置关系联接方式

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

relationship() 通常通过检查两个表之间的外键关系来创建两个表之间的联接,以确定应该比较哪些列。在各种情况下,需要对这种行为进行定制。

处理多个连接路径

要处理的最常见情况之一是两个表之间有多个外键路径。

考虑一下 Customer 类的两个外键 Address 班级:

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class Customer(Base):
    __tablename__ = 'customer'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    billing_address_id = Column(Integer, ForeignKey("address.id"))
    shipping_address_id = Column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address")
    shipping_address = relationship("Address")

class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    street = Column(String)
    city = Column(String)
    state = Column(String)
    zip = Column(String)

当我们尝试使用上面的映射时,会产生以下错误:

sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
condition between parent/child tables on relationship
Customer.billing_address - there are multiple foreign key
paths linking the tables.  Specify the 'foreign_keys' argument,
providing a list of those columns which should be
counted as containing a foreign key reference to the parent table.

上面的信息很长。有许多潜在的信息 relationship() can-return,它经过精心定制,可以检测到各种常见的配置问题;大多数会建议解决歧义或其他丢失信息所需的额外配置。

在这种情况下,消息希望我们限定每个 relationship() 通过指示每一列应考虑哪个外键列,相应的格式如下:

class Customer(Base):
    __tablename__ = 'customer'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    billing_address_id = Column(Integer, ForeignKey("address.id"))
    shipping_address_id = Column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address", foreign_keys=[billing_address_id])
    shipping_address = relationship("Address", foreign_keys=[shipping_address_id])

上面,我们指定了 foreign_keys 参数,它是 Column 或列表 Column 对象,指示那些列被视为“外部”,或者换句话说,包含引用父表的值的列。加载 Customer.billing_address 与A的关系 Customer 对象将使用 billing_address_id 以便识别中的行 Address 装载;类似地, shipping_address_id 用于 shipping_address 关系。两列的链接在持久化过程中也起作用;刚插入的 Address 对象将被复制到关联的 Customer 冲洗过程中的对象。

指定时 foreign_keys 对于声明性的,我们也可以使用字符串名称来指定,但是,如果使用列表,则 列表是字符串的一部分 ::

billing_address = relationship("Address", foreign_keys="[Customer.billing_address_id]")

在这个特定的示例中,在任何情况下都不需要列表,因为只有一个列表 Column 我们需要:

billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")

警告

当作为Python可求值字符串传递时 relationship.foreign_keys 参数使用Python的 eval() 功能。 不要将不受信任的输入传递到此字符串 . 见 关系论据的评估 有关声明性评估的详细信息 relationship() 争论。

指定备用联接条件

的默认行为 relationship() 在构造联接时,它将一边的主键列的值与另一边的外键引用列的值相等。我们可以将此标准更改为任何我们想使用的 relationship.primaryjoin 争论,以及 relationship.secondaryjoin 使用“secondary”表时的参数。

在下面的示例中,使用 User 类以及 Address 类存储街道地址,我们创建关系 boston_addresses 只装那些 Address 指定“波士顿”城市的对象:

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    boston_addresses = relationship("Address",
                    primaryjoin="and_(User.id==Address.user_id, "
                        "Address.city=='Boston')")

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

    street = Column(String)
    city = Column(String)
    state = Column(String)
    zip = Column(String)

在这个字符串SQL表达式中,我们使用了 and_() 连接构造为连接条件建立两个不同的谓词-连接 User.idAddress.user_id 列之间以及限制中的行 Address 只是 city='Boston' . 当使用声明性的、基本的SQL函数时,例如 and_() 在字符串的计算命名空间中自动可用 relationship() 争论。

警告

当作为Python可求值字符串传递时 relationship.primaryjoin 参数使用Python的 eval() 功能。 不要将不受信任的输入传递到此字符串 . 见 关系论据的评估 有关声明性评估的详细信息 relationship() 争论。

我们在 relationship.primaryjoin 通常只有当SQLAlchemy为了加载或表示此关系而呈现SQL时才有意义。也就是说,它用于发出的SQL语句中,以便执行每个属性的惰性加载,或者在查询时构造联接,例如通过 Query.join() 或者通过“联接”或“子查询”的加载样式。当内存中的对象被操纵时,我们可以放置任何 Address 我们想进入的对象 boston_addresses 集合,无论 .city 属性为。对象将一直存在于集合中,直到属性过期并从应用条件的数据库重新加载。当刷新发生时,对象在 boston_addresses 将无条件刷新,分配主键的值 user.id 列到外键保持上 address.user_id 每行的列。这个 city 条件在这里不起作用,因为刷新过程只关心将主键值同步到引用外键值。

创造习惯性的国外条件

主连接条件的另一个元素是如何确定那些被认为是“外部”的列。通常是 Column 对象将指定 ForeignKey 或以其他方式成为 ForeignKeyConstraint 这与连接条件有关。 relationship() 查找此外键状态,因为它决定如何为该关系加载和持久化数据。然而, relationship.primaryjoin 参数可用于创建不涉及任何“架构”级外键的联接条件。我们可以结合 relationship.primaryjoin 随着 relationship.foreign_keysrelationship.remote_side 为了建立这样的连接。

下面,一个班 HostEntry 连接到自身,等于字符串 content 列到 ip_address 列,它是PostgreSQL类型,名为 INET . 我们需要使用 cast() 为了将连接的一侧强制转换为另一侧的类型:

from sqlalchemy import cast, String, Column, Integer
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import INET

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class HostEntry(Base):
    __tablename__ = 'host_entry'

    id = Column(Integer, primary_key=True)
    ip_address = Column(INET)
    content = Column(String(50))

    # relationship() using explicit foreign_keys, remote_side
    parent_host = relationship("HostEntry",
                        primaryjoin=ip_address == cast(content, INET),
                        foreign_keys=content,
                        remote_side=ip_address
                    )

上述关系将产生一个连接,如:

SELECT host_entry.id, host_entry.ip_address, host_entry.content
FROM host_entry JOIN host_entry AS host_entry_1
ON host_entry_1.ip_address = CAST(host_entry.content AS INET)

上面的另一种语法是使用 foreign()remote() annotations ,在 relationship.primaryjoin 表达式。此语法表示 relationship() 通常单独应用于给定 relationship.foreign_keysrelationship.remote_side 争论。当存在显式联接条件时,这些函数可能更简洁,而且还可以精确地标记“外部”或“远程”列,而不管该列是多次声明还是在复杂的SQL表达式中声明:

from sqlalchemy.orm import foreign, remote

class HostEntry(Base):
    __tablename__ = 'host_entry'

    id = Column(Integer, primary_key=True)
    ip_address = Column(INET)
    content = Column(String(50))

    # relationship() using explicit foreign() and remote() annotations
    # in lieu of separate arguments
    parent_host = relationship("HostEntry",
                        primaryjoin=remote(ip_address) == \
                                cast(foreign(content), INET),
                    )

在联接条件中使用自定义运算符

关系的另一个用例是使用自定义运算符,例如PostgreSQL的“包含在” << 与类型(如)联接时的运算符 INETCIDR . 对于自定义运算符,我们使用 Operators.op() 功能:

inet_column.op("<<")(cidr_column)

但是,如果我们构造一个 relationship.primaryjoin 使用这个运算符, relationship() 仍然需要更多信息。这是因为当它检查我们的PrimaryJoin条件时,它专门查找用于 比较 ,这通常是包含已知比较运算符的固定列表,例如 ==< 等。因此,为了让我们的自定义运算符参与此系统,我们需要它使用 Operators.op.is_comparison 参数::

inet_column.op("<<", is_comparison=True)(cidr_column)

一个完整的例子:

class IPA(Base):
    __tablename__ = 'ip_address'

    id = Column(Integer, primary_key=True)
    v4address = Column(INET)

    network = relationship("Network",
                        primaryjoin="IPA.v4address.op('<<', is_comparison=True)"
                            "(foreign(Network.v4representation))",
                        viewonly=True
                    )
class Network(Base):
    __tablename__ = 'network'

    id = Column(Integer, primary_key=True)
    v4representation = Column(CIDR)

上面的查询,例如:

session.query(IPA).join(IPA.network)

将呈现为:

SELECT ip_address.id AS ip_address_id, ip_address.v4address AS ip_address_v4address
FROM ip_address JOIN network ON ip_address.v4address << network.v4representation

0.9.2 新版功能: -增加了 Operators.op.is_comparison 用于协助创建的标志 relationship() 使用自定义运算符构造。

基于SQL函数的自定义运算符

用例的变体 Operators.op.is_comparison 当我们不使用运算符,而是使用SQL函数时。这个用例的典型例子是postgresql postgis函数,但是任何数据库上解析为二进制条件的任何SQL函数都可能适用。为了适应这个用例, FunctionElement.as_comparison() 方法可以修改任何SQL函数,例如从 func 名称空间,向ORM指示函数生成两个表达式的比较。下面的示例用 Geoalchemy2 类库:

from geoalchemy2 import Geometry
from sqlalchemy import Column, Integer, func
from sqlalchemy.orm import relationship, foreign

class Polygon(Base):
    __tablename__ = "polygon"
    id = Column(Integer, primary_key=True)
    geom = Column(Geometry("POLYGON", srid=4326))
    points = relationship(
        "Point",
        primaryjoin="func.ST_Contains(foreign(Polygon.geom), Point.geom).as_comparison(1, 2)",
        viewonly=True,
    )

class Point(Base):
    __tablename__ = "point"
    id = Column(Integer, primary_key=True)
    geom = Column(Geometry("POINT", srid=4326))

上面, FunctionElement.as_comparison() 表示 func.ST_Contains() SQL函数正在比较 Polygon.geomPoint.geom 表达。这个 foreign() 注释还注意到在这个特定关系中哪个列承担“外键”角色。

1.3 新版功能: 补充 FunctionElement.as_comparison() .

重叠的外键

当使用组合外键时,可能会出现一种罕见的情况,例如单个列可能是通过外键约束引用的多个列的主题。

考虑(公认的复杂)映射,例如 Magazine 对象,由 Writer 对象与 Article 使用复合主键方案的对象,该方案包括 magazine_id 两者都有;然后 Article 参照 Writer 也, Article.magazine_id 参与两个独立的关系; Article.magazineArticle.writer ::

class Magazine(Base):
    __tablename__ = 'magazine'

    id = Column(Integer, primary_key=True)


class Article(Base):
    __tablename__ = 'article'

    article_id = Column(Integer)
    magazine_id = Column(ForeignKey('magazine.id'))
    writer_id = Column()

    magazine = relationship("Magazine")
    writer = relationship("Writer")

    __table_args__ = (
        PrimaryKeyConstraint('article_id', 'magazine_id'),
        ForeignKeyConstraint(
            ['writer_id', 'magazine_id'],
            ['writer.id', 'writer.magazine_id']
        ),
    )


class Writer(Base):
    __tablename__ = 'writer'

    id = Column(Integer, primary_key=True)
    magazine_id = Column(ForeignKey('magazine.id'), primary_key=True)
    magazine = relationship("Magazine")

配置上述映射后,我们将看到此警告发出::

SAWarning: relationship 'Article.writer' will copy column
writer.magazine_id to column article.magazine_id,
which conflicts with relationship(s): 'Article.magazine'
(copies magazine.id to article.magazine_id). Consider applying
viewonly=True to read-only relationships, or provide a primaryjoin
condition marking writable columns with the foreign() annotation.

这所指的源于以下事实: Article.magazine_id 是两个不同的外键约束的主题;它指 Magazine.id 直接作为源列,但也指 Writer.magazine_id 作为复合键上下文中的源列 Writer . 如果我们将 Article 有一个特别的 Magazine ,然后将 Article 用一个 Writer 这与 不同的 Magazine ,ORM将覆盖 Article.magazine_id 非确定性地,无声地改变我们所指的杂志;如果我们取消关联,它也可能试图将空值放入此列中。 Writer 从一个 Article . 警告让我们知道这是事实。

为了解决这个问题,我们需要打破 Article 包括以下三个功能:

  1. Article 首先写给 Article.magazine_id 基于 Article.magazine 仅关系,即从复制的值 Magazine.id .

  2. Article 可以写信给 Article.writer_id 代表保留在 Article.writer 关系,但只有 Writer.id 列; Writer.magazine_id 列不应写入 Article.magazine_id 因为它最终来源于 Magazine.id .

  3. ArticleArticle.magazine_id 加载时计入 Article.writer 尽管如此 代表这个关系写信给它。

为了得到1和2,我们只能指定 Article.writer_id 作为“外国钥匙” Article.writer ::

class Article(Base):
    # ...

    writer = relationship("Writer", foreign_keys='Article.writer_id')

然而,这具有 Article.writer 不采取 Article.magazine_id 查询时转入账户 Writer

SELECT article.article_id AS article_article_id,
    article.magazine_id AS article_magazine_id,
    article.writer_id AS article_writer_id
FROM article
JOIN writer ON writer.id = article.writer_id

因此,为了得到1、2和3的所有内容,我们表示连接条件以及要通过组合写入的列 relationship.primaryjoin 完全,以及 relationship.foreign_keys 参数,或者更简洁地通过注释 foreign() ::

class Article(Base):
    # ...

    writer = relationship(
        "Writer",
        primaryjoin="and_(Writer.id == foreign(Article.writer_id), "
                    "Writer.magazine_id == Article.magazine_id)")

在 1.0.0 版更改: 当一列同时用作多个关系的同步目标时,ORM将尝试发出警告。

非关系比较/物化路径

警告

本节详细介绍了一个实验特性。

使用自定义表达式意味着我们可以生成不符合常规主键/外键模型的非常规联接条件。一个这样的例子是物化路径模式,我们在其中比较字符串与重叠的路径标记,以生成树结构。

通过小心使用 foreign()remote() 我们可以建立一种关系,有效地产生一个基本的物化路径系统。基本上,当 foreign()remote() 是在 same 在比较表达式的一边,关系被认为是“一对多”;当它们处于 不同的 双方的关系被认为是“多对一”。为了在这里进行比较,我们将处理集合,因此我们将事物配置为“一对多”::

class Element(Base):
    __tablename__ = 'element'

    path = Column(String, primary_key=True)

    descendants = relationship('Element',
                           primaryjoin=
                                remote(foreign(path)).like(
                                        path.concat('/%')),
                           viewonly=True,
                           order_by=path)

上面,如果给出 Element 路径属性为的对象 "/foo/bar2" 我们要找一堆 Element.descendants 看起来像::

SELECT element.path AS element_path
FROM element
WHERE element.path LIKE ('/foo/bar2' || '/%') ORDER BY element.path

0.9.5 新版功能: 添加了支持以允许在PrimaryJoin条件内对自身进行单列比较,以及对使用的PrimaryJoin条件进行比较。 ColumnOperators.like() 作为比较运算符。

自指多对多关系

参见

本节介绍了“邻接列表”模式的两个表变体,文档如下所示 相邻列表关系 。请务必复习小节中的自引用查询模式 自引用查询策略配置自引用预加载 它们同样适用于这里讨论的映射模式。

多对多关系可以由一个或两个 relationship.primaryjoinrelationship.secondaryjoin -后者对于使用 relationship.secondary 争论。涉及使用 relationship.primaryjoinrelationship.secondaryjoin 当从一个类到它自己建立一个多对多关系时,如下所示:

from sqlalchemy import Integer, ForeignKey, String, Column, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

node_to_node = Table("node_to_node", Base.metadata,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True)
)

class Node(Base):
    __tablename__ = 'node'
    id = Column(Integer, primary_key=True)
    label = Column(String)
    right_nodes = relationship("Node",
                        secondary=node_to_node,
                        primaryjoin=id==node_to_node.c.left_node_id,
                        secondaryjoin=id==node_to_node.c.right_node_id,
                        backref="left_nodes"
    )

在上面的位置,SQLAlchemy无法自动知道哪些列应该连接到 right_nodesleft_nodes 关系。这个 relationship.primaryjoinrelationship.secondaryjoin 参数确定我们希望如何加入关联表。在上面的声明形式中,正如我们在对应于 Nodeid 变量直接作为 Column 我们希望加入的对象。

或者,我们可以定义 relationship.primaryjoinrelationship.secondaryjoin 使用字符串的参数,这适用于我们的配置没有 Node.id 列对象可用,或者 node_to_node 可能还没有桌子。指平原时 Table 对象在声明性字符串中,我们使用表的字符串名称,因为它存在于 MetaData ::

class Node(Base):
    __tablename__ = 'node'
    id = Column(Integer, primary_key=True)
    label = Column(String)
    right_nodes = relationship("Node",
                        secondary="node_to_node",
                        primaryjoin="Node.id==node_to_node.c.left_node_id",
                        secondaryjoin="Node.id==node_to_node.c.right_node_id",
                        backref="left_nodes"
    )

警告

当作为Python可求值字符串传递时 relationship.primaryjoinrelationship.secondaryjoin 参数使用Python的 eval() 功能。 不要将不受信任的输入传递给这些字符串 . 见 关系论据的评估 有关声明性评估的详细信息 relationship() 争论。

这里的经典映射情况类似,其中 node_to_node 可以加入到 node.c.id ::

from sqlalchemy import Integer, ForeignKey, String, Column, Table, MetaData
from sqlalchemy.orm import relationship, registry

metadata_obj = MetaData()
mapper_registry = registry()

node_to_node = Table("node_to_node", metadata_obj,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True)
)

node = Table("node", metadata_obj,
    Column('id', Integer, primary_key=True),
    Column('label', String)
)
class Node(object):
    pass

mapper_registry.map_imperatively(Node, node, properties={
    'right_nodes':relationship(Node,
                        secondary=node_to_node,
                        primaryjoin=node.c.id==node_to_node.c.left_node_id,
                        secondaryjoin=node.c.id==node_to_node.c.right_node_id,
                        backref="left_nodes"
                    )})

注意,在这两个例子中, relationship.backref 关键字指定 left_nodes 后退-当 relationship() 以相反的方向创建第二个关系,它足够智能地反转 relationship.primaryjoinrelationship.secondaryjoin 争论。

参见

复合“二次”连接

注解

本节介绍SQLAlchemy支持的远边缘案例,但是建议尽可能以更简单的方式解决此类问题,方法是使用合理的关系布局和/或 in-Python attributes .

有时,当一个人试图建立一个 relationship() 在两个表之间,需要涉及两个或三个以上的表才能联接它们。这是一片 relationship() 在这里,我们试图突破可能的边界,并且通常需要在sqlacalchemy邮件列表中详细说明这些外来用例的最终解决方案。

在最新版本的sqlacalchemy中, relationship.secondary 在某些情况下,可以使用参数来提供由多个表组成的复合目标。下面是这样一个连接条件的示例(至少需要0.9.2版才能按原样工作)::

class A(Base):
    __tablename__ = 'a'

    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey('b.id'))

    d = relationship("D",
                secondary="join(B, D, B.d_id == D.id)."
                            "join(C, C.d_id == D.id)",
                primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)",
                secondaryjoin="D.id == B.d_id",
                uselist=False,
                viewonly=True
                )

class B(Base):
    __tablename__ = 'b'

    id = Column(Integer, primary_key=True)
    d_id = Column(ForeignKey('d.id'))

class C(Base):
    __tablename__ = 'c'

    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey('a.id'))
    d_id = Column(ForeignKey('d.id'))

class D(Base):
    __tablename__ = 'd'

    id = Column(Integer, primary_key=True)

在上面的示例中,我们提供了 relationship.secondaryrelationship.primaryjoinrelationship.secondaryjoin ,以引用命名表的声明性样式 abcd 直接。从查询 AD 看起来像:

sess.query(A).join(A.d).all()

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a JOIN (
    b AS b_1 JOIN d AS d_1 ON b_1.d_id = d_1.id
        JOIN c AS c_1 ON c_1.d_id = d_1.id)
    ON a.b_id = b_1.id AND a.id = c_1.a_id JOIN d ON d.id = b_1.d_id

在上面的示例中,我们利用能够将多个表填充到一个“辅助”容器中的优势,这样我们就可以跨多个表进行连接,同时还可以为 relationship() 在这一点上,“左”和“右”都只有“一”个表;复杂性保持在中间。

警告

像上面这样的关系通常标记为 viewonly=True 并且应被视为只读。上面的关系有时是复杂的,而这种关系通常是可以写的。

与别名类的关系

1.3 新版功能: 这个 AliasedClass 现在可以将构造指定为 relationship() 替换了以前使用非主映射器的方法,这种方法有一些限制,例如它们不继承被映射实体的子关系,而且它们需要针对可选对象进行复杂配置。本节中的配方现在更新为使用 AliasedClass .

In the previous section, we illustrated a technique where we used relationship.secondary in order to place additional tables within a join condition. There is one complex join case where even this technique is not sufficient; when we seek to join from A to B, making use of any number of C, D, etc. in between, however there are also join conditions between A and B directly. In this case, the join from A to B may be difficult to express with just a complex relationship.primaryjoin condition, as the intermediary tables may need special handling, and it is also not expressible with a relationship.secondary object, since the A->secondary->B pattern does not support any references between A and B directly. When this extremely advanced case arises, we can resort to creating a second mapping as a target for the relationship. This is where we use AliasedClass in order to make a mapping to a class that includes all the additional tables we need for this join. In order to produce this mapper as an "alternative" mapping for our class, we use the aliased() function to produce the new construct, then use relationship() against the object as though it were a plain mapped class.

下图说明了 relationship() 通过简单的联接 AB 但是,PrimaryJoin条件被另外两个实体所增强。 CD ,其中也必须有与两行中的行对齐的行 AB 同时:

class A(Base):
    __tablename__ = 'a'

    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey('b.id'))

class B(Base):
    __tablename__ = 'b'

    id = Column(Integer, primary_key=True)

class C(Base):
    __tablename__ = 'c'

    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey('a.id'))

    some_c_value = Column(String)

class D(Base):
    __tablename__ = 'd'

    id = Column(Integer, primary_key=True)
    c_id = Column(ForeignKey('c.id'))
    b_id = Column(ForeignKey('b.id'))

    some_d_value = Column(String)

# 1. set up the join() as a variable, so we can refer
# to it in the mapping multiple times.
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

# 2. Create an AliasedClass to B
B_viacd = aliased(B, j, flat=True)

A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

通过上面的映射,一个简单的连接看起来像:

sess.query(A).join(A.b).all()

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a JOIN (b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) ON a.b_id = b.id

在查询中使用AliasedClass目标

在上一个示例中, A.b 关系指的是 B_viacd 实体作为目标,并且 not 这个 B 类直接调用。要添加涉及的其他条件,请执行以下操作 A.b 关系,则通常需要引用 B_viacd 直接使用,而不是使用 B ,特别是在以下情况下: A.b 要转换为别名或子查询。下面说明了使用子查询而不是联接的相同关系:

subq = select(B).join(D, D.b_id == B.id).join(C, C.id == D.c_id).subquery()

B_viacd_subquery = aliased(B, subq)

A.b = relationship(B_viacd_subquery, primaryjoin=A.b_id == subq.c.id)

使用上面的查询 A.b 关系将呈现一个子查询:

sess.query(A).join(A.b).all()

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a JOIN (SELECT b.id AS id, b.some_b_column AS some_b_column
FROM b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) AS anon_1 ON a.b_id = anon_1.id

如果我们想要基于 A.b 加入,我们必须在以下方面这样做 B_viacd_subquery 而不是 B 直接:

(
  sess.query(A).join(A.b).
  filter(B_viacd_subquery.some_b_column == "some b").
  order_by(B_viacd_subquery.id)
).all()

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a JOIN (SELECT b.id AS id, b.some_b_column AS some_b_column
FROM b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) AS anon_1 ON a.b_id = anon_1.id
WHERE anon_1.some_b_column = ? ORDER BY anon_1.id

与窗口函数的行限制关系

关系的另一个有趣的用例 AliasedClass 对象是关系需要连接到任何形式的专门选择的情况。一种情况是需要使用window函数时,例如限制关系应返回多少行。下面的示例说明了将为每个集合加载前十个项的非主映射器关系:

class A(Base):
    __tablename__ = 'a'

    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = 'b'
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

partition = select(
    B,
    func.row_number().over(
        order_by=B.id, partition_by=B.a_id
    ).label('index')
).alias()

partitioned_b = aliased(B, partition)

A.partitioned_bs = relationship(
    partitioned_b,
    primaryjoin=and_(partitioned_b.a_id == A.id, partition.c.index < 10)
)

我们可以用上面的 partitioned_bs 与大多数装载机策略的关系,例如 selectinload() ::

for a1 in s.query(A).options(selectinload(A.partitioned_bs)):
    print(a1.partitioned_bs)   # <-- will be no more than ten objects

上面的“selectinload”查询如下所示:

SELECT
    a_1.id AS a_1_id, anon_1.id AS anon_1_id, anon_1.a_id AS anon_1_a_id,
    anon_1.data AS anon_1_data, anon_1.index AS anon_1_index
FROM a AS a_1
JOIN (
    SELECT b.id AS id, b.a_id AS a_id, b.data AS data,
    row_number() OVER (PARTITION BY b.a_id ORDER BY b.id) AS index
    FROM b) AS anon_1
ON anon_1.a_id = a_1.id AND anon_1.index < %(index_1)s
WHERE a_1.id IN ( ... primary key collection ...)
ORDER BY a_1.id

上面,对于“a”中的每个匹配主键,我们将按照“b.id”的顺序获得前十个“bs”。通过对“a_id”进行分区,我们确保每个“row number”都是父“a_id”的本地行。

这种映射通常还包括从“a”到“b”的“普通”关系,用于持久性操作,以及当需要每个“a”的完整“b”对象集时。

正在生成启用查询的属性

非常雄心勃勃的自定义连接条件可能无法直接持久化,在某些情况下甚至可能无法正确加载。要删除公式的持久性部分,请使用标志 relationship.viewonlyrelationship() ,它将其建立为只读属性(在flush()上写入集合的数据将被忽略)。但是,在极端情况下,考虑将常规的python属性与 Query 如下:

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

    @property
    def addresses(self):
        return object_session(self).query(Address).with_parent(self).filter(...).all()

在其他情况下,可以构建描述符来利用Python中现有的数据。请参见 使用描述符和混合 对于特殊Python属性的更一般的讨论。

参见

使用描述符和混合