可以让SQLAlchemy在不重复原始查询的情况下执行子查询预加载吗?
假设我们有一个原始生成的查询,像这样:
SELECT company.x AS company_x, ...
FROM company
LEFT OUTER JOIN acc ON acc.id = company.acc
LEFT OUTER JOIN usercomp_links ON company.id = usercomp_links.pid
LEFT OUTER JOIN usergro_links ON acc.id = usergro_links.pid
WHERE usergro_links.eid = %s OR usercomp_links.eid = %s
如果我们在这个查询后面加上 .options(subqueryload(Company.childs))
,结果就会变成:
SELECT company.x AS company_x, ..., anon_1.company_id AS anon_1_company_id
FROM (
SELECT company.id AS company_id
FROM company
LEFT OUTER JOIN acc ON acc.id = company.acc
LEFT OUTER JOIN usercomp_links ON company.id = usercomp_links.pid
LEFT OUTER JOIN usergro_links ON acc.id = usergro_links.pid
WHERE usergro_links.eid = %s OR usercomp_links.eid = %s) AS anon_1
INNER JOIN acel_links AS acel_links_1 ON anon_1.company_id = acel_links_1.eid
INNER JOIN company ON company.id = acel_links_1.pid ORDER BY anon_1.company_id
但是这样会非常慢。如果我从第一个查询中获取公司ID,然后手动加载所有子公司,速度会快得多,和这种情况比起来简直是飞快。
我看过文档,也看了代码,但我不知道怎么告诉sqlalchemy只从第一个查询的结果中获取ID,然后用一个相对简单的查询来加载子公司。
我不只是依赖这个例子,我遇到过更复杂的情况,sqlalchemy根本无法加载构建好的查询。那为什么要重复做第一次查询的工作呢?
所以,有人知道怎么在不使用自动构建的“连接再连接”的方式下进行急切加载吗?
4 个回答
我在SQLAlchemy的邮件列表上发了个帖子,内容可以在这里找到:https://groups.google.com/d/msg/sqlalchemy/8-kHuliJpr8/PHUZLLtMEQAJ
Boris Serebrov提到的“in”加载方式默认似乎只支持单向操作。如果你不是在使用急切加载(eager loading),那么当你从“一”方访问“一对多”关系时,它仍然会执行查询。
我最后找到了解决办法,可以查看这里:https://gist.github.com/pawl/df5ba8923d9929dd1f4fc4e683eced40
你可以选择使用抽象的 ORM 层,这样的话,你需要把孩子属性建模为与 ORM 的 关系,大概是这样的:
from sqlalchemy.orm import relationship
children = relationship("<name of the acl_links class>", lazy="joined")
使用 lazy="joined"
会导致急切加载,这正是你想要的(这和 @vsminkov 提到的 joinedload 是一样的),文档中提到:
任何 relationship() 的默认加载策略都是通过 lazy 这个参数来配置的……在这里我们把它设置为 joined,这样孩子的关系就会通过 JOIN 被急切加载。
在定义关系时,你可以进行很多调整,所以可以查看文档,充分利用这些功能。
或者你也可以选择使用 查询 API,根据自己的需求来组合查询,比如,做一个简单的第二个 where-in 查询,例如:
list_of_ids_previously_loaded_companies = <information from your previous query>
the_session.query(<name of the acl_links class>).filter(<name of the acl_links class>.eid.in_(list_of_ids_previously_loaded_companies)
你甚至可以更深入地使用 表达式语言,像这样:
q = select([acl_links]).where(acl_links.c.eid.in_(list_of_ids_previously_loaded_companies))
the_session.execute(q).fetchall()
作为最后的选择,你可以完全使用 原始 SQL:
from sqlalchemy import text
children_results = a_db_connection.execute(text(<SQL STATEMENT STRING>).fetchall()
更新:现在在SQLAlchemy中已经实现了“select in”策略(自版本1.2起):请查看文档中的Select IN加载。
简而言之:
我认为在可能的情况下应该使用joinedload
策略,因为它比其他策略更高效,包括问题中提到的使用“IN”语句加载相关数据的策略。
实际上,“IN”策略可以很容易地在SQLAlchemy之外实现(见下面的代码),而且将其作为新的加载策略实现应该也不复杂(因为它在逻辑上与现有的subqueryload
策略类似)。
详细版本:
我开始进行一个简单的实验,看看不同策略产生的查询。
实验的完整源代码在Github上。
我的模型看起来是这样的:
class Author(ModelBase):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True, nullable=False)
name = Column(String(255))
class Book(ModelBase):
__tablename__ = 'books'
id = Column(Integer, primary_key=True)
name = Column(String)
author_id = Column(Integer, ForeignKey('authors.id'))
author = relationship(
'Author', backref=backref('books'))
现在,进行测试,首先是懒加载:
books = session.query(Book).all()
print books[0].author.name
session.commit()
输出(已清理):
-------------Lazy--------------
sqlalchemy.engine.base.Engine:
SELECT
books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books
SELECT
authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(1,)
author1
如预期的那样,懒加载会运行一个查询来获取书籍,每次访问作者时又会运行一个查询。
子查询加载:
books = session.query(Book).options(subqueryload(Book.author)).all()
print books[0].author.name
session.commit()
-------------Subquery----------
SELECT
books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books
SELECT
authors.id AS authors_id, authors.name AS authors_name,
anon_1.books_author_id AS anon_1_books_author_id
FROM (
SELECT DISTINCT books.author_id AS books_author_id
FROM books) AS anon_1
JOIN authors
ON authors.id = anon_1.books_author_id
ORDER BY anon_1.books_author_id
author1
对于子查询,我们有两个查询,第一个获取书籍,第二个通过子查询获取作者。
连接加载:
books = session.query(Book).options(joinedload(Book.author)).all()
print books[0].author.name
session.commit()
-------------Joined------------
SELECT
books.id AS books_id, books.name AS books_name,
books.author_id AS books_author_id,
authors_1.id AS authors_1_id, authors_1.name AS authors_1_name
FROM books
LEFT OUTER JOIN authors AS authors_1 ON authors_1.id = books.author_id
author1
连接策略只运行一个查询来获取书籍和作者。
立即加载:
books = session.query(Book).options(immediateload(Book.author)).all()
print books[0].author.name
session.commit()
-------------Immediate---------
SELECT
books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books
SELECT
authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(1,)
SELECT authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(2,)
author1
而immediate
策略会在第一次查询中加载书籍,然后当我们尝试访问关系时,会为每个相关记录分别运行查询来获取所有相关数据。
看起来在大多数情况下,“joinedload()”应该是最有效的(而且比“IN”策略更高效)——我们只需通过一个查询就能获取所有数据。
现在,让我们尝试在SQLAlchemy之外实现IN策略:
print '-------------IN----------------'
books = session.query(Book).all()
ids = set()
for b in books:
ids.add(b.author_id)
authors = session.query(Author).filter(Author.id.in_(ids)).all()
print books[0].author.name
print books[1].author.name
print books[2].author.name
print books[3].author.name
输出:
-------------IN----------------
SELECT
books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books
SELECT authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id IN (?, ?)
INFO:sqlalchemy.engine.base.Engine:(1, 2)
author1
author1
author2
author2
如我们所见,它运行了两个查询,然后我们可以访问所有作者。
请注意,我们并没有明确将作者与书籍连接,但当我们尝试通过书籍访问作者时,它仍然有效,因为SQLAlchemy在内部身份映射中找到了作者记录,并没有运行额外的数据库查询。
与上述“IN”策略代码类似的内容可以被概括为一个函数,可以与任何模型/关系一起使用。而且,可能将“IN”策略作为新的SQLAlchemy策略实现相对简单,它与现有的subqueryloading
类似——它也应该运行第二个查询来获取相关数据。
这是关于SQLAlchemy的内容,SQLAlchemy是一个用来处理数据库的工具。最近,它增加了一个新的功能,你现在可以使用一种叫做 selectinload
的方法来加载数据。