如何在 SQLAlchemy 中声明对称自引用多对多关系以存储有向图

How to declare symmetric self-referential many-to-many relationship in SQLAlchemy for storing directed graph

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

__all__ = [ "GraphNode" ]

Base = declarative_base()

graph_edges = Table(
    'graph_edges', Base.metadata,
    Column('from_node_id', Integer, ForeignKey('graph_nodes.id'), primary_key=True),
    Column('to_node_id', Integer, ForeignKey('graph_nodes.id'), primary_key=True)
)


class GraphNode(Base):
    __tablename__ = 'graph_nodes'
    id = Column(Integer, primary_key=True, autoincrement=False)
    node_data = Column(String(50), nullable=False)

    from_me = relationship("GraphNode", secondary=graph_edges,
                           primaryjoin=id==graph_edges.c.from_node_id,
                           secondaryjoin=id==graph_edges.c.to_node_id)
    to_me = relationship("GraphNode", secondary=graph_edges,
                         primaryjoin=id==graph_edges.c.to_node_id,
                         secondaryjoin=id==graph_edges.c.from_node_id)

当我这样做时,我从 SQLAlchemy 得到这个错误:

SAWarning: relationship 'GraphNode.to_me' will copy column graph_nodes.id to column graph_edges.from_node_id, which conflicts with relationship(s): 'GraphNode.from_me' (copies graph_nodes.id to graph_edges.from_node_id). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards. To silence this warning, add the parameter 'overlaps="from_me"' to the 'GraphNode.to_me' relationship. (Background on this error at: https://sqlalche.me/e/14/qzyx)

听起来 SQLAlchemy 注意到将某些内容放入一个节点的 from_me 列表将导致对其他节点的 to_me 列表的更改。这是期望的行为。我希望能够向后和向前遍历链接。

有没有什么方法可以在 SQLAlchemy 不向我抱怨的情况下正确地做到这一点?

这是解决问题的方法。解决方案有点在错误消息中,我不完全确定它为什么起作用。但是,这里是:

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

__all__ = [ "GraphNode" ]

Base = declarative_base()

graph_edges = Table(
    'graph_edges', Base.metadata,
    Column('from_node_id', Integer, ForeignKey('graph_nodes.id'), primary_key=True),
    Column('to_node_id', Integer, ForeignKey('graph_nodes.id'), primary_key=True)
)


class GraphNode(Base):
    __tablename__ = 'graph_nodes'
    id = Column(Integer, primary_key=True, autoincrement=False)
    node_data = Column(String(50), nullable=False)

    from_me = relationship("GraphNode", secondary=graph_edges,
                           primaryjoin=id==graph_edges.c.from_node_id,
                           secondaryjoin=id==graph_edges.c.to_node_id,
                           back_populates="to_me")
    to_me = relationship("GraphNode", secondary=graph_edges,
                         primaryjoin=id==graph_edges.c.to_node_id,
                         secondaryjoin=id==graph_edges.c.from_node_id,
                         back_populates="from_me")

基本上,我为每个关系添加了 back_populates 参数。达到了我想要的效果。

我真希望我能更好地了解发生了什么。我觉得我更像是在遵循一本食谱,而不是从食材开始,然后决定我想用它们做什么样的饭菜。我在编程时总是讨厌这样做,因为我不会理解什么是关键的,什么不是,并且做出一个微小的改变,结果证明是非常重要的。