【问题标题】:SQLAlchemy relationship with secondary table joining behaviour changes between lazy and eager loadingSQLAlchemy 与辅助表连接行为的关系在延迟加载和急切加载之间发生变化
【发布时间】:2018-04-29 20:36:39
【问题描述】:

我已经用 SQL Alchemy 玩了几个月了,到目前为止,我对它印象深刻。

我现在遇到的一个问题似乎是一个错误,但我不确定我做的是否正确。我们在这里使用 MS SQL,通过表反射来定义表类,但是我可以使用内存中的 SQLite 数据库来复制问题,我在此处包含了代码。

我正在做的是使用它们之间的链接表来定义两个表之间的多对多关系。链接表包含一条额外的信息,我想用它来过滤链接,需要对关系使用 primaryjoin 语句。这非常适合延迟加载,但是出于性能原因,我们需要急切加载,而这一切都失败了。

如果我定义了延迟加载的关系:

activefunds = relationship('Fund', secondary='fundbenchmarklink',
                           primaryjoin='and_(FundBenchmarkLink.isactive==True,'
                                       'Benchmark.id==FundBenchmarkLink.benchmarkid,'
                                       'Fund.id==FundBenchmarkLink.fundid)')

并正常查询数据库:

query = session.query(Benchmark)

我需要的行为正是我想要的,尽管性能真的很差,因为在遍历所有基准及其各自的资金时需要额外的 SQL 查询。

如果我用预加载来定义关系:

activefunds = relationship('Fund', secondary='fundbenchmarklink',
                           primaryjoin='and_(FundBenchmarkLink.isactive==True,'
                                       'Benchmark.id==FundBenchmarkLink.benchmarkid,'
                                       'Fund.id==FundBenchmarkLink.fundid)',
                           lazy='joined')

并正常查询数据库:

query = session.query(Benchmark)

它在我脸上爆炸:

sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: fund.id 
    [SQL: 'SELECT benchmark.id AS benchmark_id,
                   benchmark.name AS benchmark_name,
                   fund_1.id AS fund_1_id,
                   fund_1.name AS fund_1_name,
                   fund_2.id AS fund_2_id,
                   fund_2.name AS fund_2_name 
            FROM benchmark
            LEFT OUTER JOIN (fundbenchmarklink AS fundbenchmarklink_1
                             JOIN fund AS fund_1 ON fund_1.id = fundbenchmarklink_1.fundid) ON benchmark.id = fundbenchmarklink_1.benchmarkid
            LEFT OUTER JOIN (fundbenchmarklink AS fundbenchmarklink_2
                             JOIN fund AS fund_2 ON fund_2.id = fundbenchmarklink_2.fundid) ON fundbenchmarklink_2.isactive = 1
            AND benchmark.id = fundbenchmarklink_2.benchmarkid
            AND fund.id = fundbenchmarklink_2.fundid']

上面的 SQL 清楚地表明,在尝试从中访问列之前,链接表没有被连接。

如果我查询数据库,特别是加入链接表:

query = session.query(Benchmark).join(FundBenchmarkLink, Fund, isouter=True)

它有效,但这意味着我现在必须确保每当我查询 Benchmark 表时,我总是必须定义连接以添加两个额外的表。

我有什么遗漏吗,这是一个潜在的错误,还是仅仅是库的工作方式?

复制问题的完整工作示例代码:

import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger('sqlalchemy.engine.base').setLevel(logging.INFO)

from sqlalchemy import Column, DateTime, String, Integer, Boolean, ForeignKey, create_engine
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class FundBenchmarkLink(Base):
    __tablename__ = 'fundbenchmarklink'

    fundid = Column(Integer, ForeignKey('fund.id'), primary_key=True, autoincrement=False)
    benchmarkid = Column(Integer, ForeignKey('benchmark.id'), primary_key=True, autoincrement=False)
    isactive = Column(Boolean, nullable=False, default=True)

    fund = relationship('Fund')
    benchmark = relationship('Benchmark')

    def __repr__(self):
        return "<FundBenchmarkLink(fundid='{}', benchmarkid='{}', isactive='{}')>".format(self.fundid, self.benchmarkid, self.isactive)


class Benchmark(Base):
    __tablename__ = 'benchmark'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)

    funds = relationship('Fund', secondary='fundbenchmarklink', lazy='joined')

    # activefunds has additional filtering on the secondary table, requiring a primaryjoin statement.
    activefunds = relationship('Fund', secondary='fundbenchmarklink',
                               primaryjoin='and_(FundBenchmarkLink.isactive==True,'
                                           'Benchmark.id==FundBenchmarkLink.benchmarkid,'
                                           'Fund.id==FundBenchmarkLink.fundid)',
                               lazy='joined')

    def __repr__(self):
        return "<Benchmark(id='{}', name='{}')>".format(self.id, self.name)


