SqlAlchemy 2.0 中文文档(三十)

2024-06-26 15:32:23 浏览数 (1)

原文:docs.sqlalchemy.org/en/20/contents.html

Automap

原文:docs.sqlalchemy.org/en/20/orm/extensions/automap.html

定义一个扩展到sqlalchemy.ext.declarative系统的系统,自动生成从数据库模式到映射类和关系,通常而不一定是一个反射的数据库模式。

希望AutomapBase系统提供了一个快速和现代化的解决方案,解决了非常著名的SQLSoup也试图解决的问题,即从现有数据库动态生成快速和基本的对象模型。通过严格在映射器配置级别解决该问题,并与现有的声明类技术完全集成,AutomapBase试图提供一个与问题紧密集成的方法,以迅速自动生成临时映射。

提示

Automap 扩展针对“零声明”方法,其中可以从数据库模式动态生成包括类和预命名关系在内的完整 ORM 模型。对于仍希望使用显式类声明以及与表反射结合使用的显式关系定义的应用程序,描述在使用 DeferredReflection 中的DeferredReflection类是更好的选择。

基本用法

最简单的用法是将现有数据库反映到一个新模型中。我们创建一个新的AutomapBase类,方式类似于我们创建声明性基类,使用automap_base()。然后,我们调用AutomapBase.prepare()在生成的基类上,要求它反映模式并生成映射:

代码语言:javascript复制
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

Base = automap_base()

# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("sqlite:///mydatabase.db")

# reflect the tables
Base.prepare(autoload_with=engine)

# mapped classes are now created with names by default
# matching that of the table name.
User = Base.classes.user
Address = Base.classes.address

session = Session(engine)

# rudimentary relationships are produced
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
session.commit()

# collection-based relationships are by default named
# "<classname>_collection"
u1 = session.query(User).first()
print(u1.address_collection)

在上面,调用AutomapBase.prepare()并传递AutomapBase.prepare.reflect参数,表示将在此声明基类的MetaData集合上调用MetaData.reflect()方法; 然后,MetaData中的每个** viable **Table都将自动生成一个新的映射类。将连接各个表的ForeignKeyConstraint对象将用于在类之间生成新的双向relationship()对象。类和关系遵循一个默认命名方案,我们可以自定义。在这一点上,我们基本的映射包含了相关的UserAddress类,可以以传统方式使用。

注意

通过** viable **,我们指的是表必须指定主键才能进行映射。此外,如果检测到表是两个其他表之间的纯关联表,则不会直接映射该表,而是将其配置为两个引用表的映射之间的多对多表。

从现有元数据生成映射

我们可以将预先声明的MetaData对象传递给automap_base()。该对象可以以任何方式构造,包括以编程方式、从序列化文件或从使用MetaData.reflect()反映的自身构造。下面我们演示了反射和显式表声明的组合:

代码语言:javascript复制
from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
from sqlalchemy.ext.automap import automap_base

engine = create_engine("sqlite:///mydatabase.db")

# produce our own MetaData object
metadata = MetaData()

# we can reflect it ourselves from a database, using options
# such as 'only' to limit what tables we look at...
metadata.reflect(engine, only=["user", "address"])

# ... or just define our own Table objects with it (or combine both)
Table(
    "user_order",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("user_id", ForeignKey("user.id")),
)

# we can then produce a set of mappings from this MetaData.
Base = automap_base(metadata=metadata)

# calling prepare() just sets up mapped classes and relationships.
Base.prepare()

# mapped classes are ready
User = Base.classes.user
Address = Base.classes.address
Order = Base.classes.user_order

从多个模式生成映射

当使用反射时,AutomapBase.prepare() 方法一次最多只能从一个模式中反射表,使用 AutomapBase.prepare.schema 参数来指示要从中反射的模式的名称。为了从多个模式中填充 AutomapBase 中的表,可以多次调用 AutomapBase.prepare(),每次将不同的名称传递给 AutomapBase.prepare.schema 参数。AutomapBase.prepare() 方法会保持一个内部列表,其中包含已经映射过的 Table 对象,并且只会为自上次运行 AutomapBase.prepare() 以来新增的那些 Table 对象添加新的映射:

代码语言:javascript复制
e = create_engine("postgresql://scott:tiger@localhost/test")

Base.metadata.create_all(e)

Base = automap_base()

Base.prepare(e)
Base.prepare(e, schema="test_schema")
Base.prepare(e, schema="test_schema_2")

新版本 2.0 中新增了 AutomapBase.prepare() 方法,可以任意调用;每次运行时只会映射新增的表。在 1.4 版本及之前的版本中,多次调用会导致错误,因为它会尝试重新映射已经映射过的类。之前的解决方法是直接调用 MetaData.reflect(),该方法仍然可用。

跨多个模式自动映射同名表

对于多个模式可能有同名表的常见情况,因此可能生成同名类,可以通过使用 AutomapBase.prepare.classname_for_table 钩子在每个模式基础上应用不同的类名来解决冲突,或者通过使用 AutomapBase.prepare.modulename_for_table 钩子来解决同名类的歧义,该钩子允许通过更改它们的有效 __module__ 属性来区分同名类。在下面的示例中,此钩子用于为所有类创建一个 __module__ 属性,其形式为 mymodule.<schemaname>,如果没有模式,则使用模式名称 default

代码语言:javascript复制
e = create_engine("postgresql://scott:tiger@localhost/test")

Base.metadata.create_all(e)

def module_name_for_table(cls, tablename, table):
    if table.schema is not None:
        return f"mymodule.{table.schema}"
    else:
        return f"mymodule.default"

Base = automap_base()

Base.prepare(e, modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema", modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema_2", modulename_for_table=module_name_for_table)

同名类被组织成可在 AutomapBase.by_module 中使用的分层集合。使用特定包/模块的点分隔名称向下遍历到所需的类名。

注意

当使用 AutomapBase.prepare.modulename_for_table 钩子来返回一个不是 None 的新 __module__ 时,类不会被放入 AutomapBase.classes 集合中;只有那些没有给定显式模块名的类才会放在这里,因为该集合不能单独表示同名类。

在上面的示例中,如果数据库中包含了三个默认模式、test_schema 模式和 test_schema_2 模式中都命名为 accounts 的表,将会有三个单独的类可用,分别是:

代码语言:javascript复制
Base.by_module.mymodule.default.accounts
Base.by_module.mymodule.test_schema.accounts
Base.by_module.mymodule.test_schema_2.accounts

对于所有 AutomapBase 类生成的默认模块命名空间是 sqlalchemy.ext.automap。如果没有使用 AutomapBase.prepare.modulename_for_table 钩子,AutomapBase.by_module 的内容将完全在 sqlalchemy.ext.automap 命名空间内(例如 MyBase.by_module.sqlalchemy.ext.automap.<classname>),其中将包含与 AutomapBase.classes 中看到的相同系列的类。因此,只有在存在显式 __module__ 约定时才通常需要使用 AutomapBase.by_module

显式指定类

提示

如果在应用程序中期望显式类占据主要地位,请考虑改用 DeferredReflection

automap 扩展允许类被明确定义,类似于DeferredReflection类的方式。从AutomapBase继承的类表现得像常规的声明类,但在构建后不会立即映射,而是在调用AutomapBase.prepare()时映射。AutomapBase.prepare()方法将利用我们根据使用的表名建立的类。如果我们的模式包含表useraddress,我们可以定义要使用的一个或两个类:

代码语言:javascript复制
from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

# automap base
Base = automap_base()

# pre-declare User for the 'user' table
class User(Base):
    __tablename__ = "user"

    # override schema elements like Columns
    user_name = Column("name", String)

    # override relationships too, if desired.
    # we must use the same name that automap would use for the
    # relationship, and also must refer to the class name that automap will
    # generate for "address"
    address_collection = relationship("address", collection_class=set)

# reflect
engine = create_engine("sqlite:///mydatabase.db")
Base.prepare(autoload_with=engine)

# we still have Address generated from the tablename "address",
# but User is the same as Base.classes.User now

Address = Base.classes.address

u1 = session.query(User).first()
print(u1.address_collection)

# the backref is still there:
a1 = session.query(Address).first()
print(a1.user)

在上面,更复杂的细节之一是,我们展示了覆盖relationship()对象的过程,这是 automap 会创建的。为了做到这一点,我们需要确保名称与 automap 通常生成的名称匹配,即关系名称将是User.address_collection,而从 automap 的角度来看,所指的类的名称被称为address,尽管我们在使用这个类时将其称为Address

覆盖命名方案

automap 负责根据模式生成映射类和关系名称,这意味着它在确定这些名称时有决策点。这三个决策点是使用函数提供的,这些函数可以传递给AutomapBase.prepare()方法,并被称为classname_for_table()name_for_scalar_relationship()name_for_collection_relationship()。以下示例中提供了任意或所有这些函数,我们使用“驼峰命名法”为类名和使用 Inflect 包的“复数形式”为集合名:

代码语言:javascript复制
import re
import inflect

def camelize_classname(base, tablename, table):
    "Produce a 'camelized' class name, e.g."
    "'words_and_underscores' -> 'WordsAndUnderscores'"

    return str(
        tablename[0].upper()
          re.sub(
            r"_([a-z])",
            lambda m: m.group(1).upper(),
            tablename[1:],
        )
    )

_pluralizer = inflect.engine()

def pluralize_collection(base, local_cls, referred_cls, constraint):
    "Produce an 'uncamelized', 'pluralized' class name, e.g."
    "'SomeTerm' -> 'some_terms'"

    referred_name = referred_cls.__name__
    uncamelized = re.sub(
        r"[A-Z]",
        lambda m: "_%s" % m.group(0).lower(),
        referred_name,
    )[1:]
    pluralized = _pluralizer.plural(uncamelized)
    return pluralized

from sqlalchemy.ext.automap import automap_base

Base = automap_base()

engine = create_engine("sqlite:///mydatabase.db")

Base.prepare(
    autoload_with=engine,
    classname_for_table=camelize_classname,
    name_for_collection_relationship=pluralize_collection,
)

根据上述映射,我们现在将拥有UserAddress两个类,其中从UserAddress的集合被称为User.addresses

