Pony (ORM) 是如何运作的?

127

Pony ORM 可以将生成器表达式转换为 SQL,非常方便。例如:

>>> select(p for p in Person if p.name.startswith('Paul'))
        .order_by(Person.name)[:2]

SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
WHERE "p"."name" LIKE "Paul%"
ORDER BY "p"."name"
LIMIT 2

[Person[3], Person[1]]
>>>

我知道Python有很棒的内省和元编程功能,但是这个库如何能够在不进行预处理的情况下翻译生成器表达式呢?看起来像魔法。

[更新]

Blender写道:

这里是你要找的文件。它似乎使用了一些内省的魔法来重构生成器。我不确定它是否支持100%的Python语法,但这很酷。 - Blender

我曾经认为他们在探索生成器表达式协议的某些特性,但是看到了这个文件,以及涉及到ast模块... 不,他们不是在实时检查程序源代码,难以置信...

@BrenBarn:如果我尝试在select函数调用之外调用生成器,结果是:

>>> x = (p for p in Person if p.age > 20)
>>> x.next()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "<interactive input>", line 1, in <genexpr>
  File "C:\Python27\lib\site-packages\pony\orm\core.py", line 1822, in next
    % self.entity.__name__)
  File "C:\Python27\lib\site-packages\pony\utils.py", line 92, in throw
    raise exc
TypeError: Use select(...) function or Person.select(...) method for iteration
>>>

看起来他们正在执行更加神秘的咒语,比如实时检查select函数调用和处理Python抽象语法语法树。

我仍然希望有人能够解释一下,因为这个源码已经超出了我的魔法水平。


假设 p 对象是由 Pony 实现的一种类型的对象,它查看正在访问它的方法/属性(例如 namestartswith)并将它们转换为 SQL。 - BrenBarn
4
这是您要找的文件。看起来它使用一些内省魔法来重建生成器。我不确定它是否支持100%的Python语法,但这很酷。 - Blender
1
@Blender:我在LISP中见过这种技巧,但在Python中使用就有点过分了! - Paulo Scardine
1个回答

237

Pony ORM的作者在这里。

Pony将Python生成器翻译成SQL查询分为三个步骤:

  1. 反编译生成器字节码并重建生成器AST(抽象语法树)
  2. 将Python AST翻译成“抽象SQL”——一个SQL查询的通用基于列表的表示
  3. 将抽象SQL表示转换为特定于数据库的SQL方言

最复杂的部分是第二步,其中Pony必须理解Python表达式的“含义”。看起来您最感兴趣的是第一步,所以让我解释一下反编译的工作原理。

让我们考虑这个查询:

>>> from pony.orm.examples.estore import *
>>> select(c for c in Customer if c.country == 'USA').show()

将转换为以下SQL语句:

SELECT "c"."id", "c"."email", "c"."password", "c"."name", "c"."country", "c"."address"
FROM "Customer" "c"
WHERE "c"."country" = 'USA'

以下是将被打印出来的查询结果:

id|email              |password|name          |country|address  
--+-------------------+--------+--------------+-------+---------
1 |john@example.com   |***     |John Smith    |USA    |address 1
2 |matthew@example.com|***     |Matthew Reed  |USA    |address 2
4 |rebecca@example.com|***     |Rebecca Lawson|USA    |address 4

select()函数接受一个Python生成器作为参数,然后分析其字节码。我们可以使用标准Python dis模块获取此生成器的字节码指令:

>>> gen = (c for c in Customer if c.country == 'USA')
>>> import dis
>>> dis.dis(gen.gi_frame.f_code)
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                26 (to 32)
              6 STORE_FAST               1 (c)
              9 LOAD_FAST                1 (c)
             12 LOAD_ATTR                0 (country)
             15 LOAD_CONST               0 ('USA')
             18 COMPARE_OP               2 (==)
             21 POP_JUMP_IF_FALSE        3
             24 LOAD_FAST                1 (c)
             27 YIELD_VALUE         
             28 POP_TOP             
             29 JUMP_ABSOLUTE            3
        >>   32 LOAD_CONST               1 (None)
             35 RETURN_VALUE