class Fund(Base):
    __tablename__ = 'fund'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)

    def __repr__(self):
        return "<Fund(id='{}', name='{}')>".format(self.id, self.name)


if '__main__' == __name__:
    engine = create_engine('sqlite://')
    Base.metadata.create_all(engine)
    maker = sessionmaker(bind=engine)

    session = maker()

    # Create some data
    for bmkname in ['foo', 'bar', 'baz']:
        bmk = Benchmark(name=bmkname)
        session.add(bmk)

    for fname in ['fund1', 'fund2', 'fund3']:
        fnd = Fund(name=fname)
        session.add(fnd)

    session.add(FundBenchmarkLink(fundid=1, benchmarkid=1))
    session.add(FundBenchmarkLink(fundid=2, benchmarkid=1))
    session.add(FundBenchmarkLink(fundid=1, benchmarkid=2))
    session.add(FundBenchmarkLink(fundid=2, benchmarkid=2, isactive=False))

    session.commit()

    # This code snippet works when activefunds doesn't exist, or doesn't use eager loading
    # query = session.query(Benchmark)
    # print(query)

    # for bmk in query:
    #     print(bmk)
    #     for fund in bmk.funds:
    #         print('\t{}'.format(fund))

    # This code snippet works for activefunds with eager loading
    query = session.query(Benchmark).join(FundBenchmarkLink, Fund, isouter=True)
    print(query)

    for bmk in query:
        print(bmk)
        for fund in bmk.activefunds:
            print('\t{}'.format(fund))

【问题讨论】:

标签: python sql-server database sqlite sqlalchemy


【解决方案1】:

我认为您已经将primary joinsecondary join 混合了一点。目前,您的主要内容似乎包含两者。删除 Fund 的谓词,它应该可以工作:

activefunds = relationship(
    'Fund',
    secondary='fundbenchmarklink',
    primaryjoin='and_(FundBenchmarkLink.isactive==True,'
                'Benchmark.id==FundBenchmarkLink.benchmarkid)',
    lazy='joined')

您的显式连接似乎可以修复查询的原因是它在隐式急切加载连接之前引入了表fund,因此他们可以引用它。这并不是真正的修复,而是隐藏了错误。如果你真的想使用显式的Query.join() 和eagerloading,用contains_eager() 通知查询。请注意您选择包含的关系,具体取决于相关查询;如果没有额外的过滤,你也可以用 inactive 填充activefunds

最后,考虑使用Query.outerjoin() 而不是Query.join(..., isouter=True)

【讨论】:

  • 完美,谢谢。我想我一定是做错了什么,只是无法选择它是什么。
  • 实际上,contains_eager 对我们来说可能是一个更好的解决方案,感谢您的建议(我们的实时环境正在链接最好描述为大约 6 个表的章鱼,并且每次都必须查询所有表有点重)
  • 顺便说一句,如果您有 很多 个关系,您希望在单个查询中加载,您可以探索除了联合加载之外的其他加载技术。 Select-in loading 看起来很有趣。
猜你喜欢
  • 2015-09-30
  • 1970-01-01
  • 2018-05-15
  • 2011-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-03
相关资源
最近更新 更多