【问题标题】:sqlalchemy self-referencing many-to-many with "select" as association tablesqlalchemy 自引用多对多与“选择”作为关联表
【发布时间】:2018-12-22 08:46:55
【问题描述】:

问题描述

我正在使用 sqlalchemy (v1.2) 声明式,并且我有一个简单的类 Node 带有一个 id 和一个标签。我想建立一个自引用的多对多关系,其中关联表不是数据库表,而是动态select 语句。此语句从Node 的两个连接别名中进行选择,并返回(left_id, right_id) 形式的行,定义关系。如果我通过实例对象访问关系,到目前为止我的代码可以工作,但是当我尝试按关系过滤时,连接就会混乱。

“经典”自指多对多关系

作为参考,让我们从Self-Referential Many-to-Many Relationship 文档中的示例开始,它使用关联表:

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, unique=True)
    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"
    )

    def __repr__(self):
        return "Node(id={}, Label={})".format(self.id, self.label)

通过这种关系将Node 加入到自己:

>>> NodeAlias = aliased(Node)
>>> print(session.query(Node).join(NodeAlias, Node.right_nodes))
SELECT node.id AS node_id, node.label AS node_label 
FROM node JOIN node_to_node AS node_to_node_1 
    ON node.id = node_to_node_1.left_node_id
JOIN node AS node_1
    ON node_1.id = node_to_node_1.right_node_id

一切看起来都很好。

通过关联选择语句的多对多关系

作为示例,我们实现了一个关系next_two_nodes,它将一个节点连接到具有id+1id+2(如果存在)的两个节点。 complete code 用于测试。

这是一个为“动态”关联表生成选择语句的函数:

_next_two_nodes = None
def next_two_nodes_select():
    global _next_two_nodes
    if _next_two_nodes is None:
        _leftside = aliased(Node, name="leftside")
        _rightside = aliased(Node, name="rightside")
        _next_two_nodes = select(
            [_leftside.id.label("left_node_id"),
             _rightside.id.label("right_node_id")]
        ).select_from(
            join(
                _leftside, _rightside,
                or_(
                    _leftside.id + 1 == _rightside.id,
                    _leftside.id + 2 == _rightside.id
                )
            )
        ).alias()
    return _next_two_nodes

请注意,该函数将结果缓存在一个全局变量中,因此连续调用总是返回相同的对象,而不是使用新的别名。这是我尝试在关系中使用这个select

class Node(Base):
    __tablename__ = 'node'
    id = Column(Integer, primary_key=True)
    label = Column(String, unique=True)

    next_two_nodes = relationship(
        "Node", secondary=next_two_nodes_select,
        primaryjoin=(lambda: foreign(Node.id) 
                     == remote(next_two_nodes_select().c.left_node_id)),
        secondaryjoin=(lambda: foreign(next_two_nodes_select().c.right_node_id)
                       == remote(Node.id)),
        backref="previous_two_nodes",
        viewonly=True
    )

    def __repr__(self):
        return "Node(id={}, Label={})".format(self.id, self.label)

一些测试数据:

nodes = [
    Node(id=1, label="Node1"),
    Node(id=2, label="Node2"),
    Node(id=3, label="Node3"),
    Node(id=4, label="Node4")
]
session.add_all(nodes)
session.commit()

通过实例访问关系按预期工作:

>>> node = session.query(Node).filter_by(id=2).one()
>>> node.next_two_nodes
[Node(id=3, Label=Node3), Node(id=4, Label=Node4)]
>>> node.previous_two_nodes
[Node(id=1, Label=Node1)]

但是,对关系进行过滤并没有给出预期的结果:

>>> session.query(Node).join(NodeAlias, Node.next_two_nodes).filter(NodeAlias.id == 3).all()
[Node(id=1, Label=Node1),
 Node(id=2, Label=Node2),
 Node(id=3, Label=Node3),
 Node(id=4, Label=Node4)]

我希望只返回 Node1Node2。确实,join的SQL语句是错误的:

>>> print(session.query(Node).join(NodeAlias, Node.next_two_nodes))
SELECT node.id AS node_id, node.label AS node_label 
FROM node JOIN (SELECT leftside.id AS left_node_id, rightside.id AS right_node_id 
    FROM node AS leftside JOIN node AS rightside
    ON leftside.id + 1 = rightside.id OR leftside.id + 2 = rightside.id) AS anon_1
ON anon_1.left_node_id = anon_1.left_node_id
JOIN node AS node_1 ON anon_1.right_node_id = node_1.id

与上面的工作示例相比,它应该清楚地显示为 ON node.id = anon_1.left_node_id,而不是 ON anon_1.left_node_id = anon_1.left_node_id。我的primaryjoin 似乎是错误的,但我不知道如何连接最后一个点。

【问题讨论】:

    标签: sqlite sqlalchemy many-to-many self-referencing-table


    【解决方案1】:

    经过更多调试,我发现 "Clause Adaption" 正在替换我的 ON 子句。我不确定细节,但出于某种原因,sqlalchemy 认为我指的是来自selectnode.id,而不是来自原始的Node 表。我发现抑制子句适应的唯一方法是选择文本形式:

    select(
        [literal_column("leftside.id").label("left_node_id"),
         literal_column("rightside.id").label("right_node_id")]
    )...
    

    这样与Node 的关系被破坏并且过滤按预期工作。感觉就像是具有不可预见的副作用的 hack,也许有人知道更清洁的方法......

    【讨论】:

      猜你喜欢
      • 2021-05-31
      • 2023-03-23
      • 1970-01-01
      • 1970-01-01
      • 2018-10-02
      • 2014-09-30
      • 2016-08-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多