Pony ORM在模块pony.orm.decompiling内提供了decompile()函数,可从字节码中恢复AST。
>>> from pony.orm.decompiling import decompile
>>> ast, external_names = decompile(gen)

在这里,我们可以看到AST节点的文本表示:

>>> ast
GenExpr(GenExprInner(Name('c'), [GenExprFor(AssName('c', 'OP_ASSIGN'), Name('.0'),
[GenExprIf(Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]))])]))

现在我们来看看decompile()函数的工作原理。

decompile()函数创建了一个实现Visitor模式的Decompiler对象。 反编译器实例逐个获取字节码指令。 对于每个指令,反编译器对象都会调用自己的方法。 这个方法的名称与当前字节码指令的名称相同。

当Python计算表达式时,它使用栈存储计算的中间结果。反编译器对象也有自己的栈, 但这个栈不是存储表达式计算结果,而是存储表达式的AST节点。

当下一个字节码指令的反编译器方法被调用时, 它从栈中取出AST节点,将它们组合成一个新的AST节点,然后将该节点放在栈顶。

例如,让我们看看子表达式c.country == 'USA'如何计算。 相应的字节码片段为:

              9 LOAD_FAST                1 (c)
             12 LOAD_ATTR                0 (country)
             15 LOAD_CONST               0 ('USA')
             18 COMPARE_OP               2 (==)

所以,反编译器对象执行以下操作:
  1. 调用 decompiler.LOAD_FAST('c')。 该方法将 Name('c') 节点置于反编译器堆栈的顶部。
  2. 调用 decompiler.LOAD_ATTR('country')。 该方法从堆栈中取出 Name('c') 节点, 创建 Getattr(Name('c'), 'country') 节点并将其置于堆栈的顶部。
  3. 调用 decompiler.LOAD_CONST('USA')。 该方法将 Const('USA') 节点置于堆栈的顶部。
  4. 调用 decompiler.COMPARE_OP('==')。 该方法从堆栈中取出两个节点(Getattr 和 Const), 然后将 Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) 置于堆栈的顶部。

在处理完所有字节码指令之后,反编译器堆栈包含一个与整个生成器表达式对应的 AST 节点。

由于 Pony ORM 只需要反编译生成器和 lambda 函数,因此这并不复杂, 因为生成器的指令流相对直接 - 它只是一堆嵌套的循环。

当前 Pony ORM 包含了除以下两个事项外的完整生成器指令集:

  1. 内联 if 表达式:a if b else c
  2. 复合比较:a < b < c

如果 Pony 遇到此类表达式,则会引发 NotImplementedError 异常。但即使在 这种情况下,您也可以通过将生成器表达式作为字符串传递来使其正常工作。 当您将生成器作为字符串传递时,Pony 不使用反编译器模块。相反, 它使用标准 Python 的 compiler.parse 函数获取 AST。

希望这回答了您的问题。


29
非常高效:(1)字节码反编译非常快。(2)由于每个查询都有相应的代码对象,因此该代码对象可以用作缓存键。因此,Pony ORM仅翻译每个查询一次,而Django和SQLAlchemy必须一遍又一遍地翻译相同的查询。(3)由于Pony ORM使用IdentityMap模式,在同一个事务中缓存查询结果。有一篇帖子(俄语),作者称Pony ORM即使没有查询结果缓存,也比Django和SQLAlchemy快1.5-3倍:http://habrahabr.ru/post/188842/ - Alexander Kozlovsky
3
这与pypy JIT编译器兼容吗? - Mzzl
2
我没有测试过,但一些Reddit评论者说它是兼容的:http://tinyurl.com/ponyorm-pypy - Alexander Kozlovsky
10
SQLAlchemy具有查询缓存,ORM广泛使用此功能。默认情况下它没有开启,因为我们没有一个功能可以将SQL表达式的构建与它声明的源代码位置链接起来,这正是代码对象真正提供给您的内容。我们可以使用堆栈帧检查来获得相同的结果,但这对于我来说有点太巧妙了。无论如何,生成SQL是任何情况下最不关键的性能领域;获取行和记录更改才是关键。 - zzzeek
2
@randomsurfer_123 可能不需要,我们只需要一些时间来实现它(也许一周),而且还有其他更重要的任务需要完成。 - Alexander Kozlovsky
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接