sqlalchemy 自引用多对多与 "select" 作为关联 table
sqlalchemy self-referencing many-to-many with "select" as association table
问题描述
我正在使用 sqlalchemy (v1.2) 声明式,我有一个简单的 class Node
,带有一个 id 和一个标签。我想建立一个自引用多对多关系,其中关联 table 不是数据库 table,而是动态 select
语句。此语句 selects 来自 Node
和 returns 行的两个连接别名 (left_id, right_id)
,定义了关系。如果我通过实例对象访问关系,那么我目前拥有的代码可以工作,但是当我尝试按关系进行过滤时,连接会搞砸。
"classical" 自引用多对多关系
作为参考,让我们从 Self-Referential Many-to-Many Relationship 文档中的示例开始,该示例使用关联 table:
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
一切看起来都很好。
通过关联select语句的多对多关系
作为示例,我们实现了一个关系 next_two_nodes
,它将一个节点连接到具有 id+1
和 id+2
(如果存在)的两个节点。 complete code 用于测试。
这是一个为 "dynamic" 关联生成 select 语句的函数 table:
_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
请注意,该函数将结果缓存在一个全局变量中,因此连续调用总是 return 同一个对象,而不是使用新的别名。这是我在关系中使用此 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)]
我预计只有 Node1
和 Node2
会被 return 编辑。事实上,连接的 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 anon_1.left_node_id = anon_1.left_node_id
应该清楚地显示为 ON node.id = anon_1.left_node_id
。我的 primaryjoin
似乎是错误的,但我不知道如何连接最后的点。
经过更多调试后,我发现 "Clause Adaption" 正在替换我的 ON 子句。我不确定细节,但对于一些 reasen sqlalchemy 认为我指的是 select
中的 node.id
而不是原始 Node
table 中的 node.id
。我发现抑制子句适应的唯一方法是 select 文本形式:
select(
[literal_column("leftside.id").label("left_node_id"),
literal_column("rightside.id").label("right_node_id")]
)...
这样一来,与 Node
的关系就被打破了,过滤工作如期进行。感觉像是一个具有不可预见的副作用的 hack,也许有人知道更简洁的方法...
问题描述
我正在使用 sqlalchemy (v1.2) 声明式,我有一个简单的 class Node
,带有一个 id 和一个标签。我想建立一个自引用多对多关系,其中关联 table 不是数据库 table,而是动态 select
语句。此语句 selects 来自 Node
和 returns 行的两个连接别名 (left_id, right_id)
,定义了关系。如果我通过实例对象访问关系,那么我目前拥有的代码可以工作,但是当我尝试按关系进行过滤时,连接会搞砸。
"classical" 自引用多对多关系
作为参考,让我们从 Self-Referential Many-to-Many Relationship 文档中的示例开始,该示例使用关联 table:
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
一切看起来都很好。
通过关联select语句的多对多关系
作为示例,我们实现了一个关系 next_two_nodes
,它将一个节点连接到具有 id+1
和 id+2
(如果存在)的两个节点。 complete code 用于测试。
这是一个为 "dynamic" 关联生成 select 语句的函数 table:
_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
请注意,该函数将结果缓存在一个全局变量中,因此连续调用总是 return 同一个对象,而不是使用新的别名。这是我在关系中使用此 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)]
我预计只有 Node1
和 Node2
会被 return 编辑。事实上,连接的 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 anon_1.left_node_id = anon_1.left_node_id
应该清楚地显示为 ON node.id = anon_1.left_node_id
。我的 primaryjoin
似乎是错误的,但我不知道如何连接最后的点。
经过更多调试后,我发现 "Clause Adaption" 正在替换我的 ON 子句。我不确定细节,但对于一些 reasen sqlalchemy 认为我指的是 select
中的 node.id
而不是原始 Node
table 中的 node.id
。我发现抑制子句适应的唯一方法是 select 文本形式:
select(
[literal_column("leftside.id").label("left_node_id"),
literal_column("rightside.id").label("right_node_id")]
)...
这样一来,与 Node
的关系就被打破了,过滤工作如期进行。感觉像是一个具有不可预见的副作用的 hack,也许有人知道更简洁的方法...