SQLAlchemy 中如何实现优先分组?

Mat*_*GdV 3 python sqlalchemy python-3.x

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

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

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

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

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

Message.where(Subject.contains("Hello Friend!") & (From == "some_address@gmail.com")).execute()
Run Code Online (Sandbox Code Playgroud)

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

From == "some_address@gmail.com"
Run Code Online (Sandbox Code Playgroud)

将返回一个布尔表达式。

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

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

到现在为止还挺好。

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

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

From.contains("john") | From.contains("doe") & Subject.contains("Hi!")
Run Code Online (Sandbox Code Playgroud)

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

From.contains("john") | (From.contains("doe") & Subject.contains("Hi!"))
Run Code Online (Sandbox Code Playgroud)

当我想要的是:

(From.contains("john") | From.contains("doe")) & Subject.contains("Hi!")
Run Code Online (Sandbox Code Playgroud)

但是,如果我只是使用我当前的 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") ))
Run Code Online (Sandbox Code Playgroud)

这些打印语句分别返回,

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 = ?
Run Code Online (Sandbox Code Playgroud)

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 = ?
Run Code Online (Sandbox Code Playgroud)

SQLAlchemy 的内部结构到底如何区分这两个过滤器子句之间的区别?据我所知,python 应该以相同的方式处理它们,但显然有一些我不知道的魔法在那里发生。

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

谢谢一堆!

Mar*_*ers 8

这终于让我想到了我的问题。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__为对self.operate(or_, other)(传入operator.or_()函数)的调用。在 SQLAlchemy 中,这看起来很复杂,因为这必须委托给不同类型的对象和 SQL 方言的不同类型的比较!

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

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

convert_clauses = [
    c.self_group(against=operator) for c in convert_clauses
]
Run Code Online (Sandbox Code Playgroud)

这传入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
Run Code Online (Sandbox Code Playgroud)

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))
Run Code Online (Sandbox Code Playgroud)

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

A | B & C
(A | B) & C
Run Code Online (Sandbox Code Playgroud)

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()))"
Run Code Online (Sandbox Code Playgroud)

这些归结为

BinOp(
    left=A,
    op=or_,
    right=BinOp(left=B, op=and_, right=C)
)
Run Code Online (Sandbox Code Playgroud)

BinOp(
    left=BinOp(left=A, op=or_, right=B),
    op=and_,
    right=C
)
Run Code Online (Sandbox Code Playgroud)

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

# 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))))
Run Code Online (Sandbox Code Playgroud)

因为这里的第二个子句是一个BooleanClauseList(and_, ...)实例,.self_group()对该子句的调用不会返回Grouping(); there self.operatoris and_,它的优先级为 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))
Run Code Online (Sandbox Code Playgroud)

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

BooleanClauseList(and_, (Grouping(BooleanClauseList(or_, (A, B))), C))
Run Code Online (Sandbox Code Playgroud)

现在,如果您只想确保您的表达式按正确的顺序分组,那么您实际上不需要注入自己的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) + ")"
Run Code Online (Sandbox Code Playgroud)

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