首页 > 解决方案 > SQLAlchemy 中如何实现优先分组?

问题描述

我一直在查看 SQLAlchemy api,它非常复杂,所以我想我会在这里询问是否有人可以以某种易于理解的格式向我解释这一点。

我正在围绕 O365 python api 编写一个包装器,用于编写 Office365 REST api 查询,其语法类似于 SQLAlchemy。

O365 提供了一个流畅的查询类,如下所示:

Message.new_query().on_attribute("subject").contains("Hello Friend!").chain("and").on_attribute("from").equals("some_address@gmail.com")

我目前有一些可以工作的东西,看起来像这样:

Message.where(Subject.contains("Hello Friend!") & (From == "some_address@gmail.com")).execute()

确切的代码并不真正相关,但简而言之,它通过为运算符实现魔术方法并添加额外的方法(例如 .contains())来构建 BooleanExpression 对象。例如:

From == "some_address@gmail.com"

将返回一个 BooleanExpression。

然后将 BooleanExpression 对象与“&”或“|”组合 返回 BooleanExpressionClause 对象的运算符,这些对象基本上是 BooleanExpression 对象的列表,用于跟踪每 2 个表达式连接的运算符。

最后, .where() 方法使用单个 BooleanExpressionClause 并在后台为它构建一个流畅的查询。

到目前为止,一切都很好。

所以我遇到的障碍涉及优先分组。

假设我想要所有带有“嗨!”的消息。地址中包含“john”或地址中包含“doe”的发件人在其主题中。如果我有这样的查询:

From.contains("john") | From.contains("doe") & Subject.contains("Hi!")

我会从地址中带有“john”的任何人那里收到每条消息,因为 Microsoft 的 API 实际上将生成的 REST 请求读取为:

From.contains("john") | (From.contains("doe") & Subject.contains("Hi!"))

当我想要的是:

(From.contains("john") | From.contains("doe")) & Subject.contains("Hi!")

但是,如果我只是使用我当前的 API 编写它,那将与完全不使用任何括号编写它没有什么不同,因为据我所知,对于 python,第一个示例(没有优先级组)和第三个例子(我想要的优先级组)看起来完全一样,因为解释器只是从左到右读取这样的子句。

这终于让我想到了我的问题。SQLAlchemy 能够以某种方式理解优先级组,但我一生都无法理解它是如何做到的。

例如:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy import engine, Column
from sqlalchemy.types import Integer, String

engine = engine("some_engine_url")
Base = declarative_base()
s = sessionmaker(bind=engine)()

class Person(Base):
    __tablename__ = "person"
    id            = Column(Integer, primary_key=True)
    name          = Column(String)
    sex           = Column(String(1))

print(s.query(Person).filter( (Person.name == "john") | (Person.name == "doe") & (Person.sex == "M") ))
print(s.query(Person).filter( ((Person.name == "john") | (Person.name == "doe")) & (Person.sex == "M") ))

这些打印语句分别返回,

SELECT person.id AS person_id, person.name AS person_name, person.sex AS person_sex 
FROM person 
WHERE person.name = ? OR person.name = ? AND person.sex = ?

SELECT person.id AS person_id, person.name AS person_name, person.sex AS person_sex 
FROM person 
WHERE (person.name = ? OR person.name = ?) AND person.sex = ?

SQLAlchemy 内部到底是如何区分这两个过滤子句的呢?据我所知,python 应该以相同的方式处理它们,但显然在我不知道的某个地方发生了一些魔法。

我怎样才能复制这种行为?

非常感谢!

标签: pythonpython-3.xsqlalchemy

解决方案


这终于让我想到了我的问题。SQLAlchemy 能够以某种方式理解优先级组,但我一生都无法理解它是如何做到的。

SQLAlchemy 在这里不需要做太多工作。大部分工作由 Python 完成,它以特定顺序解析对象。Python 根据运算符优先级的规则解析表达式,因此基于优先级以特定顺序执行组合表达式。如果该优先顺序对于您的应用程序是正确的,并且不介意总是对嵌套表达式进行分组,那么您就可以了。在 SQL 中并非总是如此,并且 SQLAlchemy 想要输出有效的 SQL 表达式,并且使用最少的无关括号,因此 SQLAlchemy 确实会参考它自己的优先表。这样它就可以决定何时(...)需要在输出中进行分组。

SQLAlchemy 返回*Clause*表示对其操作数的操作的专用表达式对象(每个操作数都可以是进一步的表达式),然后当这些操作对象也用于操作时进一步组合这些对象。最后,您将拥有一棵对象树,并在编译到 SQL 期间遍历该树,然后根据需要生成您看到的分组输出。在需要优先级的地方,SQLAlchemy 会插入sqlalchemy.sql.elements.Grouping()对象,并且由 SQL 方言来生成正确的分组语法。