代码语言:javascript复制
User, Address = Base.classes.User, Base.classes.Address

u1 = User(addresses=[Address(email="foo@bar.com")])

关系检测

自动映射所实现的绝大部分是基于外键生成 relationship() 结构。其工作原理如下:

  1. 检查已知映射到特定类的给定 Table 是否存在ForeignKeyConstraint 对象。
  2. 对于每个 ForeignKeyConstraint,将匹配到的远程Table对象与其应映射到的类相匹配,如果有的话,否则将跳过。
  3. 由于我们正在检查的 ForeignKeyConstraint 对应于来自直接映射类的引用,因此关系将被设置为指向引用类的多对一关系;在引用类上将创建相应的一个对多反向引用,引用此类。
  4. 如果属于ForeignKeyConstraint 的任何列不可为空(例如 nullable=False),则将在要传递给关系或反向引用的关键字参数中添加一个 relationship.cascade 关键字参数,其值为 all, delete-orphan。如果ForeignKeyConstraint 报告对于一组非空列设置了 ForeignKeyConstraint.ondeleteCASCADE,或者对于可为空列设置了 SET NULL,则在关系关键字参数集合中将选项relationship.passive_deletes标志设置为 True。请注意,并非所有后端都支持对 ON DELETE 的反射。
  5. 关系的名称是使用AutomapBase.prepare.name_for_scalar_relationshipAutomapBase.prepare.name_for_collection_relationship可调用函数确定的。重要的是要注意,默认关系命名是从实际类名派生的。如果您通过声明给出了特定类的显式名称,或者指定了备用类命名方案,那么关系名称将从该名称派生。
  6. 对于这些名称,类被检查是否存在匹配的已映射属性。如果在一侧检测到一个,但在另一侧没有,则AutomapBase尝试在缺失的一侧创建一个关系,然后使用relationship.back_populates参数将新关系指向另一侧。
  7. 在通常情况下,如果任一侧都没有关系,则AutomapBase.prepare()会在“多对一”一侧生成一个relationship(),并使用relationship.backref参数将其与另一侧匹配。
  8. relationship()的生成以及可选地backref()的生成由AutomapBase.prepare.generate_relationship函数处理,该函数可以由最终用户提供,以增强传递给relationship()backref()的参数或者使用这些函数的自定义实现。
自定义关系参数

AutomapBase.prepare.generate_relationship 钩子可用于向关系添加参数。对于大多数情况,我们可以利用现有的 generate_relationship() 函数,在使用我们自己的参数扩充给定的关键字字典后,返回对象。

下面是如何将 relationship.cascaderelationship.passive_deletes 选项传递给所有一对多关系的示例:

代码语言:javascript复制
from sqlalchemy.ext.automap import generate_relationship
from sqlalchemy.orm import interfaces

def _gen_relationship(
    base, direction, return_fn, attrname, local_cls, referred_cls, **kw
):
    if direction is interfaces.ONETOMANY:
        kw["cascade"] = "all, delete-orphan"
        kw["passive_deletes"] = True
    # make use of the built-in function to actually return
    # the result.
    return generate_relationship(
        base, direction, return_fn, attrname, local_cls, referred_cls, **kw
    )

from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

# automap base
Base = automap_base()

engine = create_engine("sqlite:///mydatabase.db")
Base.prepare(autoload_with=engine, generate_relationship=_gen_relationship)
多对多关系

automap 将生成多对多关系,例如包含 secondary 参数的关系。生成这些关系的过程如下:

  1. 在任何映射类被分配给它之前,给定的 Table 将被检查是否包含 ForeignKeyConstraint 对象。
  2. 如果表包含两个且仅两个 ForeignKeyConstraint 对象,并且此表中的所有列都是这两个 ForeignKeyConstraint 对象的成员,则假定该表是“secondary”表,并且不会直接映射
  3. Table 所指向的两个(或一个,用于自引用)外部表将与它们将要映射到的类进行匹配,如果有的话。
  4. 如果双方的映射类位于同一位置,则在两个类之间创建一个双向的多对多 relationship() / backref() 对。
  5. 多对多的覆盖逻辑与一对多/多对一的相同;在调用 generate_relationship() 函数生成结构后,现有属性将被保留。
具有继承关系的关系

automap 不会在处于继承关系的两个类之间生成任何关系。也就是说,对于以下给定的两个类:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))
    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

EngineerEmployee 的外键不是用于建立关系,而是用于在两个类之间建立连接的继承关系。

请注意,这意味着自动映射将不会为从子类到父类的外键生成 任何 关系。如果一个映射还具有从子类到父类的实际关系,那么这些关系需要是显式的。在下面的例子中,由于 EngineerEmployee 有两个单独的外键,我们需要设置我们想要的关系以及 inherit_condition,因为这些都不是 SQLAlchemy 可以猜测的:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    favorite_employee_id = Column(Integer, ForeignKey("employee.id"))

    favorite_employee = relationship(Employee, foreign_keys=favorite_employee_id)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "inherit_condition": id == Employee.id,
    }
处理简单的命名冲突

在映射过程中如果出现命名冲突的情况,根据需要覆盖 classname_for_table()name_for_scalar_relationship()name_for_collection_relationship() 中的任何一个。例如,如果自动映射尝试将一个多对一关系命名为一个现有列相同的名称,可以有条件地选择替代约定。给定一个模式:

代码语言:javascript复制
CREATE  TABLE  table_a  (
  id  INTEGER  PRIMARY  KEY
);

CREATE  TABLE  table_b  (
  id  INTEGER  PRIMARY  KEY,
  table_a  INTEGER,
  FOREIGN  KEY(table_a)  REFERENCES  table_a(id)
);

上述模式将首先将 table_a 表自动映射为名为 table_a 的类;然后将在 table_b 的类上自动映射一个与此相关类相同名称的关系,例如 table_a。这个关系名称与映射列 table_b.table_a 冲突,并且将在映射时发出错误。

我们可以通过以下方式使用下划线解决这个冲突:

代码语言:javascript复制
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
    name = referred_cls.__name__.lower()
    local_table = local_cls.__table__
    if name in local_table.columns:
        newname = name   "_"
        warnings.warn("Already detected name %s present.  using %s" % (name, newname))
        return newname
    return name

Base.prepare(
    autoload_with=engine,
    name_for_scalar_relationship=name_for_scalar_relationship,
)

或者,我们可以在列的一侧更改名称。可以使用在 Naming Declarative Mapped Columns Explicitly 中描述的技术修改映射的列,通过将列显式地分配给一个新名称:

代码语言:javascript复制
Base = automap_base()

class TableB(Base):
    __tablename__ = "table_b"
    _table_a = Column("table_a", ForeignKey("table_a.id"))

Base.prepare(autoload_with=engine)

使用明确声明的自动映射

正如前面所述,自动映射不依赖于反射,并且可以利用MetaData 集合内的任何 Table 对象集合。由此可见,自动映射也可以在完全定义了表元数据的完整模型中生成丢失的关系:

代码语言:javascript复制
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

在上面的例子中,对于大部分完成的 UserAddress 映射,我们在 Address.user_id 上定义的 ForeignKey 允许在映射的类上生成一个双向关系对 Address.userUser.address_collection

注意,当子类化AutomapBase时,需要调用AutomapBase.prepare()方法;如果不调用,我们声明的类处于未映射状态。

拦截列定义

MetaDataTable 对象支持一个事件钩子DDLEvents.column_reflect(),可用于拦截关于数据库列反射的信息,在构建Column对象之前。例如,如果我们想要使用类似"attr_<columnname>"的命名约定来映射列,可以应用该事件:

代码语言:javascript复制
@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)

返回应用于标量对象引用的一个类到另一个类的属性名称。

代码语言:javascript复制
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()
代码语言:javascript复制
class sqlalchemy.ext.automap.AutomapBase

用于“自动映射”模式的基类。

AutomapBase 类可以与由 declarative_base() 函数生成的“声明基类”类相比较。实际上,AutomapBase 类总是与实际的声明基类一起使用作为一个 mixin。

一个新的可子类化的 AutomapBase 通常使用 automap_base() 函数实例化。

成员

by_module, classes, metadata, prepare()

另请参阅

自动映射

代码语言:javascript复制
attribute by_module: ClassVar[ByModuleProperties]

一个包含点分隔的模块名称层次结构链接到类的 Properties 实例。

这个集合是一个替代 AutomapBase.classes 集合的选择,当使用 AutomapBase.prepare.modulename_for_table 参数时,这个参数将为生成的类应用不同的 __module__ 属性。

自动映射生成的类的默认 __module__sqlalchemy.ext.automap;使用 AutomapBase.by_module 访问这个命名空间会像这样:

代码语言:javascript复制
User = Base.by_module.sqlalchemy.ext.automap.User

如果一个类的 __module__mymodule.account,访问这个命名空间会像这样:

代码语言:javascript复制
MyClass = Base.by_module.mymodule.account.MyClass

新特性在版本 2.0 中添加。

另请参阅

从多个模式生成映射

代码语言:javascript复制
attribute classes: ClassVar[Properties[Type[Any]]]

包含类的 Properties 实例。

这个对象的行为类似于表上的 .c 集合。类以它们被赋予的名称呈现,例如:

代码语言:javascript复制
Base = automap_base()
Base.prepare(autoload_with=some_engine)

User, Address = Base.classes.User, Base.classes.Address

对于类名与 Properties 方法名重叠的情况,比如 items(),也支持获取项的形式:

代码语言:javascript复制
Item = Base.classes["items"]
代码语言:javascript复制
attribute metadata: ClassVar[MetaData]

指的是将用于新 Table 对象的 MetaData 集合。

另请参见

访问表和元数据

