在SQLAlchemy中加入outerjoin之后加入

时间:2015-10-01 01:04:52

标签: sql postgresql join sqlalchemy outer-join

假设我有一对多关系,父母和子女按一些group_id分组。

  

注意:此示例是我的代码的精简版本,实际上是多对多关系。可能存在与该问题无关的一些错误。

class Node(Base):
    __tablename__ = 'node'
    id = Column(GUID, default=uuid.uuid4, primary_key=True)
    group_id = Column(GUID, nullable=False, primary_key=True)
    parent_id = Column(GUID)
    title = Column(Text, nullable=False)

class Leaf(Base):
    __tablename__ = 'leaf'
    id = Column(GUID, nullable=False, primary_key=True)
    group_id = Column(GUID, nullable=False, primary_key=True)
    parent_id = Column(GUID, nullable=False)

group_id用作创建新版本的方式 - 因此具有相同id的节点和树叶可以存在于多个组中。

我想要做的是比较两组,找到父母已经改变的所有叶子。我试图使用外连接进行比较,然后使用两个连接来过滤父节点:

def find_changed_leaves(group_id_a, group_id_b, session):
    NodeA = model.Node
    NodeB = aliased(model.Node, name='node_b')
    LeafA = model.Leaf
    LeafB = aliased(model.Leaf, name='leaf_b')

    query = (session.query(LeafA, LeafB)
        .outerjoin(LeafB, LeafA.id == LeafB.id)

        .join(NodeA, (LeafA.group_id == NodeA.group_id) &
                     (LeafA.parent_id == NodeA.id))
        .join(NodeB, (LeafB.group_id == NodeB.group_id) &
                     (LeafB.parent_id == NodeB.id))

        # Group membership
        .filter(LeafA.group_id == group_id_a,
                LeafB.group_id == group_id_b)

        # Filter for modified parents
        .filter(NodeA.title != NodeB.title)
    )

    return query.all()

这样可行,但它不会显示仅在其中一个组中的叶子(例如,如果将叶子添加到新组中的节点)。如何显示所有叶子,返回None以查找其中一个组中缺少的叶子?

编辑:我看到有perils mixing join with outer join。我尝试天真地将其更改为.outerjoin(NodeA, ...,但它没有帮助。

1 个答案:

答案 0 :(得分:1)

正如评论中所提到的,尚不完全清楚需要实现的目标。尽管如此,下面的代码至少应该为您提供一些指导。

首先,我不会尝试将它全部合并到一个查询中(可能使用完整联接和子查询),但将其拆分为3个单独的查询:

  1. 获取LeafA, LeafB其中'父母已经改变了
  2. 获取LaefA没有相应的LeafB
  3. 获取LaefB没有相应的LeafA
  4. 以下代码应该在sqlitepostgresql中运行。请注意,我已添加关系并在查询中使用它们。但是,您可以使用显式连接条件执行相同的操作,如代码段中那样。

    import uuid
    
    from sqlalchemy import (
        create_engine, Column, Integer, String, ForeignKey, Text, and_,
        ForeignKeyConstraint, UniqueConstraint, exists
    )
    from sqlalchemy.orm import sessionmaker, relationship, eagerload, aliased
    from sqlalchemy.ext.declarative import declarative_base, declared_attr
    from sqlalchemy.dialects.postgresql import UUID as GUID
    
    _db_uri = 'sqlite:///:memory:'; GUID = String
    # _db_uri = "postgresql://aaa:bbb@localhost/mytestdb"
    engine = create_engine(_db_uri, echo=True)
    Session = sessionmaker(bind=engine)
    Base = declarative_base(engine)
    
    newid = lambda: str(uuid.uuid4())
    
    # define object model
    class Node(Base):
        __tablename__ = 'node'
        id = Column(GUID, default=newid, primary_key=True)
        group_id = Column(GUID, nullable=False, primary_key=True)
        # parent_id = Column(GUID)
        title = Column(Text, nullable=False)
    
    
    class Leaf(Base):
        __tablename__ = 'leaf'
        id = Column(GUID, nullable=False, primary_key=True)
        group_id = Column(GUID, nullable=False, primary_key=True)
        parent_id = Column(GUID, nullable=False)
        title = Column(Text, nullable=False)
    
        # define relationships - easier test data creation and querying
        parent = relationship(
            Node,
            primaryjoin=and_(Node.id == parent_id, Node.group_id == group_id),
            backref="children",
        )
    
        __table_args__ = (
            ForeignKeyConstraint(
                ['parent_id', 'group_id'], ['node.id', 'node.group_id']
            ),
        )
    
    
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    
    session = Session()
    
    
    g1, g2, l1, l2, l3 = [newid() for _ in range(5)]
    
    # Create test data
    def _add_test_data():
        n11 = Node(
            title="node1", group_id=g1,
            children=[
                Leaf(id=l1, title="g1 only"),
                Leaf(id=l3, title="both groups"),
            ]
        )
    
        n21 = Node(
            title="node1 changed", group_id=g2,
            children=[
                Leaf(id=l2, title="g2 only"),
                Leaf(id=l3, title="both groups"),
            ]
        )
    
        session.add_all([n11, n21])
        session.commit()
    
    
    def find_changed_leaves(group_id_a, group_id_b):
        """
        Leaves which are in both versions, but a `title` for their parents is changed.
        """
        NodeA = aliased(Node, name='node_a')
        NodeB = aliased(Node, name='node_b')
        LeafA = aliased(Leaf, name='leaf_a')
        LeafB = aliased(Leaf, name='leaf_b')
    
        query = (
            session.query(LeafA, LeafB)
            .filter(LeafA.group_id == group_id_a)
            # @note: group membership for LeafB is part of join now
            .join(LeafB, (LeafA.id == LeafB.id) & (LeafB.group_id == group_id_b))
    
            .join(NodeA, LeafA.parent)
            .join(NodeB, LeafB.parent)
    
            # Filter for modified parents
            .filter(NodeA.title != NodeB.title)
        )
        return query.all()
    
    
    def find_orphaned_leaves(group_id_a, group_id_b):
        """
        Leaves found in group A, but not in group B.
        """
        LeafA = aliased(Leaf, name='leaf_a')
        LeafB = aliased(Leaf, name='leaf_b')
    
        query = (
            session.query(LeafA)
            .filter(~(
                session.query(LeafB)
                .filter(LeafA.id == LeafB.id)
                .filter(group_id_b == LeafB.group_id)
                .exists()
            ))
    
            # Group membership
            .filter(LeafA.group_id == group_id_a)
        )
        return query.all()
    
    
    def find_deleted_leaves(group_id_a, group_id_b):
        a_s = find_orphaned_leaves(group_id_a, group_id_b)
        return tuple((a, None) for a in a_s)
    
    def find_added_leaves(group_id_a, group_id_b):
        b_s = find_orphaned_leaves(group_id_b, group_id_a)
        return tuple((None, b) for b in b_s)
    
    
    # add test data
    _add_test_data()
    
    # check the results
    changed = find_changed_leaves(g1, g2)
    assert 1 == len(changed)
    le, ri = changed[0]
    assert le.id == ri.id == l3
    
    added = find_added_leaves(g1, g2)
    assert 1 == len(added)
    le, ri = added[0]
    assert le is None
    assert ri.id == l2
    
    deleted = find_deleted_leaves(g1, g2)
    assert 1 == len(deleted)
    le, ri = deleted[0]
    assert le.id == l1
    assert ri is None