【问题标题】:SQLAlchemy eagerly/joined loading self referential one-to-one relationshipSQLAlchemy 急切/加入加载自引用的一对一关系
【发布时间】:2017-09-15 00:55:59
【问题描述】:

我有一个自引用的一对一关系(使用uselist=False),我试图在查询时急切地加载next_node。如果我删除 uselist=False 并将 next_node 作为检测列表访问,则该列表会正确加载到原始查询中。这是一个已知问题,还是我在尝试急切地加载 next_node 反向引用关系时做错了什么?

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
    backref, joinedload, relationship, scoped_session, sessionmaker)
from sqlalchemy import create_engine, Column, Integer, ForeignKey, MetaData

class Node(declarative_base()):
    __tablename__ = 'nodes'

    id = Column(Integer, primary_key=True)
    value = Column(Integer)
    prev_node_id = Column(Integer, ForeignKey('nodes.id'), unique=True)
    prev_node = relationship(
        'Node',
        uselist=False,
        remote_side=[id],
        backref=backref('next_node', uselist=False))

engine = create_engine('sqlite://', echo=True)
metadata = MetaData()
with engine.begin() as connection:
    metadata = Node.metadata
    metadata.create_all(connection)

session = scoped_session(sessionmaker(bind=engine))

a = Node()
session.add(a)
b = Node()
session.add(b)
b.prev_node = a
session.commit()

query = session.query(Node)
query = query.options(joinedload(Node.next_node))
print("executing joined query")
for n in query:
    if n.next_node is not None:
        print(n.id, n.next_node.id)
print("done!")

两个选择语句的结果:

...
executing joined query
2017-04-19 01:30:54,514 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2017-04-19 01:30:54,515 INFO sqlalchemy.engine.base.Engine SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id, nodes_1.id AS nodes_1_id, nodes_1.value AS nodes_1_value, nodes_1.prev_node_id AS nodes_1_prev_node_id 
FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.prev_node_id
2017-04-19 01:30:54,515 INFO sqlalchemy.engine.base.Engine ()
(1, 2)
2017-04-19 01:30:54,517 INFO sqlalchemy.engine.base.Engine SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id 
FROM nodes 
WHERE ? = nodes.prev_node_id
2017-04-19 01:30:54,517 INFO sqlalchemy.engine.base.Engine (2,)
done!

但是,如果我确实使用uselist=False 标志,我会得到预期的单个查询:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
    backref, joinedload, relationship, scoped_session, sessionmaker)
from sqlalchemy import create_engine, Column, Integer, ForeignKey, MetaData

class Node(declarative_base()):
    __tablename__ = 'nodes'

    id = Column(Integer, primary_key=True)
    value = Column(Integer)
    prev_node_id = Column(Integer, ForeignKey('nodes.id'), unique=True)
    prev_node = relationship(
        'Node',
        uselist=False,
        remote_side=[id],
        backref=backref('next_node'))

engine = create_engine('sqlite://', echo=True)
metadata = MetaData()
with engine.begin() as connection:
    metadata = Node.metadata
    metadata.create_all(connection)

session = scoped_session(sessionmaker(bind=engine))

a = Node()
session.add(a)
b = Node()
session.add(b)
b.prev_node = a
session.commit()

query = session.query(Node)
query = query.options(joinedload(Node.next_node))
print("executing joined query")
for n in query:
    if n.next_node is not None and len(n.next_node) > 0:
        print(n.id, n.next_node[0].id)
print("done!")

导致单个选择语句:

...
executing joined query
2017-04-19 01:29:30,570 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2017-04-19 01:29:30,571 INFO sqlalchemy.engine.base.Engine SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id, nodes_1.id AS nodes_1_id, nodes_1.value AS nodes_1_value, nodes_1.prev_node_id AS nodes_1_prev_node_id 
FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.prev_node_id
2017-04-19 01:29:30,571 INFO sqlalchemy.engine.base.Engine ()
(1, 2)
done!

