如何在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对象的列表,该列表跟踪每2个表达式所连接的运算符。
最后,.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内部结构如何分辨这两个filter子句之间的区别?据我所知,python应该对它们进行相同的处理,但是显然我不知道的地方存在一些魔术。
我该如何复制这种行为?
谢谢一群!
问题答案:
最终,这使我想到了问题。SQLAlchemy能够以某种方式理解优先级组,但是我一生都无法理解它是如何实现的。
SQLAlchemy在这里不必做很多工作。大多数工作是由Python完成的,Python以特定的顺序解析对象。Python根据运算符优先级的规则解析表达式,因此根据优先级以特定顺序执行组合的表达式。如果该优先顺序对您的应用程序是正确的,并且不介意 总是
对嵌套表达式进行分组,那么您已设置好。在SQL中,情况并非总是如此,并且SQLAlchemy希望输出有效的SQL表达式,并且使用最少的括号,因此SQLAlchemy确实会查询其自己的优先级表。这样,它可以决定何时(...)
在输出中需要分组。
SQLAlchemy返回*Clause*
表示其操作数上的操作的专用表达式对象(每个操作对象可以是进一步的表达式),然后在这些操作对象也用于操作中时,将其进一步组合。最后,您将拥有
一棵
对象树,然后在编译为SQL时遍历该树,然后根据需要生成看到的分组输出。在需要优先级的地方,SQLAlchemy会插入sqlalchemy.sql.elements.Grouping()
object,并且由SQL方言来产生正确的分组语法。
如果您正在寻找在SQLAlchemy的源代码,你会想看看sqlalchemy.sql.operators.ColumnOperators
类和它的父类,sqlalchemy.sql.operators.Operators
,该工具__or__
作为一个呼叫self.operate(or_, other)
(传入operator.or_()
功能)。在SQLAlchemy中,这看起来很复杂,因为它必须委托不同类型的对象和SQL方言进行不同类型的比较!
但是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_
或operator.and_
,因此让每个操作数Grouping()
根据优先级决定是否需要使用实例。对于BooleanClauseList
对象(因此,结果... | ...
或... & ...
,然后与另一个或运算符 组合
),该方法将产生一个if
,其优先级低于或等于:|``&
ClauseList.self_group()
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.operator
是and_
,它具有为3的优先级,这是较高的,不小于或等于,向的precendence
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.operator
isor_
且其优先级比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()
方法。