自定义 SQL 构造和编译扩展
提供了用于创建自定义 ClauseElements 和编译器的 API。
概要
使用涉及创建一个或多个ClauseElement
子类和一个或多个定义其编译的可调用对象:
from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.expression import ColumnClause class MyColumn(ColumnClause): inherit_cache = True @compiles(MyColumn) def compile_mycolumn(element, compiler, **kw): return "[%s]" % element.name
在上面,MyColumn
扩展了ColumnClause
,这是命名列对象的基本表达式元素。compiles
装饰器向MyColumn
类注册自身,以便在将对象编译为字符串时调用它:
from sqlalchemy import select s = select(MyColumn('x'), MyColumn('y')) print(str(s))
产生:
SELECT [x], [y]
方言特定的编译规则
编译器也可以是特定于方言的。将为使用的方言调用适当的编译器:
from sqlalchemy.schema import DDLElement class AlterColumn(DDLElement): inherit_cache = False def __init__(self, column, cmd): self.column = column self.cmd = cmd @compiles(AlterColumn) def visit_alter_column(element, compiler, **kw): return "ALTER COLUMN %s ..." % element.column.name @compiles(AlterColumn, 'postgresql') def visit_alter_column(element, compiler, **kw): return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name)
当使用任何postgresql
方言时,第二个visit_alter_table
将被调用。
编译自定义表达式构造的子元素
compiler
参数是正在使用的Compiled
对象。可以检查此对象的任何有关进行中编译的信息,包括compiler.dialect
、compiler.statement
等。SQLCompiler
和DDLCompiler
都包括一个process()
方法,可用于编译嵌入属性:
from sqlalchemy.sql.expression import Executable, ClauseElement class InsertFromSelect(Executable, ClauseElement): inherit_cache = False def __init__(self, table, select): self.table = table self.select = select @compiles(InsertFromSelect) def visit_insert_from_select(element, compiler, **kw): return "INSERT INTO %s (%s)" % ( compiler.process(element.table, asfrom=True, **kw), compiler.process(element.select, **kw) ) insert = InsertFromSelect(t1, select(t1).where(t1.c.x>5)) print(insert)
产生:
"INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
注意
上述的InsertFromSelect
构造仅是一个示例,这种实际功能已经可以使用Insert.from_select()
方法。
在 SQL 和 DDL 编译器之间进行交叉编译
SQL 和 DDL 构造分别使用不同的基本编译器 - SQLCompiler
和DDLCompiler
。一个常见的需求是从 DDL 表达式中访问 SQL 表达式的编译规则。DDLCompiler
包含一个访问器sql_compiler
,因此我们可以生成嵌入 SQL 表达式的 CHECK 约束,如下所示:
@compiles(MyConstraint) def compile_my_constraint(constraint, ddlcompiler, **kw): kw['literal_binds'] = True return "CONSTRAINT %s CHECK (%s)" % ( constraint.name, ddlcompiler.sql_compiler.process( constraint.expression, **kw) )
在上面,我们在SQLCompiler.process()
中调用的过程步骤中添加了一个额外的标志,即literal_binds
标志。这表示任何引用BindParameter
对象或其他“literal”对象(如引用字符串或整数的对象)的 SQL 表达式应该原地呈现,而不是作为绑定参数引用;在发出 DDL 时,通常不支持绑定参数。
更改现有构造的默认编译
编译器扩展同样适用于现有的结构。当覆盖内置 SQL 结构的编译时,@compiles 装饰器会调用适当的类(确保使用类,即 Insert
或 Select
,而不是创建函数,比如 insert()
或 select()
)。
在新的编译函数中,要获取“原始”的编译例程,使用适当的 visit_XXX 方法 - 这是因为编译器.process() 将调用重写的例程并导致无限循环。比如,要向所有的插入语句添加“前缀”:
from sqlalchemy.sql.expression import Insert @compiles(Insert) def prefix_inserts(insert, compiler, **kw): return compiler.visit_insert(insert.prefix_with("some prefix"), **kw)
上述编译器在编译时会在所有的 INSERT 语句前加上“某个前缀”。
更改类型的编译
compiler
也适用于类型,比如下面我们为 String
/VARCHAR
实现了 MS-SQL 特定的 ‘max’ 关键字:
@compiles(String, 'mssql') @compiles(VARCHAR, 'mssql') def compile_varchar(element, compiler, **kw): if element.length == 'max': return "VARCHAR('max')" else: return compiler.visit_VARCHAR(element, **kw) foo = Table('foo', metadata, Column('data', VARCHAR('max')) )
子类化指南
使用编译器扩展的一个重要部分是子类化 SQLAlchemy 表达式结构。为了使这更容易,表达式和模式包含一组旨在用于常见任务的“基础”。概要如下:
ClauseElement
- 这是根表达式类。任何 SQL 表达式都可以从这个基类派生,并且对于长一些的构造,比如专门的 INSERT 语句,这可能是最好的选择。ColumnElement
- 所有“类似列”的元素的根。你在 SELECT 语句的“列”子句中(以及 order by 和 group by)放置的任何东西都可以从这个派生 - 该对象将自动具有 Python 的“比较”行为。ColumnElement
类希望有一个type
成员,该成员是表达式的返回类型。这可以在构造函数的实例级别或类级别上建立:
class timestamp(ColumnElement): type = TIMESTAMP() inherit_cache = True
FunctionElement
- 这是ColumnElement
和“from 子句”类似对象的混合体,并表示 SQL 函数或存储过程类型的调用。由于大多数数据库支持“SELECT FROM <某个函数>”这样的语句,FunctionElement
添加了在select()
构造的 FROM 子句中使用的能力:
from sqlalchemy.sql.expression import FunctionElement class coalesce(FunctionElement): name = 'coalesce' inherit_cache = True @compiles(coalesce) def compile(element, compiler, **kw): return "coalesce(%s)" % compiler.process(element.clauses, **kw) @compiles(coalesce, 'oracle') def compile(element, compiler, **kw): if len(element.clauses) > 2: raise TypeError("coalesce only supports two arguments on Oracle") return "nvl(%s)" % compiler.process(element.clauses, **kw)
ExecutableDDLElement
- 所有 DDL 表达式的根,比如 CREATE TABLE,ALTER TABLE 等。ExecutableDDLElement
的子类的编译由DDLCompiler
发出,而不是由SQLCompiler
发出。ExecutableDDLElement
也可以与DDLEvents.before_create()
和DDLEvents.after_create()
等事件钩子一起用作事件钩子,允许在 CREATE TABLE 和 DROP TABLE 序列期间自动调用构造。
另请参见
自定义 DDL - 包含将DDL
对象(它们本身是ExecutableDDLElement
实例)与DDLEvents
事件钩子相关联的示例。Executable
- 这是一个 mixin,应该与任何表示可以直接传递给execute()
方法的“独立”SQL 语句的表达式类一起使用。它已经隐含在DDLElement
和FunctionElement
中。
上述大部分构造也响应 SQL 语句缓存。子类构造将希望为对象定义缓存行为,这通常意味着将标志inherit_cache
设置为False
或True
的值。有关背景信息,请参见下一节为自定义构造启用缓存支持。
为自定义构造启用缓存支持
从版本 1.4 开始,SQLAlchemy 包括一个 SQL 编译缓存设施,它将允许等效的 SQL 构造缓存它们的字符串形式,以及用于从语句中获取结果的其他结构信息。
如在对象不会生成缓存键,性能影响中讨论的原因,该缓存系统的实现对于在缓存系统中包含自定义 SQL 构造和/或子类采取了保守的方法。这包括任何用户定义的 SQL 构造,包括此扩展的所有示例,除非它们明确声明能够这样做,否则默认情况下不会参与缓存。当在特定子类的类级别设置HasCacheKey.inherit_cache
属性为True
时,将指示该类的实例可以安全地进行缓存,使用直接父类的缓存键生成方案。例如,这适用于先前指示的“概要”示例:
class MyColumn(ColumnClause): inherit_cache = True @compiles(MyColumn) def compile_mycolumn(element, compiler, **kw): return "[%s]" % element.name
上面,MyColumn
类不包含任何影响其 SQL 编译的新状态;MyColumn
实例的缓存键将利用ColumnClause
超类的缓存键,这意味着它将考虑对象的类(MyColumn
)、对象的字符串名称和数据类型:
>>> MyColumn("some_name", String())._generate_cache_key() CacheKey( key=('0', <class '__main__.MyColumn'>, 'name', 'some_name', 'type', (<class 'sqlalchemy.sql.sqltypes.String'>, ('length', None), ('collation', None)) ), bindparams=[])
对于那些在许多更大语句中可能被大量使用作为组件的对象,比如Column
子类和自定义 SQL 数据类型,尽可能启用缓存非常重要,否则可能会对性能产生负面影响。
一个包含影响其 SQL 编译的状态的对象示例是在编译自定义表达式构造的子元素中所示的一个示例;这是一个将Table
和Select
构造组合在一起的“INSERT FROM SELECT”构造,每个构造独立影响构造的 SQL 字符串生成。对于这个类,示例说明它根本不参与缓存:
class InsertFromSelect(Executable, ClauseElement): inherit_cache = False def __init__(self, table, select): self.table = table self.select = select @compiles(InsertFromSelect) def visit_insert_from_select(element, compiler, **kw): return "INSERT INTO %s (%s)" % ( compiler.process(element.table, asfrom=True, **kw), compiler.process(element.select, **kw) )
虽然上述InsertFromSelect
也可能生成由Table
和Select
组件组合而成的缓存键,但目前该 API 并不完全公开。然而,对于“INSERT FROM SELECT”构造,它仅用于特定操作,缓存并不像前面的示例那样关键。
对于在相对孤立并且通常是独立的对象,例如自定义 DML 构造,如 “INSERT FROM SELECT”,缓存通常不那么关键,因为对于这种构造缺乏缓存仅对该特定操作具有局部影响。
SqlAlchemy 2.0 中文文档(三十七)(2)https://developer.aliyun.com/article/1562698