如何在SQLAlchemy中实现优先级分组?

时间:2019-07-15 12:51:24

标签: python python-3.x sqlalchemy

我一直在研究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")

我目前有一些可行的功能,看起来像这样:

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

确切的代码并没有真正的意义,但是简要地说,它通过为操作员实现魔术方法并添加诸如.contains()之类的额外方法来构建BooleanExpression对象。例如:

From == "some_address@gmail.com"

将返回BooleanExpression。

然后将

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

最后,.where()方法使用一个BooleanExpressionClause并在后台对其进行流畅的查询。

到目前为止很好。

因此,我遇到的障碍涉及优先级分组。

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

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

我会从地址中带有“ john”的任何人获得每条消息,因为微软的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应该对它们进行相同的处理,但是很明显,我不知道的地方存在一些魔术 的。

我该如何复制这种行为?

谢谢你!

1 个答案:

答案 0 :(得分:3)

  

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

SQLAlchemy在这里不必做很多工作。大多数工作由Python完成,Python以特定的顺序解析对象。 Python根据operator precedence的规则解析表达式,因此根据优先级以特定顺序执行组合的表达式。如果该优先顺序对于您的应用程序是正确的,并且不介意总是对嵌套表达式进行分组,那么您就设置好了。在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__是对{{1 }}(传入operator.or_() function)。在SQLAlchemy中,这看起来很复杂,因为它必须委托不同类型的对象和SQL方言进行不同类型的比较!

但是sqlalchemy.sql.default_comparator module是基础,其中self.operate(or_, other)or_被(间接)映射到sqlalchemy.sql.elements.BooleanClauseList的类方法,从而产生该类的实例。

BooleanClauseList._construct() method通过在两个子句上委派and_方法来负责在那里的分组:

.self_group()

这传入convert_clauses = [ c.self_group(against=operator) for c in convert_clauses ] operator.or_,因此,让每个操作数根据优先级来决定是否需要使用operator.and_实例。对于Grouping()对象(因此BooleanClauseList... | ...的结果,然后与另一个... & ...|运算符进行组合),如果&的优先级低于或等于Grouping(),则ClauseList.self_group() method将产生self.operator

against

其中sqlalchemy.sql.operators.is_precedent()咨询表达式优先级表:

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

那么您的两个表达式会怎样? Python 已选取_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

这些归结为

>>> 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.operator,其优先级为3,高于或等于and_ == 2的父级子句的优先级。

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

or_

现在,第一个子句是一个# 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_, ...)实例,因为Groupingself.operator并且优先级比{{1} },然后对象树变为:

or_

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

在SQLAlchemy中,这种情况发生在SQL compiler中,后者使用visitor pattern来调用sqlalchemy.sql.compiler.SQLCompiler.visit_grouping() method

Grouping()

该表达式的意思仅仅是:将(...)的编译输出结果放在 def visit_grouping(self, grouping, asfrom=False, **kwargs): return "(" + grouping.element._compiler_dispatch(self, **kwargs) + ")" 之前,并将(放在之后。尽管每个SQL方言的确提供了基本编译器的子类,但是没有一个重写)方法。