代码语言:javascript复制
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 – 用于执行模式反射的 EngineConnection;当指定时,MetaData.reflect() 方法将在此方法的范围内调用。
  • engine – 旧版;如果 AutomapBase.reflect 为 True,则用于指示反映表的 EngineConnection。 自 1.4 版开始弃用:AutomapBase.prepare.engine 参数已弃用,并将在未来版本中移除。请使用 AutomapBase.prepare.autoload_with 参数。
  • reflect – 旧版;如果 MetaData.reflect() 应被调用,则使用 AutomapBase.autoload_with。 自 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.schema 参数的 MetaData.reflect()。当省略时,数据库连接使用的默认模式将被使用。 注意 AutomapBase.prepare.schema 参数支持一次反射单个模式。为了包含来自多个模式的表,请多次调用 AutomapBase.prepare()。 对于多模式自动映射的概述,包括使用额外命名约定解决表名冲突,请参见 从多个模式生成映射 部分。 版本 2.0 中的新功能:AutomapBase.prepare() 支持直接调用任意次数,跟踪已经处理过的表,以避免第二次处理它们。
  • reflection_options – 当存在时,此选项字典将传递给 MetaData.reflect(),以提供一般的反射特定选项,如 only 和/或特定于方言的选项,如 oracle_resolve_synonyms。 版本 1.4 中的新功能。
代码语言:javascript复制
function sqlalchemy.ext.automap.classname_for_table(base: Type[Any], tablename: str, table: Table) → str

返回给定表名时应该使用的类名。

默认实现是:

代码语言:javascript复制
return str(tablename)

可以使用 AutomapBase.prepare.classname_for_table 参数指定备用实现。

参数:

  • base – 执行准备工作的 AutomapBase 类。
  • tablenameTable 的字符串名称。
  • tableTable 对象本身。

返回:

一个字符串类名。

注意

在 Python 2 中,用于类名的字符串必须是非 Unicode 对象,例如 str() 对象。Table.name 属性通常是 Python 的 unicode 子类,因此应该在考虑任何非 ASCII 字符后,对此名称应用 str() 函数。

代码语言:javascript复制
function sqlalchemy.ext.automap.name_for_scalar_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str

返回应该用于从一个类到另一个类引用的属性名称,用于标量对象引用。

默认实现是:

代码语言:javascript复制
return referred_cls.__name__.lower()

可以使用 AutomapBase.prepare.name_for_scalar_relationship 参数指定备用实现。

参数:

  • base – 执行准备工作的 AutomapBase 类。
  • local_cls – 要映射到本地端的类。
  • referred_cls – 要映射到引用方的类。
  • constraint – 正在检查以产生此关系的ForeignKeyConstraint
代码语言:javascript复制
function sqlalchemy.ext.automap.name_for_collection_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str

返回应该用于从一个类引用到另一个类的属性名称,用于集合引用。

默认实现如下:

代码语言:javascript复制
return referred_cls.__name__.lower()   "_collection"

可以使用AutomapBase.prepare.name_for_collection_relationship参数指定备用实现。

参数:

  • base – 进行准备工作的AutomapBase类。
  • local_cls – 在本地端映射的类。
  • referred_cls – 在引用方的类。
  • constraint – 正在检查以产生此关系的ForeignKeyConstraint
代码语言:javascript复制
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参数指定备用实现。

此函数的默认实现如下:

代码语言:javascript复制
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 – 表示关系的“方向”; 这将是ONETOMANYMANYTOONEMANYTOMANY之一。
  • return_fn – 默认用于创建关系的函数。这将是relationship()backref()中的一个。backref()函数的结果将用于在第二步产生一个新的relationship(),因此如果正在使用自定义关系函数,则用户定义的实现正确区分这两个函数非常关键。
  • attrname – 正在分配此关系的属性名称。如果generate_relationship.return_fn的值是backref()函数,则此名称是分配给反向引用的名称。
  • local_cls – 此关系或反向引用将在本地存在的“本地”类。
  • referred_cls – 关系或反向引用所指向的“被引用”类。
  • **kw – 所有额外的关键字参数都将传递给函数。

返回:

一个由 generate_relationship.return_fn 参数指定的 relationship()backref() 结构。

基本用法

最简单的用法是将现有数据库反映到新模型中。我们以与创建声明性基类相似的方式创建一个新的 AutomapBase 类,使用 automap_base()。然后,我们调用 AutomapBase.prepare() 在生成的基类上,要求它反映架构并生成映射:

代码语言:javascript复制
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

Base = automap_base()

# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("sqlite:///mydatabase.db")

# reflect the tables
Base.prepare(autoload_with=engine)

# mapped classes are now created with names by default
# matching that of the table name.
User = Base.classes.user
Address = Base.classes.address

session = Session(engine)

# rudimentary relationships are produced
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
session.commit()

# collection-based relationships are by default named
# "<classname>_collection"
u1 = session.query(User).first()
print(u1.address_collection)

上面,在传递 AutomapBase.prepare.reflect 参数时调用 AutomapBase.prepare() 表示将在此声明基类的 MetaData 集合上调用 MetaData.reflect() 方法;然后,每个 viable TableMetaData 内将自动生成一个新的映射类。将连接各个表的 ForeignKeyConstraint 对象用于在类之间生成新的双向 relationship() 对象。类和关系遵循默认命名方案,我们可以自定义。在此时,我们的基本映射由相关的 UserAddress 类组成,可以像传统方式一样使用。

注意

这里的 viable 意味着要将表映射,必须指定主键。此外,如果检测到表是两个其他表之间的纯关联表,则不会直接映射,而是将其配置为两个引用表的映射之间的多对多表。

从现有的元数据生成映射

我们可以将预先声明的MetaData对象传递给automap_base()。这个对象可以以任何方式构建,包括以编程方式、从序列化文件中或者通过MetaData.reflect()自身进行反射。下面我们展示了反射和显式表声明的结合使用:

代码语言:javascript复制
from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
from sqlalchemy.ext.automap import automap_base

engine = create_engine("sqlite:///mydatabase.db")

# produce our own MetaData object
metadata = MetaData()

# we can reflect it ourselves from a database, using options
# such as 'only' to limit what tables we look at...
metadata.reflect(engine, only=["user", "address"])

# ... or just define our own Table objects with it (or combine both)
Table(
    "user_order",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("user_id", ForeignKey("user.id")),
)

# we can then produce a set of mappings from this MetaData.
Base = automap_base(metadata=metadata)

# calling prepare() just sets up mapped classes and relationships.
Base.prepare()

# mapped classes are ready
User = Base.classes.user
Address = Base.classes.address
Order = Base.classes.user_order

从多个模式生成映射

当使用反射时,AutomapBase.prepare()方法最多一次只能从一个模式中反射表,使用AutomapBase.prepare.schema参数来指示要反射的模式的名称。为了将AutomapBase填充到来自多个模式的表中,可以多次调用AutomapBase.prepare(),每次传递不同的名称给AutomapBase.prepare.schema参数。AutomapBase.prepare()方法会保留一个已经映射过的Table对象的内部列表,并且只会为那些自上次运行AutomapBase.prepare()以来新的Table对象添加新的映射:

代码语言:javascript复制
e = create_engine("postgresql://scott:tiger@localhost/test")

Base.metadata.create_all(e)

Base = automap_base()

Base.prepare(e)
Base.prepare(e, schema="test_schema")
Base.prepare(e, schema="test_schema_2")

2.0 版本新增功能:AutomapBase.prepare()方法可以被任意次数调用;每次运行只会映射新添加的表。在 1.4 版本及更早版本中,多次调用会导致错误,因为它会尝试重新映射已经映射的类。直接调用MetaData.reflect()的先前解决方法仍然可用。

在多个模式中自动映射同名表

对于常见情况,即多个模式可能具有相同命名的表,因此可能生成相同命名的类,可以通过使用AutomapBase.prepare.classname_for_table挂钩来在每个模式基础上应用不同的类名来解决冲突,或者使用AutomapBase.prepare.modulename_for_table挂钩,通过更改它们的有效__module__属性来消除同名类的歧义。在下面的示例中,此挂钩用于为所有类创建一个__module__属性,其形式为mymodule.<schemaname>,其中如果没有模式,则使用模式名为default

代码语言:javascript复制
e = create_engine("postgresql://scott:tiger@localhost/test")

Base.metadata.create_all(e)

def module_name_for_table(cls, tablename, table):
    if table.schema is not None:
        return f"mymodule.{table.schema}"
    else:
        return f"mymodule.default"

Base = automap_base()

Base.prepare(e, modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema", modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema_2", modulename_for_table=module_name_for_table)

相同命名的类被组织成一个层次化的集合,可在AutomapBase.by_module中使用。该集合使用特定包/模块的点分隔名称进行遍历,直到所需的类名。

注意

当使用AutomapBase.prepare.modulename_for_table挂钩返回一个不是None的新__module__时,类不会放置到AutomapBase.classes集合中;只有没有给定显式模块名称的类才会放在这里,因为该集合无法表示同名类。

在上面的示例中,如果数据库中包含默认模式,test_schema模式和test_schema_2模式中的一个名为accounts的表,那么将会有三个不同的类可用:

代码语言:javascript复制
Base.by_module.mymodule.default.accounts
Base.by_module.mymodule.test_schema.accounts
Base.by_module.mymodule.test_schema_2.accounts

对于所有AutomapBase类生成的默认模块命名空间是sqlalchemy.ext.automap。如果没有使用AutomapBase.prepare.modulename_for_table挂钩,则AutomapBase.by_module的内容将完全在sqlalchemy.ext.automap命名空间内(例如MyBase.by_module.sqlalchemy.ext.automap.<classname>),其中包含与AutomapBase.classes中看到的相同的一系列类。因此,通常只有在存在显式__module__约定时才需要使用AutomapBase.by_module

在跨多个模式自动映射同名表时

对于常见情况,即多个模式可能具有相同命名的表,因此会生成相同命名的类,可以通过使用AutomapBase.prepare.classname_for_table钩子来根据每个模式应用不同的类名来解决冲突,或者通过使用AutomapBase.prepare.modulename_for_table钩子来解决相同命名类的歧义问题,该钩子允许通过更改它们的有效__module__属性来区分相同命名的类。在下面的示例中,该钩子用于创建一个形式为mymodule.<schemaname>__module__属性,其中如果不存在模式,则使用模式名称default

代码语言:javascript复制
e = create_engine("postgresql://scott:tiger@localhost/test")

Base.metadata.create_all(e)

def module_name_for_table(cls, tablename, table):
    if table.schema is not None:
        return f"mymodule.{table.schema}"
    else:
        return f"mymodule.default"