如果您正在查看 SQLAlchemy 源代码,您将需要查看sqlalchemy.sql.operators.ColumnOperators该类及其父类,sqlalchemy.sql.operators.Operators它作为对(传入函数)的调用实现。__or__在 SQLAlchemy 中这看起来很复杂,因为这必须委托给不同类型的对象和 SQL 方言的不同类型的比较!self.operate(or_, other)operator.or_()

但在基础是sqlalchemy.sql.default_comparator模块,其中or_and_(间接)映射到的类方法sqlalchemy.sql.elements.BooleanClauseList,产生该类的实例。

BooleanClauseList._construct()方法负责处理分组,通过委托给.self_group()两个子句上的方法:

convert_clauses = [
    c.self_group(against=operator) for c in convert_clauses
]

这传入operator.or_or ,因此让每个操作数根据优先级operator.and_决定是否需要使用实例。Grouping()对于BooleanClauseList对象(因此... | ...or... & ...但随后与另一个or运算符组合的结果),该方法将产生一个if与 相比具有更低或相等的优先级:|&ClauseList.self_group()Grouping()self.operatoragainst

def self_group(self, against=None):
    # type: (Optional[Any]) -> ClauseElement
    if self.group and operators.is_precedent(self.operator, against):
        return Grouping(self)
    else:
        return self

wheresqlalchemy.sql.operators.is_precedent()查询表达式优先级表:

_PRECEDENCE = {
    # ... many lines elided

    and_: 3,
    or_: 2,

    # ... more lines elided
}

def is_precedent(operator, against):
    if operator is against and is_natural_self_precedent(operator):
        return False
    else:
        return _PRECEDENCE.get(
            operator, getattr(operator, "precedence", _smallest)
        ) <= _PRECEDENCE.get(against, getattr(against, "precedence", _largest))

那么你的两个表达式会发生什么?Python采用了()括号分组。让我们首先将表达式简化为基本组件,您基本上有:

A | B & C
(A | B) & C

Python 根据自己的优先规则解析这两个表达式,并生成自己的抽象语法树

>>> import ast
>>> ast.dump(ast.parse('A | B & C', mode='eval').body)
"BinOp(left=Name(id='A', ctx=Load()), op=BitOr(), right=BinOp(left=Name(id='B', ctx=Load()), op=BitAnd(), right=Name(id='C', ctx=Load())))"
>>> ast.dump(ast.parse('(A | B) & C', mode='eval').body)
"BinOp(left=BinOp(left=Name(id='A', ctx=Load()), op=BitOr(), right=Name(id='B', ctx=Load())), op=BitAnd(), right=Name(id='C', ctx=Load()))"

这些归结为

BinOp(
    left=A,
    op=or_,
    right=BinOp(left=B, op=and_, right=C)
)

BinOp(
    left=BinOp(left=A, op=or_, right=B),
    op=and_,
    right=C
)

这会改变对象组合的顺序!所以第一个导致:

# process A, then B | C

leftop = A
rightop = BooleanClauseList(and_, (B, C))

# combine into A & (B | C)
final = BooleanClauseList(or_, (leftop, rightop))

# which is
BooleanClauseList(or_, (A, BooleanClauseList(and_, (B, C))))

因为这里的第二个子句是一个BooleanClauseList(and_, ...)实例,所以.self_group()对该子句的调用不会返回一个Grouping(); 有,self.operatorand_的优先级为 3,高于而不是低于或等于or_父子句的 == 2 的优先级。

另一个表达式由 Python 以不同的顺序执行:

# process A | B, then C

leftop = BooleanClauseList(or_, (A, B))
rightop = C

# combine into (A | B) & C
final = BooleanClauseList(and_, (leftop, rightop))

# which is
BooleanClauseList(and_, (BooleanClauseList(or_, (A, B)), C))

现在第一个子句是一个BooleanClauseList(or_, ...)实例,它实际上产生了一个Grouping实例,因为self.operatorisor_和 that 从父子句列表中具有较低的优先级and_,因此对象树变为:

BooleanClauseList(and_, (Grouping(BooleanClauseList(or_, (A, B))), C))

现在,如果您要做的只是确保您的表达式按正确的顺序分组,那么您实际上不需要注入自己的Grouping()对象。and_(or_(A, B), C)是否处理或何时通过遍历处理对象树并不重要and_((or_(A, B)), C),但是如果您需要再次输出文本(如 SQLAlchemy 必须,发送到数据库),那么Grouping()对象对于记录您需要的位置非常有帮助添加(...)文本。

在 SQLAlchemy 中,这发生在SQL 编译器中,它使用访问者模式来调​​用sqlalchemy.sql.compiler.SQLCompiler.visit_grouping()方法

 def visit_grouping(self, grouping, asfrom=False, **kwargs):
     return "(" + grouping.element._compiler_dispatch(self, **kwargs) + ")"

该表达式仅表示:无论编译输出是什么,都放置(在之前和)之后grouping.element。虽然每种 SQL 方言都提供了基本编译器的子类,但没有一个会覆盖该visit_grouping()方法。


推荐阅读