【问题标题】:How is precedence grouping implemented in SQLAlchemy?SQLAlchemy 中如何实现优先分组?
【发布时间】:2019-11-24 04:19:38
【问题描述】:

我一直在查看 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 应该以相同的方式处理它们,但显然那里有一些我不知道的魔法 的。

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

非常感谢!

【问题讨论】:

    标签: python python-3.x sqlalchemy


    【解决方案1】:

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

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

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

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

    但基础是sqlalchemy.sql.default_comparator module,其中or_and_ (间接)映射到sqlalchemy.sql.elements.BooleanClauseList 的类方法,生成该类的一个实例。

    BooleanClauseList._construct() method 负责在那里处理分组,方法是在两个子句上委托给.self_group() 方法:

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

    这会传入operator.or_operator.and_,因此让每个操作数根据优先级决定是否需要使用Grouping() 实例。对于BooleanClauseList 对象(所以... | ...... & ... 的结果,但随后组合 与另一个|& 运算符),ClauseList.self_group() method 将产生Grouping() 如果self.operator 的优先级低于或等于 against

    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
    

    sqlalchemy.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.operatoror_,并且它的优先级低于父子句列表中的and_,因此对象树变成:

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

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

    在 SQLAlchemy 中,这发生在 SQL compiler 中,它使用 visitor pattern 来调用 sqlalchemy.sql.compiler.SQLCompiler.visit_grouping() method

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

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

    【讨论】:

    • 说真的,非常感谢!我无法相信答案有多好和有多深入。我以为 & 和 |运算符具有相同的优先级,并没有想到要检查。愚蠢的。感谢您对 AST 的剖析,以及对 SQLAlchemy 内部结构的解释。我要去阅读有关访问者设计模式的内容,哇。总的来说,这可能比我目前的编程能力略高,但像 SQLAlchemy 这样的项目如此复杂和结构良好,真是令人着迷。我觉得我什至无法正确地欣赏架构的出色之处,但我真的很想。
    猜你喜欢
    • 2012-02-08
    • 2011-07-12
    • 2012-04-15
    • 2020-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-14
    • 1970-01-01
    相关资源
    最近更新 更多