Base = automap_base()

Base.prepare(e, modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema", modulename_for_table=module_name_for_table)
Base.prepare(e, schema="test_schema_2", modulename_for_table=module_name_for_table)

相同命名的类被组织成一个层次结构集合,可在AutomapBase.by_module中使用。该集合通过特定包/模块的点分隔名称向下遍历到所需的类名。

注意

当使用AutomapBase.prepare.modulename_for_table钩子返回一个不是None的新__module__时,该类不会被放置到AutomapBase.classes集合中;只有那些没有给定显式模块名的类会被放置在此处,因为集合不能单独表示同名类。

在上述示例中,如果数据库中包含了三个默认模式、test_schema模式和test_schema_2模式中都命名为accounts的表,则会分别获得三个单独的类:

代码语言:javascript复制
Base.by_module.mymodule.default.accounts
Base.by_module.mymodule.test_schema.accounts
Base.by_module.mymodule.test_schema_2.accounts

为所有AutomapBase类生成的默认模块命名空间是sqlalchemy.ext.automap。 如果未使用AutomapBase.prepare.modulename_for_table挂钩,则AutomapBase.by_module的内容将完全在sqlalchemy.ext.automap命名空间内(例如,MyBase.by_module.sqlalchemy.ext.automap.<classname>),其中包含与AutomapBase.classes中看到的相同系列的类。 因此,仅当存在显式的__module__约定时才通常需要使用AutomapBase.by_module

明确指定类

提示

如果明确的类在应用程序中占主导地位,请考虑改用DeferredReflection

automap扩展允许以与DeferredReflection类相似的方式明确定义类。 从AutomapBase继承的类表现得像常规的声明性类一样,但在构造后不会立即映射,而是在调用AutomapBase.prepare()时映射。 AutomapBase.prepare()方法将利用我们基于所使用的表名建立的类。 如果我们的模式包含表useraddress,我们可以定义要使用的一个或两个类:

代码语言:javascript复制
from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

# automap base
Base = automap_base()

# pre-declare User for the 'user' table
class User(Base):
    __tablename__ = "user"

    # override schema elements like Columns
    user_name = Column("name", String)

    # override relationships too, if desired.
    # we must use the same name that automap would use for the
    # relationship, and also must refer to the class name that automap will
    # generate for "address"
    address_collection = relationship("address", collection_class=set)

# reflect
engine = create_engine("sqlite:///mydatabase.db")
Base.prepare(autoload_with=engine)

# we still have Address generated from the tablename "address",
# but User is the same as Base.classes.User now

Address = Base.classes.address

u1 = session.query(User).first()
print(u1.address_collection)

# the backref is still there:
a1 = session.query(Address).first()
print(a1.user)

上面,更复杂的细节之一是,我们说明了如何覆盖relationship()对象之一,该对象 automap 将会创建。 为此,我们需要确保名称与 automap 通常生成的名称相匹配,即关系名称将为User.address_collection,并且从 automap 的角度来看,所引用的类的名称称为address,即使我们在对此类的使用中将其称为Address

覆盖命名方案

automap 被要求根据模式生成映射类和关系名称,这意味着它在确定这些名称的方式上有决策点。这三个决策点通过可以传递给AutomapBase.prepare()方法的函数来提供,分别称为classname_for_table()name_for_scalar_relationship()name_for_collection_relationship()。以下示例中提供了任意或全部这些函数,我们使用了“驼峰命名法”作为类名,并使用了 Inflect 包来对集合名称进行“复数化”:

代码语言:javascript复制
import re
import inflect

def camelize_classname(base, tablename, table):
    "Produce a 'camelized' class name, e.g."
    "'words_and_underscores' -> 'WordsAndUnderscores'"

    return str(
        tablename[0].upper()
          re.sub(
            r"_([a-z])",
            lambda m: m.group(1).upper(),
            tablename[1:],
        )
    )

_pluralizer = inflect.engine()

def pluralize_collection(base, local_cls, referred_cls, constraint):
    "Produce an 'uncamelized', 'pluralized' class name, e.g."
    "'SomeTerm' -> 'some_terms'"

    referred_name = referred_cls.__name__
    uncamelized = re.sub(
        r"[A-Z]",
        lambda m: "_%s" % m.group(0).lower(),
        referred_name,
    )[1:]
    pluralized = _pluralizer.plural(uncamelized)
    return pluralized

from sqlalchemy.ext.automap import automap_base

Base = automap_base()

engine = create_engine("sqlite:///mydatabase.db")

Base.prepare(
    autoload_with=engine,
    classname_for_table=camelize_classname,
    name_for_collection_relationship=pluralize_collection,
)

从上面的映射中,我们现在会有 UserAddress 两个类,其中从 UserAddress 的集合被称为 User.addresses

代码语言:javascript复制
User, Address = Base.classes.User, Base.classes.Address

u1 = User(addresses=[Address(email="foo@bar.com")])

关系检测

automap 的绝大部分工作是根据外键生成relationship()结构。它对于多对一和一对多关系的工作机制如下:

  1. 已知映射到特定类的给定Table,会被检查其是否存在ForeignKeyConstraint对象。
  2. 对于每一个ForeignKeyConstraint,远程的Table对象被匹配到其要映射的类,如果有的话,否则将被跳过。
  3. 由于我们正在检查的ForeignKeyConstraint对应于从直接映射类的引用,该关系将被设置为指向被引用类的多对一关系;在被引用的类上将创建一个相应的一对多反向引用,指向该类。
  4. 如果ForeignKeyConstraint的任何一列不可为空(例如,nullable=False),将会将all, delete-orphanrelationship.cascade关键字参数添加到要传递给关联或反向引用的关键字参数中。如果ForeignKeyConstraint报告对于一组非空列设置了CASCADE或对于可为空列设置了SET NULLForeignKeyConstraint.ondelete,则将在关系关键字参数集合中将选项relationship.passive_deletes标志设置为True。请注意,并非所有后端都支持删除操作的反射。
  5. 关联的名称是使用AutomapBase.prepare.name_for_scalar_relationshipAutomapBase.prepare.name_for_collection_relationship可调用函数确定的。重要的是要注意,默认的关联命名从实际类名派生名称。如果您通过声明为特定类指定了显式名称,或指定了替代类命名方案,则关系名称将从该名称派生。
  6. 检查类以查找与这些名称匹配的现有映射属性。如果在一侧检测到一个属性,但在另一侧没有,则AutomapBase尝试在缺失的一侧创建一个关联,然后使用relationship.back_populates参数指向新关联到另一侧。
  7. 在通常情况下,如果任何一侧都没有关联,则AutomapBase.prepare()会在“多对一”一侧产生一个relationship(),并使用relationship.backref参数将其与另一侧匹配。
  8. relationship()的生成以及可选的backref()的生成被交由AutomapBase.prepare.generate_relationship函数处理,该函数可以由最终用户提供,以增强传递给relationship()backref()的参数,或者利用这些函数的自定义实现。
自定义关系参数

AutomapBase.prepare.generate_relationship钩子可用于向关系添加参数。对于大多数情况,我们可以利用现有的generate_relationship()函数,在用自己的参数扩充给定关键字字典后返回对象。

下面是如何向所有一对多关系发送relationship.cascaderelationship.passive_deletes选项的示例:

代码语言:javascript复制
from sqlalchemy.ext.automap import generate_relationship
from sqlalchemy.orm import interfaces

def _gen_relationship(
    base, direction, return_fn, attrname, local_cls, referred_cls, **kw
):
    if direction is interfaces.ONETOMANY:
        kw["cascade"] = "all, delete-orphan"
        kw["passive_deletes"] = True
    # make use of the built-in function to actually return
    # the result.
    return generate_relationship(
        base, direction, return_fn, attrname, local_cls, referred_cls, **kw
    )

from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

# automap base
Base = automap_base()

engine = create_engine("sqlite:///mydatabase.db")
Base.prepare(autoload_with=engine, generate_relationship=_gen_relationship)
多对多关系

automap将生成多对多关系,例如包含secondary参数的关系。生成这些关系的过程如下:

  1. 给定的Table在分配任何映射类之前将被检查其ForeignKeyConstraint对象。
  2. 如果表包含两个且仅两个ForeignKeyConstraint对象,并且此表中的所有列都是这两个ForeignKeyConstraint对象的成员,则假定该表是“次要”表,并且不会直接映射
  3. Table引用的两个(对于自引用的情况则为一个)外部表会与它们将要映射到的类匹配,如果有的话。
  4. 如果两边的映射类被定位,那么在两个类之间将创建一个多对多的双向 relationship() / backref() 对。
  5. 对于多对多的覆盖逻辑与一对多/多对一的逻辑相同;调用generate_relationship() 函数来生成结构,已存在的属性将被保留。
具有继承关系的关系

automap 不会在处于继承关系的两个类之间生成任何关系。 也就是说,给定以下两个类:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))
    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

EngineerEmployee的外键不是用于关系,而是用于在两个类之间建立联合继承。

请注意,这意味着 automap 将不会为从子类到超类的外键生成 任何 关系。 如果映射还具有从子类到超类的实际关系,那么这些关系需要显式说明。 如下,由于从EngineerEmployee有两个单独的外键,我们需要设置我们想要的关系以及inherit_condition,因为这些不是 SQLAlchemy 可以猜测的事情:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    favorite_employee_id = Column(Integer, ForeignKey("employee.id"))

    favorite_employee = relationship(Employee, foreign_keys=favorite_employee_id)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "inherit_condition": id == Employee.id,
    }
处理简单的命名冲突

在映射过程中出现命名冲突的情况下,根据需要覆盖 classname_for_table()name_for_scalar_relationship()name_for_collection_relationship() 中的任何一个。 例如,如果 automap 正试图将多对一关系命名为现有列相同的名称,可以条件地选择替代约定。 给定一个模式:

代码语言:javascript复制
CREATE  TABLE  table_a  (
  id  INTEGER  PRIMARY  KEY
);

CREATE  TABLE  table_b  (
  id  INTEGER  PRIMARY  KEY,
  table_a  INTEGER,
  FOREIGN  KEY(table_a)  REFERENCES  table_a(id)
);