【问题讨论】:

    标签: python sqlalchemy


    【解决方案1】:

    关系配置缺少参数join_depth

    如果设置为join_depth=0,即

    prev_node = orm.relationship(
        'Node', uselist=False,
        remote_side=[id], join_depth=0, 
        backref=orm.backref('next_node', uselist=False)
    )
    

    那么你的第一个代码块应该在单个查询中预先加载 next_node。

    编辑:添加了可执行的 python 脚本和输出。

    在 sqlalchemy 1.1.8、python 3.6.0 和 OS X El Capitan 上测试

    # eager.py
    import sqlalchemy.ext.declarative as dec
    import sqlalchemy as sa
    import sqlalchemy.orm as orm
    
    Base = dec.declarative_base()
    
    class Node(Base):
      __tablename__ = 'nodes'
      id = sa.Column(sa.Integer, primary_key=True)
      value = sa.Column(sa.Integer)
      prev_node_id = sa.Column(sa.Integer, sa.ForeignKey('nodes.id'), unique=True)
      prev_node = orm.relationship(
        'Node', uselist=False,
        remote_side=[id],
        join_depth=0, backref=orm.backref('next_node', uselist=False)
      )
    
    print('setup database & schema')
    engine = sa.create_engine('sqlite://', echo=True)
    Base.metadata.create_all(engine)
    
    session = orm.scoped_session(orm.sessionmaker(bind=engine))
    a = Node()
    b = Node()
    b.prev_node = a
    print("inserting nodes")
    session.add(a)
    session.add(b)
    session.commit()
    query = session.query(Node)
    query = query.options(orm.joinedload(Node.next_node))
    
    print('string representation of query:')
    print(str(query))
    print('*'*80)
    print('executing joined query')
    
    for n in query:
      if n.next_node is not None:
        print(n.id, n.next_node.id)
    print('done!')
    

    而且,此脚本产生以下输出:

    # output of ``$ python eager.py``
    setup database & schema
    2017-04-19 22:23:36,491 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
    2017-04-19 22:23:36,491 INFO sqlalchemy.engine.base.Engine ()
    2017-04-19 22:23:36,492 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
    2017-04-19 22:23:36,492 INFO sqlalchemy.engine.base.Engine ()
    2017-04-19 22:23:36,492 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("nodes")
    2017-04-19 22:23:36,493 INFO sqlalchemy.engine.base.Engine ()
    2017-04-19 22:23:36,493 INFO sqlalchemy.engine.base.Engine
    CREATE TABLE nodes (
        id INTEGER NOT NULL,
        value INTEGER,
        prev_node_id INTEGER,
        PRIMARY KEY (id),
        UNIQUE (prev_node_id),
        FOREIGN KEY(prev_node_id) REFERENCES nodes (id)
    )
    
    
    2017-04-19 22:23:36,493 INFO sqlalchemy.engine.base.Engine ()
    2017-04-19 22:23:36,494 INFO sqlalchemy.engine.base.Engine COMMIT
    inserting nodes
    2017-04-19 22:23:36,497 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
    2017-04-19 22:23:36,498 INFO sqlalchemy.engine.base.Engine INSERT INTO nodes (value, prev_node_id) VALUES (?, ?)
    2017-04-19 22:23:36,498 INFO sqlalchemy.engine.base.Engine (None, None)
    2017-04-19 22:23:36,498 INFO sqlalchemy.engine.base.Engine INSERT INTO nodes (value, prev_node_id) VALUES (?, ?)
    2017-04-19 22:23:36,499 INFO sqlalchemy.engine.base.Engine (None, 1)
    2017-04-19 22:23:36,499 INFO sqlalchemy.engine.base.Engine COMMIT
    string representation of query:
    SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id, nodes_1.id AS nodes_1_id, nodes_1.value AS nodes_1_value, nodes_1.prev_node_id AS nodes_1_prev_node_id
    FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.prev_node_id
    ********************************************************************************
    executing joined query
    2017-04-19 22:23:36,501 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
    2017-04-19 22:23:36,501 INFO sqlalchemy.engine.base.Engine SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id, nodes_1.id AS nodes_1_id, nodes_1.value AS nodes_1_value, nodes_1.prev_node_id AS nodes_1_prev_node_id
    FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.prev_node_id
    2017-04-19 22:23:36,501 INFO sqlalchemy.engine.base.Engine ()
    1 2
    done!
    

    从日志中可以看出,数据库上只运行了 1 个查询:

    SELECT nodes.id AS nodes_id, nodes.value AS nodes_value, nodes.prev_node_id AS nodes_prev_node_id, nodes_1.id AS nodes_1_id, nodes_1.value AS nodes_1_value, nodes_1.prev_node_id AS nodes_1_prev_node_id
    FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.prev_node_id
    

    【讨论】:

    • 我按照你的建议添加了join_depth=0,我的易于运行的示例仍然发出两个选择语句。
    • 我添加了用于检查我的建议是否有效的脚本,以及它在我的机器上产生的输出。
    • 谢谢! join_depth 不是必需的,但您的回答指出我要检查我正在使用的 SQLAlchemy 的版本(在我的情况下为 1.0.14)。升级后,脚本按预期工作。再次感谢!
    猜你喜欢
    • 2014-10-13
    • 1970-01-01
    • 2016-04-25
    • 2021-12-01
    • 1970-01-01
    • 2014-02-24
    • 1970-01-01
    • 2018-05-15
    • 2013-10-07
    相关资源
    最近更新 更多