SqlAlchemy 2.0 中文文档(四十二)(1)https://developer.aliyun.com/article/1563012
应用 SQL 级别的绑定/结果处理
如在扩展现有类型一节中所见,SQLAlchemy 允许在参数发送到语句时以及从数据库加载结果行时调用 Python 函数,以对发送到或从数据库的值应用转换。还可以定义 SQL 级别的转换。其理念在于,当只有关系数据库包含一系列必要的函数来在应用程序和持久性格式之间强制转换传入和传出数据时。示例包括使用数据库定义的加密/解密函数,以及处理地理数据的存储过程。
任何 TypeEngine
、UserDefinedType
或 TypeDecorator
的子类都可以包含 TypeEngine.bind_expression()
和/或 TypeEngine.column_expression()
的实现,当定义为返回非 None
值时,应返回一个 ColumnElement
表达式,以注入到 SQL 语句中,要么围绕绑定参数,要么是列表达式。例如,要构建一个 Geometry
类型,它将对所有传出值应用 PostGIS 函数 ST_GeomFromText
,并对所有传入数据应用函数 ST_AsText
,我们可以创建我们自己的 UserDefinedType
的子类,并与 func
一起提供这些方法:
from sqlalchemy import func from sqlalchemy.types import UserDefinedType class Geometry(UserDefinedType): def get_col_spec(self): return "GEOMETRY" def bind_expression(self, bindvalue): return func.ST_GeomFromText(bindvalue, type_=self) def column_expression(self, col): return func.ST_AsText(col, type_=self)
我们可以将 Geometry
类型应用到 Table
元数据中,并在 select()
构造中使用它:
geometry = Table( "geometry", metadata, Column("geom_id", Integer, primary_key=True), Column("geom_data", Geometry), ) print( select(geometry).where( geometry.c.geom_data == "LINESTRING(189412 252431,189631 259122)" ) )
结果的 SQL 将两个函数嵌入到适当的位置。ST_AsText
应用于列子句,以便返回值在进入结果集之前通过该函数运行,而 ST_GeomFromText
则在绑定参数上运行,以便转换传入的值:
SELECT geometry.geom_id, ST_AsText(geometry.geom_data) AS geom_data_1 FROM geometry WHERE geometry.geom_data = ST_GeomFromText(:geom_data_2)
TypeEngine.column_expression()
方法与编译器的机制交互,以确保 SQL 表达式不会干扰包装表达式的标记。例如,如果我们对表达式的 label()
渲染一个 select()
,字符串标签将移动到包装表达式的外部:
print(select(geometry.c.geom_data.label("my_data")))
输出:
SELECT ST_AsText(geometry.geom_data) AS my_data FROM geometry
另一个例子是我们装饰 BYTEA
以提供一个 PGPString
,它将使用 PostgreSQL 的 pgcrypto
扩展来透明地加密/解密值:
from sqlalchemy import ( create_engine, String, select, func, MetaData, Table, Column, type_coerce, TypeDecorator, ) from sqlalchemy.dialects.postgresql import BYTEA class PGPString(TypeDecorator): impl = BYTEA cache_ok = True def __init__(self, passphrase): super(PGPString, self).__init__() self.passphrase = passphrase def bind_expression(self, bindvalue): # convert the bind's type from PGPString to # String, so that it's passed to psycopg2 as is without # a dbapi.Binary wrapper bindvalue = type_coerce(bindvalue, String) return func.pgp_sym_encrypt(bindvalue, self.passphrase) def column_expression(self, col): return func.pgp_sym_decrypt(col, self.passphrase) metadata_obj = MetaData() message = Table( "message", metadata_obj, Column("username", String(50)), Column("message", PGPString("this is my passphrase")), ) engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True) with engine.begin() as conn: metadata_obj.create_all(conn) conn.execute( message.insert(), {"username": "some user", "message": "this is my message"}, ) print( conn.scalar(select(message.c.message).where(message.c.username == "some user")) )
pgp_sym_encrypt
和 pgp_sym_decrypt
函数应用于 INSERT 和 SELECT 语句:
INSERT INTO message (username, message) VALUES (%(username)s, pgp_sym_encrypt(%(message)s, %(pgp_sym_encrypt_1)s)) -- {'username': 'some user', 'message': 'this is my message', -- 'pgp_sym_encrypt_1': 'this is my passphrase'} SELECT pgp_sym_decrypt(message.message, %(pgp_sym_decrypt_1)s) AS message_1 FROM message WHERE message.username = %(username_1)s -- {'pgp_sym_decrypt_1': 'this is my passphrase', 'username_1': 'some user'} ```## 重新定义和创建新操作符 SQLAlchemy Core 定义了一组固定的表达式运算符,可用于所有列表达式。其中一些操作的效果是重载 Python 的内置运算符;此类运算符的示例包括`ColumnOperators.__eq__()`(`table.c.somecolumn == 'foo'`)、`ColumnOperators.__invert__()`(`~table.c.flag`)和`ColumnOperators.__add__()`(`table.c.x + table.c.y`)。其他运算符作为列表达式的显式方法公开,例如`ColumnOperators.in_()`(`table.c.value.in_(['x', 'y'])`)和`ColumnOperators.like()`(`table.c.value.like('%ed%')`)。 当需要使用尚未直接支持的 SQL 运算符时,最快的方法是在任何 SQL 表达式对象上使用`Operators.op()`方法;此方法接受表示要渲染的 SQL 运算符的字符串,并返回一个 Python 可调用对象,该对象接受任意的右侧表达式: ```py >>> from sqlalchemy import column >>> expr = column("x").op(">>")(column("y")) >>> print(expr) x >> y
当使用自定义 SQL 类型时,还有一种方法可以实现自定义运算符,这些运算符与使用该列类型的任何列表达式自动关联,而无需每次使用运算符时直接调用Operators.op()
。
为了实现这一点,SQL 表达式构造会查询与构造关联的TypeEngine
对象,以确定内置运算符的行为,以及查找可能已调用的新方法。 TypeEngine
定义了一个由Comparator
类实现的“比较”对象,为 SQL 运算符提供基本行为,许多特定类型提供了该类的自己的子实现。用户定义的Comparator
实现可以直接构建到特定类型的简单子类中,以覆盖或定义新操作。下面,我们创建一个Integer
子类,该子类重写了ColumnOperators.__add__()
运算符,而该运算符又使用Operators.op()
来生成自定义的 SQL 本身:
from sqlalchemy import Integer class MyInt(Integer): class comparator_factory(Integer.Comparator): def __add__(self, other): return self.op("goofy")(other)
上述配置创建了一个名为MyInt
的新类,该类将TypeEngine.comparator_factory
属性设置为指向一个新的类,该类是Integer
类型相关联的Comparator
类的子类。
用法:
>>> sometable = Table("sometable", metadata, Column("data", MyInt)) >>> print(sometable.c.data + 5) sometable.data goofy :data_1
ColumnOperators.__add__()
的实现由拥有的 SQL 表达式查询,通过将其自身作为expr
属性来实例化Comparator
。当实现需要直接引用源 ColumnElement
对象时,可以使用该属性:
from sqlalchemy import Integer class MyInt(Integer): class comparator_factory(Integer.Comparator): def __add__(self, other): return func.special_addition(self.expr, other)
添加到 Comparator
的新方法通过动态查找方案暴露在拥有的 SQL 表达式对象上,该方案将 Comparator
添加到拥有的 ColumnElement
表达式构造上的方法。例如,要为整数添加 log()
函数:
from sqlalchemy import Integer, func class MyInt(Integer): class comparator_factory(Integer.Comparator): def log(self, other): return func.log(self.expr, other)
使用上述类型:
>>> print(sometable.c.data.log(5)) log(:log_1, :log_2)
当使用返回布尔结果的比较操作的 Operators.op()
时,应将 Operators.op.is_comparison
标志设置为 True
:
class MyInt(Integer): class comparator_factory(Integer.Comparator): def is_frobnozzled(self, other): return self.op("--is_frobnozzled->", is_comparison=True)(other)
一元操作也是可能的。例如,要添加 PostgreSQL 阶乘运算符的实现,我们将 UnaryExpression
构造与 custom_op
结合起来产生阶乘表达式:
from sqlalchemy import Integer from sqlalchemy.sql.expression import UnaryExpression from sqlalchemy.sql import operators class MyInteger(Integer): class comparator_factory(Integer.Comparator): def factorial(self): return UnaryExpression( self.expr, modifier=operators.custom_op("!"), type_=MyInteger )
使用上述类型:
>>> from sqlalchemy.sql import column >>> print(column("x", MyInteger).factorial()) x !
另请参阅
Operators.op()
TypeEngine.comparator_factory
创建新类型
UserDefinedType
类被提供为定义全新数据库类型的简单基类。使用此类来表示 SQLAlchemy 不知道的原生数据库类型。如果只需要 Python 转换行为,请改用 TypeDecorator
。
对象名称 | 描述 |
UserDefinedType | 用户自定义类型的基础。 |
class sqlalchemy.types.UserDefinedType
用户自定义类型的基础。
这应该是新类型的基础。注意,对于大多数情况,TypeDecorator
可能更合适:
import sqlalchemy.types as types class MyType(types.UserDefinedType): cache_ok = True def __init__(self, precision = 8): self.precision = precision def get_col_spec(self, **kw): return "MYTYPE(%s)" % self.precision def bind_processor(self, dialect): def process(value): return value return process def result_processor(self, dialect, coltype): def process(value): return value return process
一旦类型创建完成,就可以立即使用:
table = Table('foo', metadata_obj, Column('id', Integer, primary_key=True), Column('data', MyType(16)) )
get_col_spec()
方法在大多数情况下将接收一个关键字参数 type_expression
,该参数指的是正在编译的类型的拥有表达式,例如 Column
或 cast()
构造。只有在方法接受关键字参数(例如 **kw
)时才会发送此关键字;对于支持此函数的旧形式,使用内省来检查是否存在此关键字。
UserDefinedType.cache_ok
类级标志指示此自定义 UserDefinedType
是否安全用作缓存键的一部分。此标志默认为 None
,当 SQL 编译器尝试为使用此类型的语古句生成缓存键时,将首先生成警告。如果不能保证 UserDefinedType
每次产生相同的绑定/结果行为和 SQL 生成行为,则应将此标志设置为 False
;否则,如果类每次产生相同的行为,则可以将其设置为 True
。有关此工作原理的更多说明,请参见 UserDefinedType.cache_ok
。
新版本 1.4.28 中的更新:将 ExternalType.cache_ok
标志泛化,以便它对 TypeDecorator
和 'dUserDefinedType`](#sqlalchemy.types.UserDefinedType “sqlalchemy.types.UserDefinedType”) 都可用。
成员
cache_ok, coerce_compared_value(), ensure_kwarg
类签名
类 sqlalchemy.types.UserDefinedType
(sqlalchemy.types.ExternalType
、sqlalchemy.types.TypeEngineMixin
、sqlalchemy.types.TypeEngine
、sqlalchemy.util.langhelpers.EnsureKWArg
)
attribute cache_ok: bool | None = None
继承自 ExternalType.cache_ok
属性的 ExternalType
指示使用此 ExternalType
的语句是否“可缓存”。
默认值 None
将发出警告,然后不允许缓存包含此类型的语句。将其设置为 False
可完全禁用对包含此类型的语句进行缓存而不发出警告。当设置为 True
时,对象的类和其状态中的选定元素将作为缓存键的一部分使用。例如,使用 TypeDecorator
:
class MyType(TypeDecorator): impl = String cache_ok = True def __init__(self, choices): self.choices = tuple(choices) self.internal_only = True
上述类型的缓存键将等同于:
>>> MyType(["a", "b", "c"])._static_cache_key (<class '__main__.MyType'>, ('choices', ('a', 'b', 'c')))
缓存方案将从与 __init__()
方法中的参数名称相对应的类型中提取属性。在上面的例子中,“choices”属性成为缓存键的一部分,但“internal_only”则不是,因为没有名为“internal_only”的参数。
可缓存元素的要求是它们可哈希,并且它们指示对于给定缓存值的情况下,每次使用此类型的表达式渲染的 SQL 相同。
为了适应指向不可哈希结构(如字典、集合和列表)的数据类型,这些对象可以通过将可哈希结构分配给与参数名称相对应的属性来使其“可缓存”。例如,接受查找值字典的数据类型可能会将其公开为排序后的元组系列。假设以前不可缓存的类型为:
class LookupType(UserDefinedType): '''a custom type that accepts a dictionary as a parameter. this is the non-cacheable version, as "self.lookup" is not hashable. ''' def __init__(self, lookup): self.lookup = lookup def get_col_spec(self, **kw): return "VARCHAR(255)" def bind_processor(self, dialect): # ... works with "self.lookup" ...
其中“lookup”是一个字典。该类型将无法生成缓存键:
>>> type_ = LookupType({"a": 10, "b": 20}) >>> type_._static_cache_key <stdin>:1: SAWarning: UserDefinedType LookupType({'a': 10, 'b': 20}) will not produce a cache key because the ``cache_ok`` flag is not set to True. Set this flag to True if this type object's state is safe to use in a cache key, or False to disable this warning. symbol('no_cache')
如果我们设置了这样的缓存键,它将无法使用。我们将得到一个包含字典的元组结构,该字典本身不能作为“缓存字典”中的键,比如 SQLAlchemy 的语句缓存,因为 Python 字典不可哈希:
>>> # set cache_ok = True >>> type_.cache_ok = True >>> # this is the cache key it would generate >>> key = type_._static_cache_key >>> key (<class '__main__.LookupType'>, ('lookup', {'a': 10, 'b': 20})) >>> # however this key is not hashable, will fail when used with >>> # SQLAlchemy statement cache >>> some_cache = {key: "some sql value"} Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'dict'
可通过将排序后的元组系列分配给“.lookup”属性来使类型可缓存:
class LookupType(UserDefinedType): '''a custom type that accepts a dictionary as a parameter. The dictionary is stored both as itself in a private variable, and published in a public variable as a sorted tuple of tuples, which is hashable and will also return the same value for any two equivalent dictionaries. Note it assumes the keys and values of the dictionary are themselves hashable. ''' cache_ok = True def __init__(self, lookup): self._lookup = lookup # assume keys/values of "lookup" are hashable; otherwise # they would also need to be converted in some way here self.lookup = tuple( (key, lookup[key]) for key in sorted(lookup) ) def get_col_spec(self, **kw): return "VARCHAR(255)" def bind_processor(self, dialect): # ... works with "self._lookup" ...
在上面的例子中,LookupType({"a": 10, "b": 20})
的缓存键将是:
>>> LookupType({"a": 10, "b": 20})._static_cache_key (<class '__main__.LookupType'>, ('lookup', (('a', 10), ('b', 20))))
新版本 1.4.14 中新增:- 添加了 cache_ok
标志,以允许对 TypeDecorator
类进行某些缓存的可配置性。
新版本 1.4.28 中新增:- 添加了ExternalType
mixin,该 mixin 将 cache_ok
标志泛化到了 TypeDecorator
和 UserDefinedType
类。
另请参见
SQL 编译缓存
method coerce_compared_value(op: OperatorType | None, value: Any) → TypeEngine[Any]
建议为表达式中的“强制转换”Python 值提供一个类型。
UserDefinedType
的默认行为与TypeDecorator
的默认行为相同;默认情况下,它返回self
,假设比较的值应强制转换为与此相同类型。有关更多详细信息,请参阅TypeDecorator.coerce_compared_value()
。
attribute ensure_kwarg: str = 'get_col_spec'
表示应接受**kw
参数的方法名称的正则表达式。
类将扫描与名称模板匹配的方法,并在必要时装饰它们,以确保接受**kw
参数。
使用自定义类型和反射
需要注意的是,被修改以具有附加 Python 行为的数据库类型,包括基于TypeDecorator
的类型以及其他用户定义的数据类型子类,不在数据库模式中具有任何表示。当使用数据库中所述的反射功能时 Reflecting Database Objects,SQLAlchemy 使用一个固定映射,将数据库服务器报告的数据类型信息链接到 SQLAlchemy 数据类型对象。例如,如果我们在 PostgreSQL 模式中查看特定数据库列的定义,我们可能会收到字符串"VARCHAR"
。SQLAlchemy 的 PostgreSQL 方言具有一个硬编码映射,将字符串名称"VARCHAR"
链接到 SQLAlchemy VARCHAR
类,这就是为什么当我们发出类似Table('my_table', m, autoload_with=engine)
的语句时,其中的Column
对象将在其内部具有VARCHAR
的实例的原因。
这意味着如果一个Table
对象使用的类型对象与数据库本地类型名称不直接对应,如果我们在其他地方使用反射为此数据库表创建一个新的Table
对象,并针对一个新的MetaData
集合,那么它将不会有此数据类型。例如:
>>> from sqlalchemy import ( ... Table, ... Column, ... MetaData, ... create_engine, ... PickleType, ... Integer, ... ) >>> metadata = MetaData() >>> my_table = Table( ... "my_table", metadata, Column("id", Integer), Column("data", PickleType) ... ) >>> engine = create_engine("sqlite://", echo="debug") >>> my_table.create(engine) INFO sqlalchemy.engine.base.Engine CREATE TABLE my_table ( id INTEGER, data BLOB )
在上面,我们使用了PickleType
,这是一个在LargeBinary
数据类型上工作的TypeDecorator
,在 SQLite 中对应于数据库类型BLOB
。在 CREATE TABLE 中,我们看到使用了BLOB
数据类型。SQLite 数据库对我们使用的PickleType
一无所知。
如果我们查看my_table.c.data.type
的数据类型,因为这是我们直接创建的一个 Python 对象,它是PickleType
:
>>> my_table.c.data.type PickleType()
然而,如果我们使用反射创建另一个Table
实例,我们创建的 SQLite 数据库中不会表示使用了PickleType
;相反,我们会得到BLOB
:
>>> metadata_two = MetaData() >>> my_reflected_table = Table("my_table", metadata_two, autoload_with=engine) INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("my_table") INFO sqlalchemy.engine.base.Engine () DEBUG sqlalchemy.engine.base.Engine Col ('cid', 'name', 'type', 'notnull', 'dflt_value', 'pk') DEBUG sqlalchemy.engine.base.Engine Row (0, 'id', 'INTEGER', 0, None, 0) DEBUG sqlalchemy.engine.base.Engine Row (1, 'data', 'BLOB', 0, None, 0) >>> my_reflected_table.c.data.type BLOB()
通常,当应用程序定义具有自定义类型的显式Table
元数据时,没有必要使用表反射,因为必要的Table
元数据已经存在。但是,对于需要同时使用显式Table
元数据(其中包括自定义的 Python 级别数据类型)以及设置其Column
对象作为从数据库反映出的Table
对象的应用程序或它们的组合的情况,仍然需要展示自定义数据类型的附加 Python 行为,必须采取额外的步骤来允许这样做。
最简单的方法是根据重写反射列中描述的重写特定列。在这种技术中,我们简单地使用反射与那些我们想要使用自定义或装饰的数据类型的列的显式Column
对象结合使用:
>>> metadata_three = MetaData() >>> my_reflected_table = Table( ... "my_table", ... metadata_three, ... Column("data", PickleType), ... autoload_with=engine, ... )
上面的my_reflected_table
对象被反射,并将从 SQLite 数据库加载“id”列的定义。但对于“data”列,我们已经使用了一个显式的Column
定义来覆盖反射对象,其中包括我们想要的 Python 数据类型,PickleType
。反射过程将保持此Column
对象不变:
>>> my_reflected_table.c.data.type PickleType()
一个更加详细的将数据库原生类型对象转换为自定义数据类型的方法是使用DDLEvents.column_reflect()
事件处理程序。例如,如果我们知道我们想要所有的BLOB
数据类型实际上是PickleType
,我们可以设置一个全局规则:
from sqlalchemy import BLOB from sqlalchemy import event from sqlalchemy import PickleType from sqlalchemy import Table @event.listens_for(Table, "column_reflect") def _setup_pickletype(inspector, table, column_info): if isinstance(column_info["type"], BLOB): column_info["type"] = PickleType()
当上述代码在任何表反射发生之前被调用之前(还要注意它应该仅在应用程序中调用一次,因为它是一个全局规则)时,当反射包含有BLOB
数据类型的任何Table
时,结果数据类型将存储在Column
对象中,作为PickleType
。
在实践中,上述基于事件的方法可能会有额外的规则,以便只影响那些数据类型重要的列,比如表名和可能列名的查找表,或者其他启发式方法,以准确确定应该用 Python 数据类型来建立哪些列。
重写类型编译
经常需要强制使用类型的“字符串”版本,即在 CREATE TABLE 语句或其他 SQL 函数(如 CAST)中呈现的版本进行更改。例如,应用程序可能希望强制对所有平台渲染BINARY
,除了一个平台外,该平台希望渲染BLOB
。对于大多数用例,使用现有的通用类型,例如LargeBinary
,更为合适。但是为了更准确地控制类型,可以将每个方言的编译指令与任何类型相关联:
from sqlalchemy.ext.compiler import compiles from sqlalchemy.types import BINARY @compiles(BINARY, "sqlite") def compile_binary_sqlite(type_, compiler, **kw): return "BLOB"
上面的代码允许使用BINARY
,在除 SQLite 以外的所有后端中,它将产生字符串BINARY
,而在 SQLite 中,它将产生BLOB
。
请参阅 更改类型的编译 部分,自定义 SQL 构造和编译扩展 的一个子节,以获取其他示例。
SqlAlchemy 2.0 中文文档(四十二)(3)https://developer.aliyun.com/article/1563014