上述模式首先将table_a表自动映射为一个名为table_a的类;然后将关系自动映射到table_b的类上,该关系的名称与此相关类的名称相同,例如table_a。 此关系名称与映射列table_b.table_a冲突,并且在映射时会发出错误。

我们可以通过以下方式使用下划线来解决此冲突:

代码语言:javascript复制
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
    name = referred_cls.__name__.lower()
    local_table = local_cls.__table__
    if name in local_table.columns:
        newname = name   "_"
        warnings.warn("Already detected name %s present.  using %s" % (name, newname))
        return newname
    return name

Base.prepare(
    autoload_with=engine,
    name_for_scalar_relationship=name_for_scalar_relationship,
)

或者,我们可以在列方面更改名称。 可以使用在 Naming Declarative Mapped Columns Explicitly 中描述的技术来修改映射的列,通过将列显式地分配给新名称:

代码语言:javascript复制
Base = automap_base()

class TableB(Base):
    __tablename__ = "table_b"
    _table_a = Column("table_a", ForeignKey("table_a.id"))

Base.prepare(autoload_with=engine)
自定义关系参数

AutomapBase.prepare.generate_relationship 钩子可用于向关系添加参数。对于大多数情况,我们可以利用现有的 generate_relationship() 函数,在使用我们自己的参数扩充给定的关键字字典后返回对象。

下面是如何将 relationship.cascaderelationship.passive_deletes 选项传递给所有一对多关系的示例:

代码语言:javascript复制
from sqlalchemy.ext.automap import generate_relationship
from sqlalchemy.orm import interfaces

def _gen_relationship(
    base, direction, return_fn, attrname, local_cls, referred_cls, **kw
):
    if direction is interfaces.ONETOMANY:
        kw["cascade"] = "all, delete-orphan"
        kw["passive_deletes"] = True
    # make use of the built-in function to actually return
    # the result.
    return generate_relationship(
        base, direction, return_fn, attrname, local_cls, referred_cls, **kw
    )

from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

# automap base
Base = automap_base()

engine = create_engine("sqlite:///mydatabase.db")
Base.prepare(autoload_with=engine, generate_relationship=_gen_relationship)
多对多关系

automap 将生成多对多关系,例如那些包含 secondary 参数的关系。生成这些关系的过程如下:

  1. 在为其分配任何映射类之前,将检查给定的 Table 是否包含 ForeignKeyConstraint 对象。
  2. 如果表包含两个并且仅有两个 ForeignKeyConstraint 对象,并且此表中的所有列都是这两个 ForeignKeyConstraint 对象的成员,则假定该表是一个“次要”表,并且不会直接映射
  3. Table 所引用的两个(或一个,用于自引用)外部表将与它们将被映射到的类匹配,如果有的话。
  4. 如果两侧的映射类位于同一处,则在两个类之间创建一个双向的多对多 relationship() / backref() 对。
  5. 对于多对多的覆盖逻辑与一对多/多对一的逻辑相同;调用 generate_relationship() 函数来生成结构,并将保留现有属性。
继承关系

automap 将不会在处于继承关系的两个类之间生成任何关系。也就是说,对于以下两个给定的类:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))
    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

EngineerEmployee 的外键不是用于关系,而是用于在两个类之间建立联合继承。

请注意,这意味着 automap 不会为从子类到超类的外键生成任何关系。如果映射实际上还有从子类到超类的关系,那么这些关系需要是显式的。在下面的例子中,由于从 EngineerEmployee 有两个单独的外键,我们需要设置我们想要的关系以及 inherit_condition,因为这些是 SQLAlchemy 无法猜测的事情:

代码语言:javascript复制
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    type = Column(String(50))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Engineer(Employee):
    __tablename__ = "engineer"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
    favorite_employee_id = Column(Integer, ForeignKey("employee.id"))

    favorite_employee = relationship(Employee, foreign_keys=favorite_employee_id)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "inherit_condition": id == Employee.id,
    }
处理简单的命名冲突

在映射过程中出现命名冲突的情况下,根据需要覆盖任何 classname_for_table()name_for_scalar_relationship()name_for_collection_relationship()。例如,如果 automap 尝试将一个多对一关系命名为现有列的名称,可以有条件地选择替代约定。给定一个模式:

代码语言:javascript复制
CREATE  TABLE  table_a  (
  id  INTEGER  PRIMARY  KEY
);

CREATE  TABLE  table_b  (
  id  INTEGER  PRIMARY  KEY,
  table_a  INTEGER,
  FOREIGN  KEY(table_a)  REFERENCES  table_a(id)
);

上述模式将首先将 table_a 表自动映射为名为 table_a 的类;然后将在 table_b 类上自动映射一个与此相关类相同名称的关系,例如 table_a。这个关系名称与映射列 table_b.table_a 冲突,并且在映射时会发出错误。

通过使用下划线,我们可以解决这个冲突:

代码语言:javascript复制
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint):
    name = referred_cls.__name__.lower()
    local_table = local_cls.__table__
    if name in local_table.columns:
        newname = name   "_"
        warnings.warn("Already detected name %s present.  using %s" % (name, newname))
        return newname
    return name

Base.prepare(
    autoload_with=engine,
    name_for_scalar_relationship=name_for_scalar_relationship,
)

或者,我们可以在列的一侧更改名称。可以使用在 显式命名声明性映射列 中描述的技术修改映射的列,通过将列显式分配给一个新名称:

代码语言:javascript复制
Base = automap_base()

class TableB(Base):
    __tablename__ = "table_b"
    _table_a = Column("table_a", ForeignKey("table_a.id"))

Base.prepare(autoload_with=engine)

使用具有显式声明的 Automap

正如之前所指出的,automap 不依赖于反射,并且可以利用 Table 对象集合中的任何对象在 MetaData 集合中。由此可见,automap 也可以用于生成缺失的关系,只要有一个完全定义了表元数据的完整模型:

代码语言:javascript复制
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

在上面的例子中,给定了大部分完整的 UserAddress 映射,我们在 Address.user_id 上定义的 ForeignKey 允许在映射类上生成一个双向关系对 Address.userUser.address_collection

请注意,当子类化AutomapBase时,需要调用AutomapBase.prepare()方法;如果未调用,则我们声明的类处于未映射状态。

拦截列定义

MetaDataTable对象支持一个事件钩子DDLEvents.column_reflect(),可用于在构建Column对象之前拦截有关数据库列的反射信息。例如,如果我们想要使用命名约定来映射列,例如"attr_<columnname>",则可以应用该事件如下:

代码语言:javascript复制
@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)

返回用于从一个类引用另一个类的属性名称,用于标量对象引用。

代码语言:javascript复制
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()
代码语言:javascript复制
class sqlalchemy.ext.automap.AutomapBase

用于“automap”模式的基类。

AutomapBase 类可以与由 declarative_base() 函数产生的“声明性基类”类进行比较。在实践中,AutomapBase 类始终与实际的声明性基类一起使用作为混入。

一个新的可子类化的 AutomapBase 通常是使用 automap_base() 函数实例化的。

成员

by_module, classes, metadata, prepare()

另请参阅

Automap

代码语言:javascript复制
attribute by_module: ClassVar[ByModuleProperties]

包含点分隔的模块名称的层次结构,链接到类的 Properties 实例。

这个集合是 AutomapBase.classes 集合的一种替代方法,当利用 AutomapBase.prepare.modulename_for_table 参数时,该参数将为生成的类应用不同的 __module__ 属性。

automap 生成类的默认 __module__sqlalchemy.ext.automap;要使用 AutomapBase.by_module 访问此命名空间,看起来像这样:

代码语言:javascript复制
User = Base.by_module.sqlalchemy.ext.automap.User

如果一个类的 __module__mymodule.account,访问此命名空间看起来像这样:

代码语言:javascript复制
MyClass = Base.by_module.mymodule.account.MyClass

2.0 版中的新功能。

另请参阅

从多个模式生成映射

代码语言:javascript复制
attribute classes: ClassVar[Properties[Type[Any]]]

包含类的 Properties 实例。

此对象的行为类似于表上的 .c 集合。类以其给定的名称存在,例如:

代码语言:javascript复制
Base = automap_base()
Base.prepare(autoload_with=some_engine)

User, Address = Base.classes.User, Base.classes.Address

对于与 Properties 方法名重叠的类名,例如 items(),也支持使用 getitem 形式:

代码语言:javascript复制
Item = Base.classes["items"]
代码语言:javascript复制
attribute metadata: ClassVar[MetaData]

指的是将用于新 Table 对象的 MetaData 集合。

另请参见

访问表和元数据

代码语言:javascript复制
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 – 使用与其执行模式反射的 EngineConnection;当指定时,MetaData.reflect() 方法将在此方法的范围内调用。
  • engine – 已弃用;使用 AutomapBase.autoload_with。用于指示在反映表时使用的 EngineConnection,如果 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 中。
代码语言:javascript复制
function sqlalchemy.ext.automap.classname_for_table(base: Type[Any], tablename: str, table: Table) → str

返回应使用的类名,给定表的名称。

默认实现为:

代码语言:javascript复制
return str(tablename)

可以使用 AutomapBase.prepare.classname_for_table 参数指定替代实现。

参数:

  • base – 进行准备的 AutomapBase 类。
  • tablenameTable 的字符串名称。
  • tableTable 对象本身。

返回:

一个字符串类名。

注意

在 Python 2 中,用于类名的字符串 必须 是非 Unicode 对象,例如 str() 对象。Table.name 属性通常是 Python unicode 子类,因此应在考虑任何非 ASCII 字符后,应用 str() 函数到此名称。

代码语言:javascript复制
function sqlalchemy.ext.automap.name_for_scalar_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str

返回应用于从一个类到另一个类的引用的属性名称,用于标量对象引用。

默认实现为:

代码语言:javascript复制
return referred_cls.__name__.lower()

可以使用 AutomapBase.prepare.name_for_scalar_relationship 参数指定替代实现。

参数:

  • base – 进行准备的 AutomapBase 类。
  • local_cls – 映射到本地方的类。
  • referred_cls – 映射到引用方的类。
  • constraint – 正在检查以生成此关系的ForeignKeyConstraint
