SqlAlchemy 2.0 中文文档(四十二)(2)

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: SqlAlchemy 2.0 中文文档(四十二)

SqlAlchemy 2.0 中文文档(四十二)(1)https://developer.aliyun.com/article/1563012


应用 SQL 级别的绑定/结果处理

如在扩展现有类型一节中所见,SQLAlchemy 允许在参数发送到语句时以及从数据库加载结果行时调用 Python  函数,以对发送到或从数据库的值应用转换。还可以定义 SQL  级别的转换。其理念在于,当只有关系数据库包含一系列必要的函数来在应用程序和持久性格式之间强制转换传入和传出数据时。示例包括使用数据库定义的加密/解密函数,以及处理地理数据的存储过程。

任何 TypeEngineUserDefinedTypeTypeDecorator 的子类都可以包含 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_encryptpgp_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,该参数指的是正在编译的类型的拥有表达式,例如 Columncast() 构造。只有在方法接受关键字参数(例如 **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.UserDefinedTypesqlalchemy.types.ExternalTypesqlalchemy.types.TypeEngineMixinsqlalchemy.types.TypeEnginesqlalchemy.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 标志泛化到了 TypeDecoratorUserDefinedType 类。

另请参见

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

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
4月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(三十八)(4)
SqlAlchemy 2.0 中文文档(三十八)
42 0
|
4月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(三十七)(2)
SqlAlchemy 2.0 中文文档(三十七)
40 2
|
4月前
|
SQL 存储 缓存
SqlAlchemy 2.0 中文文档(三十七)(3)
SqlAlchemy 2.0 中文文档(三十七)
33 1
|
4月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(三十七)(5)
SqlAlchemy 2.0 中文文档(三十七)
21 1
|
4月前
|
SQL 存储 缓存
SqlAlchemy 2.0 中文文档(三十七)(4)
SqlAlchemy 2.0 中文文档(三十七)
49 1
|
4月前
|
SQL 缓存 JSON
SqlAlchemy 2.0 中文文档(四十二)(1)
SqlAlchemy 2.0 中文文档(四十二)
41 0
|
4月前
|
SQL 存储 缓存
SqlAlchemy 2.0 中文文档(四十二)(3)
SqlAlchemy 2.0 中文文档(四十二)
46 0
|
4月前
|
存储 JSON 数据库
SqlAlchemy 2.0 中文文档(四十二)(4)
SqlAlchemy 2.0 中文文档(四十二)
42 0
|
4月前
|
SQL 缓存 数据库
SqlAlchemy 2.0 中文文档(四十二)(5)
SqlAlchemy 2.0 中文文档(四十二)
30 0
|
4月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(四十六)(5)
SqlAlchemy 2.0 中文文档(四十六)
34 0