SqlAlchemy 2.0 中文文档(三十)(3)https://developer.aliyun.com/article/1562437
使用具有显式声明的 Automap
正如之前所指出的,automap 不依赖于反射,并且可以利用 Table
对象集合中的任何对象在 MetaData
集合中。由此可见,automap 也可以用于生成缺失的关系,只要有一个完全定义了表元数据的完整模型:
from sqlalchemy.ext.automap import automap_base from sqlalchemy import Column, Integer, String, ForeignKey Base = automap_base() class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String) class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(ForeignKey("user.id")) # produce relationships Base.prepare() # mapping is complete, with "address_collection" and # "user" relationships a1 = Address(email="u1") a2 = Address(email="u2") u1 = User(address_collection=[a1, a2]) assert a1.user is u1
在上面的例子中,给定了大部分完整的 User
和 Address
映射,我们在 Address.user_id
上定义的 ForeignKey
允许在映射类上生成一个双向关系对 Address.user
和 User.address_collection
。
请注意,当子类化AutomapBase
时,需要调用AutomapBase.prepare()
方法;如果未调用,则我们声明的类处于未映射状态。
拦截列定义
MetaData
和Table
对象支持一个事件钩子DDLEvents.column_reflect()
,可用于在构建Column
对象之前拦截有关数据库列的反射信息。例如,如果我们想要使用命名约定来映射列,例如"attr_"
,则可以应用该事件如下:
@event.listens_for(Base.metadata, "column_reflect") def column_reflect(inspector, table, column_info): # set column.key = "attr_<lower_case_name>" column_info["key"] = "attr_%s" % column_info["name"].lower() # run reflection Base.prepare(autoload_with=engine)
版本 1.4.0b2 中的新内容:DDLEvents.column_reflect()
事件可以应用于一个MetaData
对象。
另请参阅
DDLEvents.column_reflect()
从反射表自动命名方案 - 在 ORM 映射文档中
API 参考
对象名称 | 描述 |
automap_base([declarative_base], **kw) | 生成一个声明式自动映射基类。 |
AutomapBase | 用于“自动映射”模式的基类。 |
classname_for_table(base, tablename, table) | 给定表名,返回应该使用的类名。 |
generate_relationship(base, direction, return_fn, attrname, …, **kw) | 代表两个映射类生成一个relationship() 或者backref() 。 |
name_for_collection_relationship(base, local_cls, referred_cls, constraint) | 返回用于从一个类引用另一个类的属性名称,用于集合引用。 |
name_for_scalar_relationship(base, local_cls, referred_cls, constraint) | 返回用于从一个类引用另一个类的属性名称,用于标量对象引用。 |
function sqlalchemy.ext.automap.automap_base(declarative_base: Type[Any] | None = None, **kw: Any) → Any
生成一个声明式自动映射基类。
此函数生成一个新的基类,该基类是由 AutomapBase
类和由 declarative_base()
产生的声明性基类的产品。
除了 declarative_base
外的所有参数都是直接传递给 declarative_base()
函数的关键字参数。
参数:
declarative_base
– 由declarative_base()
产生的现有类。当传递此参数时,函数不再调用declarative_base()
自身,并且所有其他关键字参数都将被忽略。**kw
– 关键字参数会传递给declarative_base()
。
class sqlalchemy.ext.automap.AutomapBase
用于“automap”模式的基类。
AutomapBase
类可以与由 declarative_base()
函数产生的“声明性基类”类进行比较。在实践中,AutomapBase
类始终与实际的声明性基类一起使用作为混入。
一个新的可子类化的 AutomapBase
通常是使用 automap_base()
函数实例化的。
成员
by_module, classes, metadata, prepare()
另请参阅
Automap
attribute by_module: ClassVar[ByModuleProperties]
包含点分隔的模块名称的层次结构,链接到类的 Properties
实例。
这个集合是 AutomapBase.classes
集合的一种替代方法,当利用 AutomapBase.prepare.modulename_for_table
参数时,该参数将为生成的类应用不同的 __module__
属性。
automap 生成类的默认 __module__
是 sqlalchemy.ext.automap
;要使用 AutomapBase.by_module
访问此命名空间,看起来像这样:
User = Base.by_module.sqlalchemy.ext.automap.User
如果一个类的 __module__
是 mymodule.account
,访问此命名空间看起来像这样:
MyClass = Base.by_module.mymodule.account.MyClass
2.0 版中的新功能。
另请参阅
从多个模式生成映射
attribute classes: ClassVar[Properties[Type[Any]]]
包含类的 Properties
实例。
此对象的行为类似于表上的 .c
集合。类以其给定的名称存在,例如:
Base = automap_base() Base.prepare(autoload_with=some_engine) User, Address = Base.classes.User, Base.classes.Address
对于与 Properties
方法名重叠的类名,例如 items()
,也支持使用 getitem 形式:
Item = Base.classes["items"]
attribute metadata: ClassVar[MetaData]
指的是将用于新 Table
对象的 MetaData
集合。
另请参见
访问表和元数据
classmethod prepare(autoload_with: Engine | None = None, engine: Any | None = None, reflect: bool = False, schema: str | None = None, classname_for_table: PythonNameForTableType | None = None, modulename_for_table: PythonNameForTableType | None = None, collection_class: Any | None = None, name_for_scalar_relationship: NameForScalarRelationshipType | None = None, name_for_collection_relationship: NameForCollectionRelationshipType | None = None, generate_relationship: GenerateRelationshipType | None = None, reflection_options: Dict[_KT, _VT] | immutabledict[_KT, _VT] = {}) → None
从 MetaData
中提取映射类和关系,并执行映射。
有关完整文档和示例,请参见 基本使用。
参数:
autoload_with
– 使用与其执行模式反射的Engine
或Connection
;当指定时,MetaData.reflect()
方法将在此方法的范围内调用。engine
–
已弃用;使用AutomapBase.autoload_with
。用于指示在反映表时使用的Engine
或Connection
,如果AutomapBase.reflect
为 True。
自版本 1.4 起已弃用:AutomapBase.prepare.engine
参数已弃用,并将在未来版本中删除。请使用AutomapBase.prepare.autoload_with
参数。reflect
–
已弃用;使用AutomapBase.autoload_with
。指示是否应调用MetaData.reflect()
。
自版本 1.4 起已弃用:AutomapBase.prepare.reflect
参数已弃用,并将在未来版本中删除。当传递了AutomapBase.prepare.autoload_with
时启用反射。classname_for_table
– 一个可调用的函数,将根据表名生成新类名。默认为classname_for_table()
。modulename_for_table
–
可调用函数,用于为内部生成的类生成有效的__module__
,以允许在单个自动映射基类中具有相同名称的多个类,这些类将位于不同的“模块”中。
默认为None
,表示__module__
不会被显式设置;Python 运行时将为这些类使用值sqlalchemy.ext.automap
。
在为生成的类分配__module__
时,可以基于点分隔的模块名称使用AutomapBase.by_module
集合访问它们。使用此钩子分配了显式__module_
的类不会放入AutomapBase.classes
集合中,而只会放入AutomapBase.by_module
中。
2.0 版中的新功能。
另请参见
从多个模式生成映射name_for_scalar_relationship
– 可调用函数,用于为标量关系生成关系名称。默认为name_for_scalar_relationship()
。name_for_collection_relationship
– 可调用函数,用于为面向集合的关系生成关系名称。默认为name_for_collection_relationship()
。generate_relationship
– 可调用函数,用于实际生成relationship()
和backref()
构造。默认为generate_relationship()
。collection_class
– 当创建代表集合的新relationship()
对象时将使用的 Python 集合类。默认为list
。schema
–
反映表时要反映的模式名称,使用AutomapBase.prepare.autoload_with
参数。该名称传递给MetaData.reflect()
的MetaData.reflect.schema
参数。当省略时,将使用数据库连接使用的默认模式。
注意AutomapBase.prepare.schema
参数支持一次反射单个模式。要包含来自多个模式的表,请多次调用AutomapBase.prepare()
。
有关多模式自动映射的概述,包括使用附加命名约定解决表名冲突的方法,请参阅从多个模式生成映射 部分。
新版本 2.0 中:AutomapBase.prepare()
可以直接调用任意次数,并跟踪已处理的表,以避免再次处理它们。reflection_options
–
当存在时,此选项字典将传递给MetaData.reflect()
以提供通用的反射特定选项,如only
和/或特定于方言的选项,如oracle_resolve_synonyms
。
新版本 1.4 中。
function sqlalchemy.ext.automap.classname_for_table(base: Type[Any], tablename: str, table: Table) → str
返回应使用的类名,给定表的名称。
默认实现为:
return str(tablename)
可以使用 AutomapBase.prepare.classname_for_table
参数指定替代实现。
参数:
base
– 进行准备的AutomapBase
类。tablename
–Table
的字符串名称。table
–Table
对象本身。
返回:
一个字符串类名。
注意
在 Python 2 中,用于类名的字符串 必须 是非 Unicode 对象,例如 str()
对象。Table
的 .name
属性通常是 Python unicode 子类,因此应在考虑任何非 ASCII 字符后,应用 str()
函数到此名称。
function sqlalchemy.ext.automap.name_for_scalar_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str
返回应用于从一个类到另一个类的引用的属性名称,用于标量对象引用。
默认实现为:
return referred_cls.__name__.lower()
可以使用 AutomapBase.prepare.name_for_scalar_relationship
参数指定替代实现。
参数:
base
– 进行准备的AutomapBase
类。local_cls
– 映射到本地方的类。referred_cls
– 映射到引用方的类。constraint
– 正在检查以生成此关系的ForeignKeyConstraint
。
function sqlalchemy.ext.automap.name_for_collection_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str
返回应用于从一个类到另一个类的引用的属性名称,用于集合引用。
默认实现如下:
return referred_cls.__name__.lower() + "_collection"
可以使用AutomapBase.prepare.name_for_collection_relationship
参数指定替代实现。
参数:
base
– 执行准备工作的AutomapBase
类。local_cls
– 要映射到本地方的类。referred_cls
– 要映射到引用方的类。constraint
– 正在检查以生成此关系的ForeignKeyConstraint
。
function sqlalchemy.ext.automap.generate_relationship(base: Type[Any], direction: RelationshipDirection, return_fn: Callable[..., Relationship[Any]] | Callable[..., ORMBackrefArgument], attrname: str, local_cls: Type[Any], referred_cls: Type[Any], **kw: Any) → Relationship[Any] | ORMBackrefArgument
代表两个映射类生成一个relationship()
或backref()
。
可以使用AutomapBase.prepare.generate_relationship
参数指定此函数的替代实现。
此函数的默认实现如下:
if return_fn is backref: return return_fn(attrname, **kw) elif return_fn is relationship: return return_fn(referred_cls, **kw) else: raise TypeError("Unknown relationship function: %s" % return_fn)
参数:
base
– 执行准备工作的AutomapBase
类。direction
– 指示关系的“方向”; 这将是ONETOMANY
、MANYTOONE
、MANYTOMANY
之一。return_fn
– 默认用于创建关系的函数。这将是relationship()
或backref()
之一。backref()
函数的结果将用于在第二步生成新的relationship()
,因此如果使用自定义关系函数,则用户定义的实现必须正确区分这两个函数。attrname
– 正在分配此关系的属性名称。如果generate_relationship.return_fn
的值是backref()
函数,则此名称是分配给反向引用的名称。local_cls
– 此关系或反向引用将在本地存在的“本地”类。referred_cls
– 此关系或反向引用所指向的“引用”类。**kw
– 所有附加的关键字参数都将传递给该函数。
返回值:
relationship()
或 backref()
构造,由 generate_relationship.return_fn
参数所指定。
烘焙查询
baked
为Query
对象提供了一种替代的创建模式,允许缓存对象的构建和字符串编译步骤。这意味着对于一个特定的Query
构建场景,如果该场景被多次使用,那么从初始构建查询到生成 SQL 字符串所涉及的所有 Python 函数调用将只会发生一次,而不是每次构建和执行查询时都会发生。
这个系统的理念是极大地减少 Python 解释器在发出 SQL 之前发生的一切的开销。 “baked”系统的缓存不会以任何方式减少 SQL 调用或缓存来自数据库的返回结果。一个展示 SQL 调用和结果集本身缓存的技术在 Dogpile Caching 中可用。
从版本 1.4 开始弃用:SQLAlchemy 1.4 和 2.0 具有全新的直接查询缓存系统,不再需要BakedQuery
系统。现在,对于所有 Core 和 ORM 查询,缓存现在是透明激活的,用户不需要采取任何操作,使用在 SQL Compilation Caching 中描述的系统。
深度炼金术
sqlalchemy.ext.baked
扩展不适合初学者。正确使用它需要对 SQLAlchemy、数据库驱动程序以及后端数据库之间的交互有很好的高级理解。这个扩展提供了一种非常特定的优化,通常是不需要的。如上所述,它不会缓存查询,只会缓存 SQL 本身的字符串形式。
概要
使用 baked 系统的开始是生成所谓的“面包店”,它代表了一系列特定查询对象的存储:
from sqlalchemy.ext import baked bakery = baked.bakery()
上述的“面包店”将缓存数据存储在一个默认为 200 个元素的 LRU 缓存中,需要注意的是 ORM 查询通常会包含一个用于调用 ORM 查询的条目,以及每个数据库方言的 SQL 字符串的一个条目。
该面包店允许我们通过指定其构造方式为一系列 Python 可调用对象(通常为 lambda 函数)来构建一个Query
对象。为了简洁使用,它重写了+=
运算符,使得典型的查询构建看起来像下面这样:
from sqlalchemy import bindparam def search_for_user(session, username, email=None): baked_query = bakery(lambda session: session.query(User)) baked_query += lambda q: q.filter(User.name == bindparam("username")) baked_query += lambda q: q.order_by(User.id) if email: baked_query += lambda q: q.filter(User.email == bindparam("email")) result = baked_query(session).params(username=username, email=email).all() return result
以下是关于上述代码的一些观察:
baked_query
对象是BakedQuery
的一个实例。该对象本质上是一个真正的 ormQuery
对象的“构建者”,但它本身并不是实际的Query
对象。- 实际的
Query
对象根本没有构建,直到在函数的最后调用Result.all()
时。 - 添加到
baked_query
对象的步骤都表示为 Python 函数,通常是 lambda。传递给bakery()
函数的第一个 lambda 接收一个Session
作为其参数。其余的 lambda 每个接收一个Query
作为其参数。 - 在上述代码中,即使我们的应用程序可能多次调用
search_for_user()
,即使在每次调用中我们都建立一个全新的BakedQuery
对象,所有的 lambda 只调用一次。只要此查询在烘培中被缓存,每个 lambda 在此期间都不会被第二次调用。 - 缓存是通过存储lambda 对象本身的引用来实现的,以便构建缓存键;也就是说,Python 解释器将这些函数分配为 Python 标识,这决定了如何在后续运行中识别查询。对于那些指定了
email
参数的search_for_user()
调用,可调用的lambda q: q.filter(User.email == bindparam('email'))
将成为被检索到的缓存键的一部分;当email
为None
时,这个可调用函数不会成为缓存键的一部分。 - 由于 lambda 都只调用一次,因此在 lambda 内部不得引用可能跨调用改变的变量;相反,假设这些是要绑定到 SQL 字符串中的值,我们使用
bindparam()
构建命名参数,稍后使用Result.params()
应用它们的实际值。
性能
烘焙查询可能看起来有些奇怪、有些笨拙、有些冗长。然而,对于在应用程序中多次调用的查询,Python 性能的节约非常显著。在 性能 中演示的示例套件 short_selects
说明了查询的比较,每个查询仅返回一行,如下所示的常规查询:
session = Session(bind=engine) for id_ in random.sample(ids, n): session.query(Customer).filter(Customer.id == id_).one()
相比于等效的“烘焙”查询:
bakery = baked.bakery() s = Session(bind=engine) for id_ in random.sample(ids, n): q = bakery(lambda s: s.query(Customer)) q += lambda q: q.filter(Customer.id == bindparam("id")) q(s).params(id=id_).one()
对于每个块的 10000 次调用的 Python 函数调用计数的差异为:
test_baked_query : test a baked query of the full entity. (10000 iterations); total fn calls 1951294 test_orm_query : test a straight ORM query of the full entity. (10000 iterations); total fn calls 7900535
以强大的笔记本电脑上的秒数来看,这是这样的:
test_baked_query : test a baked query of the full entity. (10000 iterations); total time 2.174126 sec test_orm_query : test a straight ORM query of the full entity. (10000 iterations); total time 7.958516 sec
请注意,此测试非常有意地包含了仅返回一行的查询。对于返回许多行的查询,烘焙查询的性能优势将越来越小,与获取行所花费的时间成比例。必须牢记的是,烘焙查询功能仅适用于构建查询本身,而不适用于获取结果。使用烘焙特性绝不是对更快应用程序的担保;它只是一种潜在有用的功能,适用于那些已经被证明受到这种特定形式的开销影响的应用程序。
理由
上述“lambda”方法是更传统的“参数化”方法的一个超集。假设我们希望构建一个简单的系统,在该系统中我们仅构建一次Query
,然后将其存储在字典中以供重复使用。现在就可以通过简单地构建查询并通过调用my_cached_query = query.with_session(None)
来移除其Session
来实现这一点:
my_simple_cache = {} def lookup(session, id_argument): if "my_key" not in my_simple_cache: query = session.query(Model).filter(Model.id == bindparam("id")) my_simple_cache["my_key"] = query.with_session(None) else: query = my_simple_cache["my_key"].with_session(session) return query.params(id=id_argument).all()
上述方法为我们带来了非常小的性能优势。通过重用Query
,我们节省了session.query(Model)
构造函数内部的 Python 工作以及调用filter(Model.id == bindparam('id'))
,这将跳过为我们构建核心表达式以及将其发送到Query.filter()
的过程。然而,该方法仍然每次调用Query.all()
时重新生成完整的Select
对象,并且每次都将此全新的Select
发送到字符串编译步骤,对于像上面这样的简单情况,这可能约占开销的 70%。
为了减少额外的开销,我们需要一些更专门的逻辑,一些记忆构造选择对象和 SQL 构造的方法。在维基百科的 BakedQuery 部分有一个示例,这是这个特性的前身,但在那个系统中,我们没有缓存查询的构造。为了去除所有开销,我们需要缓存查询的构造以及 SQL 编译。假设我们按照这种方式调整了配方,并制作了一个 .bake()
方法,用于预编译查询的 SQL,生成一个可以以最小开销调用的新对象。我们的例子变成了:
my_simple_cache = {} def lookup(session, id_argument): if "my_key" not in my_simple_cache: query = session.query(Model).filter(Model.id == bindparam("id")) my_simple_cache["my_key"] = query.with_session(None).bake() else: query = my_simple_cache["my_key"].with_session(session) return query.params(id=id_argument).all()
在上述例子中,我们已经解决了性能问题,但我们仍然需要处理这个字符串缓存键。
我们可以使用“面包店”方法来重新构建上面的内容,使其看起来不像“逐步建立 lambda”方法那样不寻常,而更像是对简单“重用查询”方法的简单改进:
bakery = baked.bakery() def lookup(session, id_argument): def create_model_query(session): return session.query(Model).filter(Model.id == bindparam("id")) parameterized_query = bakery.bake(create_model_query) return parameterized_query(session).params(id=id_argument).all()
在上述示例中,我们使用“烘焙”系统的方式与简单的“缓存查询”系统非常相似。但是,它使用了两行代码少,不需要制造一个“my_key”的缓存键,还包括与我们自定义的“烘焙”函数相同的功能,该函数从查询的构造函数到过滤器调用再到Select
对象的生成,再到字符串编译步骤,都缓存了 100% 的 Python 调用工作。
从上面的内容,如果我们问自己,“如果查找需要根据查询结构做条件决策怎么办?”,这就是为什么“烘焙”是这样的方式的地方。我们可以从任意数量的函数构建参数化查询,而不是从一个函数(这是我们最初认为烘焙可能的工作方式)开始。考虑我们的简单例子,如果我们需要在条件基础上在查询中添加一个附加子句:
my_simple_cache = {} def lookup(session, id_argument, include_frobnizzle=False): if include_frobnizzle: cache_key = "my_key_with_frobnizzle" else: cache_key = "my_key_without_frobnizzle" if cache_key not in my_simple_cache: query = session.query(Model).filter(Model.id == bindparam("id")) if include_frobnizzle: query = query.filter(Model.frobnizzle == True) my_simple_cache[cache_key] = query.with_session(None).bake() else: query = my_simple_cache[cache_key].with_session(session) return query.params(id=id_argument).all()
我们的“简单”参数化系统现在必须负责生成考虑到“include_frobnizzle”标志是否已传递的缓存键,因为此标志的存在意味着生成的 SQL 将完全不同。很明显,随着查询构建复杂性的提高,缓存这些查询的任务会非常快地变得繁重。我们可以将上述示例转换为以下对“面包店”直接使用:
bakery = baked.bakery() def lookup(session, id_argument, include_frobnizzle=False): def create_model_query(session): return session.query(Model).filter(Model.id == bindparam("id")) parameterized_query = bakery.bake(create_model_query) if include_frobnizzle: def include_frobnizzle_in_query(query): return query.filter(Model.frobnizzle == True) parameterized_query = parameterized_query.with_criteria( include_frobnizzle_in_query ) return parameterized_query(session).params(id=id_argument).all()
在上述示例中,我们再次缓存的不仅是查询对象,还有它需要执行的所有工作以生成 SQL。我们也不再需要处理确保生成准确考虑到我们所做的所有结构修改的缓存键;这现在是自动处理的,而且没有错误的机会。
此代码示例比朴素示例少了几行代码,消除了处理缓存键的需求,并且具有完整的所谓“已烘焙”功能的巨大性能优势。但仍然有点啰嗦!因此,我们将像BakedQuery.add_criteria()
和BakedQuery.with_criteria()
这样的方法简化为操作符,并鼓励(尽管绝对不是必需的!)使用简单的 lambda 函数,仅作为减少冗长性的手段:
bakery = baked.bakery() def lookup(session, id_argument, include_frobnizzle=False): parameterized_query = bakery.bake( lambda s: s.query(Model).filter(Model.id == bindparam("id")) ) if include_frobnizzle: parameterized_query += lambda q: q.filter(Model.frobnizzle == True) return parameterized_query(session).params(id=id_argument).all()
在上述情况中,该方法更容易实现,并且在代码流程上更类似于非缓存查询函数的情况,因此使代码更易于移植。
上述描述本质上是对到达当前“已烘焙”方法的设计过程的总结。从“正常”方法开始,还需要解决缓存键的构建和管理、移除所有冗余的 Python 执行以及需要使用条件构建的查询等附加问题,从而导致了最终的方法。
SqlAlchemy 2.0 中文文档(三十)(5)https://developer.aliyun.com/article/1562439