代码语言:javascript复制
function sqlalchemy.ext.automap.name_for_collection_relationship(base: Type[Any], local_cls: Type[Any], referred_cls: Type[Any], constraint: ForeignKeyConstraint) → str

返回应用于从一个类到另一个类的引用的属性名称,用于集合引用。

默认实现如下:

代码语言:javascript复制
return referred_cls.__name__.lower()   "_collection"

可以使用AutomapBase.prepare.name_for_collection_relationship参数指定替代实现。

参数:

  • base – 执行准备工作的AutomapBase类。
  • local_cls – 要映射到本地方的类。
  • referred_cls – 要映射到引用方的类。
  • constraint – 正在检查以生成此关系的ForeignKeyConstraint
代码语言:javascript复制
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参数指定此函数的替代实现。

此函数的默认实现如下:

代码语言:javascript复制
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 – 指示关系的“方向”; 这将是ONETOMANYMANYTOONEMANYTOMANY之一。
  • return_fn – 默认用于创建关系的函数。这将是relationship()backref()之一。backref()函数的结果将用于在第二步生成新的relationship(),因此如果使用自定义关系函数,则用户定义的实现必须正确区分这两个函数。
  • attrname – 正在分配此关系的属性名称。如果generate_relationship.return_fn的值是backref()函数,则此名称是分配给反向引用的名称。
  • local_cls – 此关系或反向引用将在本地存在的“本地”类。
  • referred_cls – 此关系或反向引用所指向的“引用”类。
  • **kw – 所有附加的关键字参数都将传递给该函数。

返回值:

relationship()backref() 构造,由 generate_relationship.return_fn 参数所指定。

烘焙查询

原文:docs.sqlalchemy.org/en/20/orm/extensions/baked.html

bakedQuery对象提供了一种替代的创建模式,允许缓存对象的构建和字符串编译步骤。这意味着对于一个特定的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 系统的开始是生成所谓的“面包店”,它代表了一系列特定查询对象的存储:

代码语言:javascript复制
from sqlalchemy.ext import baked

bakery = baked.bakery()

上述的“面包店”将缓存数据存储在一个默认为 200 个元素的 LRU 缓存中,需要注意的是 ORM 查询通常会包含一个用于调用 ORM 查询的条目,以及每个数据库方言的 SQL 字符串的一个条目。

该面包店允许我们通过指定其构造方式为一系列 Python 可调用对象(通常为 lambda 函数)来构建一个Query对象。为了简洁使用,它重写了 =运算符,使得典型的查询构建看起来像下面这样:

代码语言:javascript复制
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

以下是关于上述代码的一些观察:

  1. baked_query 对象是 BakedQuery 的一个实例。该对象本质上是一个真正的 orm Query 对象的“构建者”,但它本身并不是实际的 Query 对象。
  2. 实际的 Query 对象根本没有构建,直到在函数的最后调用 Result.all() 时。
  3. 添加到 baked_query 对象的步骤都表示为 Python 函数,通常是 lambda。传递给 bakery() 函数的第一个 lambda 接收一个 Session 作为其参数。其余的 lambda 每个接收一个 Query 作为其参数。
  4. 在上述代码中,即使我们的应用程序可能多次调用 search_for_user(),即使在每次调用中我们都建立一个全新的 BakedQuery 对象,所有的 lambda 只调用一次。只要此查询在烘培中被缓存,每个 lambda 在此期间都不会被第二次调用。
  5. 缓存是通过存储lambda 对象本身的引用来实现的,以便构建缓存键;也就是说,Python 解释器将这些函数分配为 Python 标识,这决定了如何在后续运行中识别查询。对于那些指定了 email 参数的 search_for_user() 调用,可调用的 lambda q: q.filter(User.email == bindparam('email')) 将成为被检索到的缓存键的一部分;当 emailNone 时,这个可调用函数不会成为缓存键的一部分。
  6. 由于 lambda 都只调用一次,因此在 lambda 内部不得引用可能跨调用改变的变量;相反,假设这些是要绑定到 SQL 字符串中的值,我们使用 bindparam() 构建命名参数,稍后使用 Result.params() 应用它们的实际值。

性能

烘焙查询可能看起来有些奇怪、有些笨拙、有些冗长。然而,对于在应用程序中多次调用的查询,Python 性能的节约非常显著。在 性能 中演示的示例套件 short_selects 说明了查询的比较,每个查询仅返回一行,如下所示的常规查询:

代码语言:javascript复制
session = Session(bind=engine)
for id_ in random.sample(ids, n):
    session.query(Customer).filter(Customer.id == id_).one()

相比于等效的“烘焙”查询:

代码语言:javascript复制
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 函数调用计数的差异为:

代码语言:javascript复制
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

以强大的笔记本电脑上的秒数来看,这是这样的:

代码语言:javascript复制
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来实现这一点:

代码语言:javascript复制
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,生成一个可以以最小开销调用的新对象。我们的例子变成了:

代码语言:javascript复制
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”方法那样不寻常,而更像是对简单“重用查询”方法的简单改进:

代码语言:javascript复制
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 调用工作。

从上面的内容,如果我们问自己,“如果查找需要根据查询结构做条件决策怎么办?”,这就是为什么“烘焙”是这样的方式的地方。我们可以从任意数量的函数构建参数化查询,而不是从一个函数(这是我们最初认为烘焙可能的工作方式)开始。考虑我们的简单例子,如果我们需要在条件基础上在查询中添加一个附加子句:

代码语言:javascript复制
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 将完全不同。很明显,随着查询构建复杂性的提高,缓存这些查询的任务会非常快地变得繁重。我们可以将上述示例转换为以下对“面包店”直接使用:

代码语言:javascript复制
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 函数,仅作为减少冗长性的手段:

代码语言:javascript复制
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 执行以及需要使用条件构建的查询等附加问题,从而导致了最终的方法。

特殊查询技术

本节将描述一些特定查询情况下的技术。

使用 IN 表达式

SQLAlchemy 中的ColumnOperators.in_()方法在历史上基于传递给方法的项目列表呈现一个可变的绑定参数集。对于已烘焙的查询,这不起作用,因为该列表的长度可能在不同的调用中发生变化。为了解决这个问题,bindparam.expanding参数支持一个延迟呈现的 IN 表达式,在烘焙查询内安全地进行缓存。实际元素列表在语句执行时呈现,而不是在语句编译时:

代码语言:javascript复制
bakery = baked.bakery()

baked_query = bakery(lambda session: session.query(User))
baked_query  = lambda q: q.filter(User.name.in_(bindparam("username", expanding=True)))

result = baked_query.with_session(session).params(username=["ed", "fred"]).all()

请参阅下文

bindparam.expanding

ColumnOperators.in_()

使用子查询

当使用Query对象时,通常需要一个Query对象用于在另一个查询中生成子查询。在Query目前处于烘焙形式的情况下,可以使用一个临时方法来检索Query对象,使用BakedQuery.to_query()方法。此方法传递给生成烘焙查询特定步骤的 lambda 可调用参数的SessionQuery

代码语言:javascript复制
bakery = baked.bakery()

# a baked query that will end up being used as a subquery
my_subq = bakery(lambda s: s.query(User.id))
my_subq  = lambda q: q.filter(User.id == Address.user_id)

# select a correlated subquery in the top columns list,
# we have the "session" argument, pass that
my_q = bakery(lambda s: s.query(Address.id, my_subq.to_query(s).as_scalar()))

# use a correlated subquery in some of the criteria, we have
# the "query" argument, pass that.
my_q  = lambda q: q.filter(my_subq.to_query(q).exists())

版本 1.3 中的新功能。

使用 before_compile 事件

从 SQLAlchemy 1.3.11 开始,针对特定Query使用QueryEvents.before_compile()事件将禁止烘焙查询系统缓存查询,如果事件挂钩返回一个与传入的不同的新Query对象。这样,QueryEvents.before_compile()挂钩可以在每次使用特定Query时被调用,以适应每次以不同方式修改查询的挂钩。要允许QueryEvents.before_compile()修改sqlalchemy.orm.Query()对象,但仍然允许结果被缓存,可以注册传递bake_ok=True标志的事件:

代码语言:javascript复制
@event.listens_for(Query, "before_compile", retval=True, bake_ok=True)
def my_event(query):
    for desc in query.column_descriptions:
        if desc["type"] is User:
            entity = desc["entity"]
            query = query.filter(entity.deleted == False)
    return query

上述策略适用于每次都以完全相同方式修改给定Query的事件,不依赖于特定参数或外部状态的更改。

版本 1.3.11 中的新功能:- 在QueryEvents.before_compile()事件中添加了“bake_ok”标志,并且如果此标志未设置,则不允许通过“baked”扩展进行缓存以发生对返回新Query对象的事件处理程序。

禁用全局会话烘焙查询

标志Session.enable_baked_queries可以设置为 False,导致所有烘焙查询在针对该Session使用时不使用缓存:

代码语言:javascript复制
session = Session(engine, enable_baked_queries=False)

像所有会话标志一样,它也被工厂对象(如sessionmaker)和方法(如sessionmaker.configure())接受。

此标志的直接理由是,一个应用程序如果出现问题,可能是由于用户定义的烘焙查询或其他烘焙查询问题导致的缓存键冲突,可以关闭该行为,以确定或排除烘焙查询作为问题原因。

版本 1.2 中的新功能。

惰性加载集成

从版本 1.4 开始更改:从 SQLAlchemy 1.4 开始,“烘焙查询”系统不再是关系加载系统的一部分。而是改用本地缓存系统。

API 文档

对象名称

描述

BakedQuery

用于构建Query对象的构建器对象。

bakery

构建一个新的烘焙坊。

Bakery

返回一个BakedQuery的可调用对象。

代码语言:javascript复制
function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)

构建一个新的烘焙坊。

返回:

一个Bakery的实例。

代码语言:javascript复制
class sqlalchemy.ext.baked.BakedQuery

成员

add_criteria(), bakery(), for_session(), spoil(), to_query(), with_criteria()

用于构建Query对象的构建器对象。

代码语言:javascript复制
method add_criteria(fn, *args)

向这个BakedQuery添加一个条件函数。

这相当于使用 =运算符就地修改BakedQuery

代码语言:javascript复制
classmethod bakery(size=200, _size_alert=None)

构建一个新的烘焙坊。

返回:

一个Bakery的实例。

代码语言:javascript复制
method for_session(session)

为这个BakedQuery返回一个Result对象。

这相当于将BakedQuery作��Python 可调用对象调用,例如result = my_baked_query(session)

代码语言:javascript复制
method spoil(full=False)

取消在此BakedQuery对象上发生的任何查询缓存。

BakedQuery可以继续正常使用,但是附加的创建函数不会被缓存;它们将在每次调用时被调用。

这是为了支持在构建烘焙查询的特定步骤中,某些使查询无法缓存的情况,例如依赖于某些不可缓存值的变体。

参数:

full – 如果为 False,则仅在破坏步骤之后添加到此BakedQuery对象的函数将不被缓存;直到此时为止的BakedQuery的状态将从缓存中拉取。如果为 True,则每次完全从头构建整个Query对象,每次调用都会调用所有创建函数。

代码语言:javascript复制
method to_query(query_or_session)

返回作为子查询使用的Query对象。

此方法应在用于生成封闭的BakedQuery步骤的 lambda 可调用对象内部使用。参数通常应该是传递给 lambda 的Query对象:

代码语言:javascript复制
sub_bq = self.bakery(lambda s: s.query(User.name))
sub_bq  = lambda q: q.filter(
    User.id == Address.user_id).correlate(Address)

main_bq = self.bakery(lambda s: s.query(Address))
main_bq  = lambda q: q.filter(
    sub_bq.to_query(q).exists())

在子查询用于第一个针对Session的可调用对象时,也接受Session

代码语言:javascript复制
sub_bq = self.bakery(lambda s: s.query(User.name))
sub_bq  = lambda q: q.filter(
    User.id == Address.user_id).correlate(Address)

main_bq = self.bakery(
    lambda s: s.query(
    Address.id, sub_bq.to_query(q).scalar_subquery())
)

参数:

query_or_session

一个Query对象或一个类Session对象,假设它在封闭的BakedQuery可调用对象的上下文中。

版本 1.3 中的新功能。

代码语言:javascript复制
method with_criteria(fn, *args)

向从此克隆的BakedQuery添加一个条件函数。

这相当于使用 运算符产生一个具有修改的新BakedQuery

代码语言:javascript复制
class sqlalchemy.ext.baked.Bakery

返回一个返回BakedQuery的可调用对象。

此对象由类方法BakedQuery.bakery()返回。它存在为了便于检查“缓存”。

版本 1.2 中的新功能。

代码语言:javascript复制
class sqlalchemy.ext.baked.Result

对一个Session发起一个BakedQuery的调用。

Result对象是实际创建或从缓存中检索的针对目标SessionQuery对象,并且然后为结果调用。

代码语言:javascript复制
method all()

返回所有行。

等效于Query.all()

代码语言:javascript复制
method count()

返回‘count’。

等效于Query.count()

请注意,这使用子查询确保准确计算,而不管原始语句的结构如何。

代码语言:javascript复制
method first()

返回第一行。

等效于Query.first()

代码语言:javascript复制
method get(ident)

根据标识检索对象。

等效于Query.get()

代码语言:javascript复制
method one()

返回确切的一个结果或引发异常。

等效于Query.one()

代码语言:javascript复制
method one_or_none()

返回一个或零个结果,或者对于多行引发异常。

等效于Query.one_or_none()

代码语言:javascript复制
method params(*args, **kw)

指定要替换为字符串 SQL 语句的参数。

代码语言:javascript复制
method scalar()

返回第一个结果的第一个元素,如果没有行,则返回 None。如果返回多行,则引发 MultipleResultsFound 异常。

等效于Query.scalar()

代码语言:javascript复制
method with_post_criteria(fn)

添加一个将在缓存后应用的条件函数。

这将添加一个函数,该函数将针对从缓存中检索的Query对象运行。目前仅包括Query.params()Query.execution_options()方法。

警告

Result.with_post_criteria()函数应用于Query对象之后查询的 SQL 语句对象已从缓存中检索。只应使用Query.params()Query.execution_options()方法。

在版本 1.2 中新增。

概要

使用烘焙系统的方法是首先生成所谓的“烘焙坊”,该坊代表一系列特定的查询对象的存储:

代码语言:javascript复制
from sqlalchemy.ext import baked

bakery = baked.bakery()

上述“面包店”将在默认为 200 个元素的 LRU 缓存中存储缓存数据,需要注意的是,ORM 查询通常会包含一个为调用的 ORM 查询条目,以及每个数据库方言的 SQL 字符串的条目。

面包店允许我们通过指定其构造方式为一系列 Python 可调用对象来构建一个Query对象,这些对象通常是 lambda 表达式。为了简洁使用,它重写了 =运算符,使得典型的查询构建看起来像下面这样:

代码语言:javascript复制
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

以下是关于上述代码的一些观察:

  1. baked_query对象是BakedQuery的一个实例。这个对象本质上是一个真正的 orm Query对象的“构造器”,但它本身并不是真正的 Query对象。
  2. 实际的Query对象根本没有构建,直到函数的最后一刻调用Result.all()时。
  3. 添加到baked_query对象的步骤都表示为 Python 函数,通常是 lambda 函数。给bakery()函数的第一个 lambda 函数以Session作为其参数。其余的 lambda 函数每个都以Query作为其参数。
  4. 在上述代码中,即使我们的应用程序可能多次调用search_for_user(),即使在每次调用中我们都会构建一个全新的BakedQuery对象,所有的 lambda 函数只会被调用一次。只要此查询被缓存在面包店中,每个 lambda 函数永远不会再次被调用。
  5. 缓存是通过存储lambda 对象本身的引用来实现的,以形成一个缓存键;也就是说,Python 解释器将这些函数分配给 Python 标识符,这决定了如何在后续运行中识别查询。对于那些指定了email参数的search_for_user()调用,可调用对象lambda q: q.filter(User.email == bindparam('email'))将成为检索到的缓存键的一部分;当emailNone时,此可调用对象不会成为缓存键的一部分。
  6. 因为 lambda 函数只被调用一次,所以至关重要的是在 lambda 函数内部不引用可能在调用之间更改的变量;相反,假设这些是要绑定到 SQL 字符串中的值,我们使用 bindparam() 来构造命名参数,在稍后使用 Result.params() 应用其实际值。

性能

烘焙查询可能看起来有点奇怪,有点笨拙,有点啰嗦。然而,在应用程序中调用多次的查询中,Python 性能的节约非常显著。在 Performance 中演示的示例套件 short_selects 对比了每个仅返回一行的查询,例如以下常规查询:

代码语言:javascript复制
session = Session(bind=engine)
for id_ in random.sample(ids, n):
    session.query(Customer).filter(Customer.id == id_).one()

与等效的“烘焙”查询相比:

代码语言:javascript复制
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 函数调用次数的差异为:

代码语言:javascript复制
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

在一台性能强大的笔记本电脑上,这在秒数上表现如下:

代码语言:javascript复制
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来实现这一点:

代码语言:javascript复制
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'))时所需的工作,这将为我们跳过 Core 表达式的构建以及将其发送到Query.filter()。然而,该方法仍然在每次调用Query.all()时重新生成完整的Select对象,并且每次还会将这个全新的Select对象发送到字符串编译步骤中,对于像上面这样的简单情况,这可能占据了大约 70% 的开销。

为了减少额外的开销,我们需要一些更专门的逻辑,一种记忆构建选择对象和构建 SQL 的方法。在维基中的BakedQuery部分有一个例子,这是该功能的前身,但在那个系统中,我们没有缓存查询的构建。为了消除所有开销,我们需要缓存查询的构建以及 SQL 编译。假设我们按照这种方式调整了配方,并制作了一个.bake()方法,用于预先编译查询的 SQL,生成一个可以以最小开销调用的新对象。我们的例子变成了:

代码语言:javascript复制
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”方法那样不寻常,而更像是对简单“重用查询”的简单改进:

代码语言:javascript复制
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()

上面,我们使用“baked”系统的方式与简单的“缓存查询”系统非常相似。但是,它使用了两行更少的代码,不需要制造一个“my_key”的缓存键,而且还包含了与我们自定义的“bake”函数相同的功能,该函数缓存了查询构造函数,筛选调用,生成Select对象以及字符串编译步骤的 100% Python 调用工作。

从上面的内容中,如果我们问自己,“如果查找需要根据查询结构做条件决策,会怎样?”,这就是为什么“烘焙”是这样的方式的原因。我们可以从任意数量的函数构建参数化查询,而不是从一个函数构建(这是我们最初认为烘焙可能起作用的方式)。考虑我们的简单示例,如果我们需要在查询中有一个额外的条件子句:

代码语言:javascript复制
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”如下:

代码语言:javascript复制
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 表达式,只是为了减少冗长:

代码语言:javascript复制
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 执行以及需要处理条件构建的查询都需要解决,最终导致了最终方法。

特殊查询技术

这一部分将描述一些特定查询情况下的技术。

使用 IN 表达式

在 SQLAlchemy 中,ColumnOperators.in_() 方法在历史上基于传递给方法的项目列表渲染一组变量绑定参数。这对于烘焙查询不起作用,因为该列表的长度可能在不同调用时发生变化。为了解决这个问题,bindparam.expanding 参数支持一个延迟渲染的 IN 表达式,可以安全地缓存在烘焙查询内部。实际元素列表在语句执行时渲染,而不是在语句编译时:

代码语言:javascript复制
bakery = baked.bakery()

baked_query = bakery(lambda session: session.query(User))
baked_query  = lambda q: q.filter(User.name.in_(bindparam("username", expanding=True)))

result = baked_query.with_session(session).params(username=["ed", "fred"]).all()

另请参阅

bindparam.expanding

ColumnOperators.in_()

使用子查询

在使用Query对象时,通常需要一个Query对象用于在另一个内部生成子查询。在Query当前处于烘焙形式的情况下,可以使用一个中间方法来检索Query对象,使用BakedQuery.to_query()方法。该方法传递给生成烘焙查询特定步骤的 lambda 可调用参数的SessionQuery

代码语言:javascript复制
bakery = baked.bakery()

# a baked query that will end up being used as a subquery
my_subq = bakery(lambda s: s.query(User.id))
my_subq  = lambda q: q.filter(User.id == Address.user_id)

# select a correlated subquery in the top columns list,
# we have the "session" argument, pass that
my_q = bakery(lambda s: s.query(Address.id, my_subq.to_query(s).as_scalar()))

# use a correlated subquery in some of the criteria, we have
# the "query" argument, pass that.
my_q  = lambda q: q.filter(my_subq.to_query(q).exists())

版本 1.3 中新增。

使用 before_compile 事件

自 SQLAlchemy 1.3.11 起,针对特定的 Query 使用 QueryEvents.before_compile() 事件将禁止烘焙查询系统缓存查询,如果事件挂钩返回一个与传入的不同的新 Query 对象。这样,每次使用特定的 Query 都可以调用 QueryEvents.before_compile() 钩子,以适应每次更改查询的钩子。要允许 QueryEvents.before_compile() 修改 sqlalchemy.orm.Query() 对象,但仍然允许结果被缓存,可以注册事件并传递 bake_ok=True 标志:

代码语言:javascript复制
@event.listens_for(Query, "before_compile", retval=True, bake_ok=True)
def my_event(query):
    for desc in query.column_descriptions:
        if desc["type"] is User:
            entity = desc["entity"]
            query = query.filter(entity.deleted == False)
    return query

上述策略适用于每次以完全相同方式修改给定的 Query 的事件,不依赖于特定参数或更改的外部状态。

新版本 1.3.11 中添加了 bake_ok 标志到 QueryEvents.before_compile() 事件,并且如果此标志未设置,则不允许为返回新 Query 对象的事件处理程序通过“烘焙”扩展进行缓存。 ### 使用 IN 表达式

SQLAlchemy 中的 ColumnOperators.in_() 方法在历史上根据传递给方法的项目列表呈现一组变量绑定参数。对于烘焙查询,这不起作用,因为该列表的长度可以在不同的调用中改变。为解决此问题,bindparam.expanding 参数支持在烘焙查询中安全缓存的延迟呈现 IN 表达式。实际元素列表在语句执行时呈现,而不是在语句编译时:

代码语言:javascript复制
bakery = baked.bakery()

baked_query = bakery(lambda session: session.query(User))
baked_query  = lambda q: q.filter(User.name.in_(bindparam("username", expanding=True)))

result = baked_query.with_session(session).params(username=["ed", "fred"]).all()

另请参阅

bindparam.expanding

ColumnOperators.in_()

使用子查询

当使用Query对象时,通常需要一个Query对象用于在另一个查询中生成子查询。在当前Query处于烘焙形式时,可能需要使用一个临时方法来检索Query对象,该方法使用BakedQuery.to_query()方法。此方法传递给用于生成烘焙查询特定步骤的 lambda 可调用的SessionQuery参数:

代码语言:javascript复制
bakery = baked.bakery()

# a baked query that will end up being used as a subquery
my_subq = bakery(lambda s: s.query(User.id))
my_subq  = lambda q: q.filter(User.id == Address.user_id)

# select a correlated subquery in the top columns list,
# we have the "session" argument, pass that
my_q = bakery(lambda s: s.query(Address.id, my_subq.to_query(s).as_scalar()))

# use a correlated subquery in some of the criteria, we have
# the "query" argument, pass that.
my_q  = lambda q: q.filter(my_subq.to_query(q).exists())

新版本 1.3。

使用 before_compile 事件

从 SQLAlchemy 1.3.11 开始,针对特定Query使用QueryEvents.before_compile()事件将阻止烘焙查询系统缓存查询,如果事件钩子返回一个与传入的不同的新Query对象。这是为了每次使用时都可以调用特定QueryEvents.before_compile()钩子,以适应每次都以不同方式修改查询的钩子。要允许QueryEvents.before_compile()修改sqlalchemy.orm.Query()对象,但仍然允许结果被缓存,可以注册事件并传递bake_ok=True标志:

代码语言:javascript复制
@event.listens_for(Query, "before_compile", retval=True, bake_ok=True)
def my_event(query):
    for desc in query.column_descriptions:
        if desc["type"] is User:
            entity = desc["entity"]
            query = query.filter(entity.deleted == False)
    return query

以上策略适用于每次都会以完全相同方式修改给定的Query的事件,不依赖于特定参数或会发生变化的外部状态。

新版本 1.3.11 中新增:- 在QueryEvents.before_compile()事件中添加了“bake_ok”标志,并且如果未设置此标志,则禁止通过“baked”扩展对返回新的Query对象的事件处理程序进行缓存。

禁用全局 Baked 查询

标志Session.enable_baked_queries可以设置为 False,导致所有烘焙查询在用于该Session时不使用缓存:

代码语言:javascript复制
session = Session(engine, enable_baked_queries=False)

与所有会话标志一样,它也被工厂对象如sessionmaker和方法如sessionmaker.configure()所接受。

此标志的直接理由是,应用程序可能由于用户定义的烘焙查询或其他烘焙查询问题而看到问题,可以将行为关闭,以识别或排除烘焙查询作为问题的原因。

版本 1.2 中的新功能。

惰性加载集成

从版本 1.4 起更改:自 SQLAlchemy 1.4 起,“烘焙查询”系统不再是关系加载系统的一部分。取而代之的是使用本地缓存系统。

API 文档

对象名称

描述

BakedQuery

用于Query对象的构建器对象。

bakery

构建一个新的面包店。

Bakery

返回一个BakedQuery的可调用对象。

代码语言:javascript复制
function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)

构建一个新的面包店。

返回:

一个Bakery的实例

代码语言:javascript复制
class sqlalchemy.ext.baked.BakedQuery

成员

add_criteria(), bakery(), for_session(), spoil(), to_query(), with_criteria()

用于Query对象的构建器对象。

代码语言:javascript复制
method add_criteria(fn, *args)

为此BakedQuery添加一个条件函数。

这相当于使用 = 运算符就地修改BakedQuery

代码语言:javascript复制
classmethod bakery(size=200, _size_alert=None)

构建一个新的面包店。

返回:

一个Bakery的实例

代码语言:javascript复制
method for_session(session)

为此BakedQuery返回一个Result对象。

这相当于将BakedQuery作为 Python 可调用对象调用,例如 result = my_baked_query(session)

代码语言:javascript复制
method spoil(full=False)

取消此BakedQuery对象上将发生的任何查询缓存。

BakedQuery仍然可以正常使用,但是额外的创建函数不会被缓存;它们将在每次调用时被调用。

这是为了支持构建烘焙查询的特定步骤使查询无法缓存的情况,例如依赖于某些不可缓存值的变体。

参数:

full – 如果为 False,则仅在 spoil 步骤之后添加到此BakedQuery对象的函数将不被缓存;直到此点为止的BakedQuery状态将从缓存中提取。如果为 True,则每次都会从头开始构建整个Query对象,每次调用都会调用所有创建函数。

代码语言:javascript复制
method to_query(query_or_session)

返回用作子查询的Query对象。

此方法应在用于生成封闭BakedQuery步骤的 lambda 可调用内使用。参数通常应为传递给 lambda 的Query对象:

代码语言:javascript复制
sub_bq = self.bakery(lambda s: s.query(User.name))
sub_bq  = lambda q: q.filter(
    User.id == Address.user_id).correlate(Address)

main_bq = self.bakery(lambda s: s.query(Address))
main_bq  = lambda q: q.filter(
    sub_bq.to_query(q).exists())

在第一个可调用中使用子查询针对Session的情况下,也接受Session

代码语言:javascript复制
sub_bq = self.bakery(lambda s: s.query(User.name))
sub_bq  = lambda q: q.filter(
    User.id == Address.user_id).correlate(Address)

main_bq = self.bakery(
    lambda s: s.query(
    Address.id, sub_bq.to_query(q).scalar_subquery())
)

参数:

query_or_session

一个Query对象或一个类Session对象,假定在封闭BakedQuery可调用的上下文中。

版本 1.3 中新增。

代码语言:javascript复制
method with_criteria(fn, *args)

向从此克隆的BakedQuery添加一个条件函数。

这相当于使用 运算符生成具有修改的新BakedQuery

代码语言:javascript复制
class sqlalchemy.ext.baked.Bakery

返回一个返回BakedQuery的可调用对象。

此对象由类方法BakedQuery.bakery()返回。它作为一个对象存在,以便可以轻松检查“缓存”。

版本 1.2 中新增。

代码语言:javascript复制
class sqlalchemy.ext.baked.Result

针对Session调用BakedQuery

Result 对象是实际创建或从缓存中检索到的Query对象,针对目标Session进行调用以获取结果。

代码语言:javascript复制
method all()

返回所有行。

等同于Query.all()

代码语言:javascript复制
method count()

返回‘count’。

等同于Query.count()

请注意,这使用子查询来确保准确计数,而不考虑原始语句的结构。

代码语言:javascript复制
method first()

返回第一行。

等同于Query.first()

代码语言:javascript复制
method get(ident)

根据标识检索对象。

等同于Query.get()

代码语言:javascript复制
method one()

返回确切的一个结果或引发异常。

等同于Query.one()

代码语言:javascript复制
method one_or_none()

返回一个或零个结果,或者对于多行会引发异常。

等同于Query.one_or_none()

代码语言:javascript复制
method params(*args, **kw)

指定要替换到字符串 SQL 语句中的参数。

代码语言:javascript复制
method scalar()

返回第一个结果的第一个元素,如果没有行则返回 None。如果返回多行,则引发 MultipleResultsFound 异常。

等同于Query.scalar()

代码语言:javascript复制
method with_post_criteria(fn)

添加一个将在缓存后应用的条件函数。

这添加了一个将在从缓存中检索到的Query对象上运行的函数。目前仅包括Query.params()Query.execution_options()方法。

警告

Result.with_post_criteria() 函数应用于查询的Query对象之后查询的 SQL 语句对象已从缓存中检索。只应使用Query.params()Query.execution_options()方法。

新版本 1.2 中新增。

0 人点赞