SqlAlchemy-2-0-中文文件-十六-

绝不原创的飞龙發表於2024-06-22

SqlAlchemy 2.0 中文文件(十六)

原文: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()在生成的基類上,要求它反映模式並生成對映:

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()反映的自身構造。下面我們演示了反射和顯式表宣告的組合:

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 物件新增新的對映:

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

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 的表,將會有三個單獨的類可用,分別是:

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,我們可以定義要使用的一個或兩個類:

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 包的“複數形式”為集合名:

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

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 選項傳遞給所有一對多關係的示例:

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 不會在處於繼承關係的兩個類之間生成任何關係。也就是說,對於以下給定的兩個類:

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 可以猜測的:

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() 中的任何一個。例如,如果自動對映嘗試將一個多對一關係命名為一個現有列相同的名稱,可以有條件地選擇替代約定。給定一個模式:

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 衝突,並且將在對映時發出錯誤。

我們可以透過以下方式使用下劃線解決這個衝突:

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 中描述的技術修改對映的列,透過將列顯式地分配給一個新名稱:

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 物件集合。由此可見,自動對映也可以在完全定義了表後設資料的完整模型中生成丟失的關係:

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>"的命名約定來對映列,可以應用該事件:

@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

用於“自動對映”模式的基類。

AutomapBase 類可以與由 declarative_base() 函式生成的“宣告基類”類相比較。實際上,AutomapBase 類總是與實際的宣告基類一起使用作為一個 mixin。

一個新的可子類化的 AutomapBase 通常使用 automap_base() 函式例項化。

成員

by_module, classes, metadata, prepare()

另請參閱

自動對映

attribute by_module: ClassVar[ByModuleProperties]

一個包含點分隔的模組名稱層次結構連結到類的 Properties 例項。

這個集合是一個替代 AutomapBase.classes 集合的選擇,當使用 AutomapBase.prepare.modulename_for_table 引數時,這個引數將為生成的類應用不同的 __module__ 屬性。

自動對映生成的類的預設 __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(),也支援獲取項的形式:

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 – 用於執行模式反射的 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 中的新功能。

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 類。

  • tablenameTable 的字串名稱。

  • tableTable 物件本身。

返回:

一個字串類名。

注意

在 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 – 表示關係的“方向”; 這將是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() 在生成的基類上,要求它反映架構並生成對映:

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()自身進行反射。下面我們展示了反射和顯式表宣告的結合使用:

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物件新增新的對映:

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

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的表,那麼將會有三個不同的類可用:

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

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的表,則會分別獲得三個單獨的類:

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,我們可以定義要使用的一個或兩個類:

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 包來對集合名稱進行“複數化”:

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

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選項的示例:

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 不會在處於繼承關係的兩個類之間生成任何關係。 也就是說,給定以下兩個類:

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 可以猜測的事情:

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 正試圖將多對一關係命名為現有列相同的名稱,可以條件地選擇替代約定。 給定一個模式:

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衝突,並且在對映時會發出錯誤。

我們可以透過以下方式使用下劃線來解決此衝突:

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 中描述的技術來修改對映的列,透過將列顯式地分配給新名稱:

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 選項傳遞給所有一對多關係的示例:

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 將不會在處於繼承關係的兩個類之間生成任何關係。也就是說,對於以下兩個給定的類:

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 無法猜測的事情:

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 嘗試將一個多對一關係命名為現有列的名稱,可以有條件地選擇替代約定。給定一個模式:

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 衝突,並且在對映時會發出錯誤。

透過使用下劃線,我們可以解決這個衝突:

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,
)

或者,我們可以在列的一側更改名稱。可以使用在 顯式命名宣告性對映列 中描述的技術修改對映的列,透過將列顯式分配給一個新名稱:

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 也可以用於生成缺失的關係,只要有一個完全定義了表後設資料的完整模型:

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>",則可以應用該事件如下:

@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 – 使用與其執行模式反射的 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 中。

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 類。

  • tablenameTable 的字串名稱。

  • tableTable 物件本身。

返回:

一個字串類名。

注意

在 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 – 指示關係的“方向”; 這將是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 系統的開始是生成所謂的“麵包店”,它代表了一系列特定查詢物件的儲存:

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

以下是關於上述程式碼的一些觀察:

  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 說明了查詢的比較,每個查詢僅返回一行,如下所示的常規查詢:

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 執行以及需要使用條件構建的查詢等附加問題,從而導致了最終的方法。

特殊查詢技術

本節將描述一些特定查詢情況下的技術。

使用 IN 表示式

SQLAlchemy 中的ColumnOperators.in_()方法在歷史上基於傳遞給方法的專案列表呈現一個可變的繫結引數集。對於已烘焙的查詢,這不起作用,因為該列表的長度可能在不同的呼叫中發生變化。為了解決這個問題,bindparam.expanding引數支援一個延遲呈現的 IN 表示式,在烘焙查詢內安全地進行快取。實際元素列表在語句執行時呈現,而不是在語句編譯時:

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

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標誌的事件:

@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使用時不使用快取:

session = Session(engine, enable_baked_queries=False)

像所有會話標誌一樣,它也被工廠物件(如sessionmaker)和方法(如sessionmaker.configure())接受。

此標誌的直接理由是,一個應用程式如果出現問題,可能是由於使用者定義的烘焙查詢或其他烘焙查詢問題導致的快取鍵衝突,可以關閉該行為,以確定或排除烘焙查詢作為問題原因。

版本 1.2 中的新功能。

惰性載入整合

從版本 1.4 開始更改:從 SQLAlchemy 1.4 開始,“烘焙查詢”系統不再是關係載入系統的一部分。而是改用本地快取系統。

API 文件

物件名稱 描述
BakedQuery 用於構建Query物件的構建器物件。
bakery 構建一個新的烘焙坊。
Bakery 返回一個BakedQuery的可呼叫物件。
function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)

構建一個新的烘焙坊。

返回:

一個Bakery的例項。

class sqlalchemy.ext.baked.BakedQuery

成員

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

用於構建Query物件的構建器物件。

method add_criteria(fn, *args)

向這個BakedQuery新增一個條件函式。

這相當於使用+=運算子就地修改BakedQuery

classmethod bakery(size=200, _size_alert=None)

構建一個新的烘焙坊。

返回:

一個Bakery的例項。

method for_session(session)

為這個BakedQuery返回一個Result物件。

這相當於將BakedQuery作��Python 可呼叫物件呼叫,例如result = my_baked_query(session)

method spoil(full=False)

取消在此BakedQuery物件上發生的任何查詢快取。

BakedQuery可以繼續正常使用,但是附加的建立函式不會被快取;它們將在每次呼叫時被呼叫。

這是為了支援在構建烘焙查詢的特定步驟中,某些使查詢無法快取的情況,例如依賴於某些不可快取值的變體。

引數:

full – 如果為 False,則僅在破壞步驟之後新增到此BakedQuery物件的函式將不被快取;直到此時為止的BakedQuery的狀態將從快取中拉取。如果為 True,則每次完全從頭構建整個Query物件,每次呼叫都會呼叫所有建立函式。

method to_query(query_or_session)

返回作為子查詢使用的Query物件。

此方法應在用於生成封閉的BakedQuery步驟的 lambda 可呼叫物件內部使用。引數通常應該是傳遞給 lambda 的Query物件:

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

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 中的新功能。

method with_criteria(fn, *args)

向從此克隆的BakedQuery新增一個條件函式。

這相當於使用+運算子產生一個具有修改的新BakedQuery

class sqlalchemy.ext.baked.Bakery

返回一個返回BakedQuery的可呼叫物件。

此物件由類方法BakedQuery.bakery()返回。它存在為了便於檢查“快取”。

版本 1.2 中的新功能。

class sqlalchemy.ext.baked.Result

對一個Session發起一個BakedQuery的呼叫。

Result物件是實際建立或從快取中檢索的針對目標SessionQuery物件,並且然後為結果呼叫。

method all()

返回所有行。

等效於Query.all()

method count()

返回‘count’。

等效於Query.count()

請注意,這使用子查詢確保準確計算,而不管原始語句的結構如何。

method first()

返回第一行。

等效於Query.first()

method get(ident)

根據標識檢索物件。

等效於Query.get()

method one()

返回確切的一個結果或引發異常。

等效於Query.one()

method one_or_none()

返回一個或零個結果,或者對於多行引發異常。

等效於Query.one_or_none()

method params(*args, **kw)

指定要替換為字串 SQL 語句的引數。

method scalar()

返回第一個結果的第一個元素,如果沒有行,則返回 None。如果返回多行,則引發 MultipleResultsFound 異常。

等效於Query.scalar()

method with_post_criteria(fn)

新增一個將在快取後應用的條件函式。

這將新增一個函式,該函式將針對從快取中檢索的Query物件執行。目前僅包括Query.params()Query.execution_options()方法。

警告

Result.with_post_criteria()函式應用於Query物件之後查詢的 SQL 語句物件已從快取中檢索。只應使用Query.params()Query.execution_options()方法。

在版本 1.2 中新增。

概要

使用烘焙系統的方法是首先生成所謂的“烘焙坊”,該坊代表一系列特定的查詢物件的儲存:

from sqlalchemy.ext import baked

bakery = baked.bakery()

上述“麵包店”將在預設為 200 個元素的 LRU 快取中儲存快取資料,需要注意的是,ORM 查詢通常會包含一個為呼叫的 ORM 查詢條目,以及每個資料庫方言的 SQL 字串的條目。

麵包店允許我們透過指定其構造方式為一系列 Python 可呼叫物件來構建一個Query物件,這些物件通常是 lambda 表示式。為了簡潔使用,它重寫了+=運算子,使得典型的查詢構建看起來像下面這樣:

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 對比了每個僅返回一行的查詢,例如以下常規查詢:

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'))時所需的工作,這將為我們跳過 Core 表示式的構建以及將其傳送到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()

上面,我們使用“baked”系統的方式與簡單的“快取查詢”系統非常相似。但是,它使用了兩行更少的程式碼,不需要製造一個“my_key”的快取鍵,而且還包含了與我們自定義的“bake”函式相同的功能,該函式快取了查詢建構函式,篩選呼叫,生成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”如下:

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 執行以及需要處理條件構建的查詢都需要解決,最終導致了最終方法。

特殊查詢技術

這一部分將描述一些特定查詢情況下的技術。

使用 IN 表示式

在 SQLAlchemy 中,ColumnOperators.in_() 方法在歷史上基於傳遞給方法的專案列表渲染一組變數繫結引數。這對於烘焙查詢不起作用,因為該列表的長度可能在不同呼叫時發生變化。為了解決這個問題,bindparam.expanding 引數支援一個延遲渲染的 IN 表示式,可以安全地快取在烘焙查詢內部。實際元素列表在語句執行時渲染,而不是在語句編譯時:

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

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 標誌:

@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 表示式。實際元素列表在語句執行時呈現,而不是在語句編譯時:

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引數:

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標誌:

@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時不使用快取:

session = Session(engine, enable_baked_queries=False)

與所有會話標誌一樣,它也被工廠物件如sessionmaker和方法如sessionmaker.configure()所接受。

此標誌的直接理由是,應用程式可能由於使用者定義的烘焙查詢或其他烘焙查詢問題而看到問題,可以將行為關閉,以識別或排除烘焙查詢作為問題的原因。

版本 1.2 中的新功能。

惰性載入整合

從版本 1.4 起更改:自 SQLAlchemy 1.4 起,“烘焙查詢”系統不再是關係載入系統的一部分。取而代之的是使用本地快取系統。

API 文件

物件名稱 描述
BakedQuery 用於Query物件的構建器物件。
bakery 構建一個新的麵包店。
Bakery 返回一個BakedQuery的可呼叫物件。
function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)

構建一個新的麵包店。

返回:

一個Bakery的例項

class sqlalchemy.ext.baked.BakedQuery

成員

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

用於Query物件的構建器物件。

method add_criteria(fn, *args)

為此BakedQuery新增一個條件函式。

這相當於使用 += 運算子就地修改BakedQuery

classmethod bakery(size=200, _size_alert=None)

構建一個新的麵包店。

返回:

一個Bakery的例項

method for_session(session)

為此BakedQuery返回一個Result物件。

這相當於將BakedQuery作為 Python 可呼叫物件呼叫,例如 result = my_baked_query(session)

method spoil(full=False)

取消此BakedQuery物件上將發生的任何查詢快取。

BakedQuery仍然可以正常使用,但是額外的建立函式不會被快取;它們將在每次呼叫時被呼叫。

這是為了支援構建烘焙查詢的特定步驟使查詢無法快取的情況,例如依賴於某些不可快取值的變體。

引數:

full – 如果為 False,則僅在 spoil 步驟之後新增到此BakedQuery物件的函式將不被快取;直到此點為止的BakedQuery狀態將從快取中提取。如果為 True,則每次都會從頭開始構建整個Query物件,每次呼叫都會呼叫所有建立函式。

method to_query(query_or_session)

返回用作子查詢的Query物件。

此方法應在用於生成封閉BakedQuery步驟的 lambda 可呼叫內使用。引數通常應為傳遞給 lambda 的Query物件:

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

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 中新增。

method with_criteria(fn, *args)

向從此克隆的BakedQuery新增一個條件函式。

這相當於使用+運算子生成具有修改的新BakedQuery

class sqlalchemy.ext.baked.Bakery

返回一個返回BakedQuery的可呼叫物件。

此物件由類方法BakedQuery.bakery()返回。它作為一個物件存在,以便可以輕鬆檢查“快取”。

版本 1.2 中新增。

class sqlalchemy.ext.baked.Result

針對Session呼叫BakedQuery

Result 物件是實際建立或從快取中檢索到的Query物件,針對目標Session進行呼叫以獲取結果。

method all()

返回所有行。

等同於Query.all()

method count()

返回‘count’。

等同於Query.count()

請注意,這使用子查詢來確保準確計數,而不考慮原始語句的結構。

method first()

返回第一行。

等同於Query.first()

method get(ident)

根據標識檢索物件。

等同於Query.get()

method one()

返回確切的一個結果或引發異常。

等同於Query.one()

method one_or_none()

返回一個或零個結果,或者對於多行會引發異常。

等同於Query.one_or_none()

method params(*args, **kw)

指定要替換到字串 SQL 語句中的引數。

method scalar()

返回第一個結果的第一個元素,如果沒有行則返回 None。如果返回多行,則引發 MultipleResultsFound 異常。

等同於Query.scalar()

method with_post_criteria(fn)

新增一個將在快取後應用的條件函式。

這新增了一個將在從快取中檢索到的Query物件上執行的函式。目前僅包括Query.params()Query.execution_options()方法。

警告

Result.with_post_criteria() 函式應用於查詢的Query物件之後查詢的 SQL 語句物件已從快取中檢索。只應使用Query.params()Query.execution_options()方法。

新版本 1.2 中新增。

宣告式擴充套件

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

宣告式對映 API 特定的擴充套件。

1.4 版本更改:絕大部分宣告式擴充套件現在已整合到 SQLAlchemy ORM 中,並可從 sqlalchemy.orm 名稱空間匯入。請參閱宣告式對映的文件以獲取新文件。有關更改的概述,請參閱宣告式現已與 ORM 整合,並帶有新功能。

物件名稱 描述
AbstractConcreteBase 一個用於“具體”宣告式對映的輔助類。
ConcreteBase 一個用於“具體”宣告式對映的輔助類。
DeferredReflection 一個用於基於延遲反射步驟構建對映的輔助類。
class sqlalchemy.ext.declarative.AbstractConcreteBase

一個用於“具體”宣告式對映的輔助類。

AbstractConcreteBase 將自動使用 polymorphic_union() 函式,對所有作為此類的子類對映的表執行。該函式透過 __declare_first__() 函式呼叫,這實際上是一個 before_configured() 事件的鉤子。

AbstractConcreteBase 應用 Mapper 到其直接繼承的類,就像對任何其他宣告式對映的類一樣。然而,Mapper 沒有對映到任何特定的 Table 物件。相反,它直接對映到由 polymorphic_union() 產生的“多型”可選擇的物件,並且不執行自己的持久化操作。與 ConcreteBase 相比,後者將其直接繼承的類對映到直接儲存行的實際 Table

注意

AbstractConcreteBase延遲了基類的對映器建立,直到所有子類都已定義,因為它需要建立一個針對包含所有子類表的可選擇項的對映。為了實現這一點,它等待對映器配置事件發生,然後掃描所有配置的子類,並設定一個將一次性查詢所有子類的對映。

雖然此事件通常會自動呼叫,但在AbstractConcreteBase的情況下,如果第一個操作是針對此基類的查詢,則可能需要在定義所有子類對映之後顯式呼叫它。為此,一旦所有期望的類都已配置,可以呼叫正在使用的registry上的registry.configure()方法,該方法可在特定宣告基類的關係中使用:

Base.registry.configure()

示例:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.ext.declarative import AbstractConcreteBase

class Base(DeclarativeBase):
    pass

class Employee(AbstractConcreteBase, Base):
    pass

class Manager(Employee):
    __tablename__ = 'manager'
    employee_id = Column(Integer, primary_key=True)
    name = Column(String(50))
    manager_data = Column(String(40))

    __mapper_args__ = {
        'polymorphic_identity':'manager',
        'concrete':True
    }

Base.registry.configure()

抽象基類在宣告時以一種特殊的方式處理;在類配置時,它的行為類似於宣告式的混入或__abstract__基類。一旦類被配置並生成對映,它會被對映自身,但在其所有子類之後。這是在任何其他 SQLAlchemy API 功能中都找不到的非常獨特的對映系統。

使用這種方法,我們可以指定將在對映的子類上發生的列和屬性,就像我們通常在 Mixin 和自定義基類中所做的那樣:

from sqlalchemy.ext.declarative import AbstractConcreteBase

class Company(Base):
    __tablename__ = 'company'
    id = Column(Integer, primary_key=True)

class Employee(AbstractConcreteBase, Base):
    strict_attrs = True

    employee_id = Column(Integer, primary_key=True)

    @declared_attr
    def company_id(cls):
        return Column(ForeignKey('company.id'))

    @declared_attr
    def company(cls):
        return relationship("Company")

class Manager(Employee):
    __tablename__ = 'manager'

    name = Column(String(50))
    manager_data = Column(String(40))

    __mapper_args__ = {
        'polymorphic_identity':'manager',
        'concrete':True
    }

Base.registry.configure()

然而,當我們使用我們的對映時,ManagerEmployee都將擁有一個可獨立使用的.company屬性:

session.execute(
    select(Employee).filter(Employee.company.has(id=5))
)

引數:

strict_attrs

當在基類上指定時,“嚴格”屬性模式被啟用,試圖將基類上的 ORM 對映屬性限制為僅當下立即存在的屬性,同時仍保留“多型”載入行為。

2.0 版中新增。

另請參閱

ConcreteBase

具體表繼承

抽象具體類

類簽名

sqlalchemy.ext.declarative.AbstractConcreteBase (sqlalchemy.ext.declarative.extensions.ConcreteBase)

class sqlalchemy.ext.declarative.ConcreteBase

用於‘具體’宣告對映的輔助類。

ConcreteBase 會自動使用 polymorphic_union() 函式,針對所有對映為該類的子類的表。該函式透過 __declare_last__() 函式呼叫,這實質上是 after_configured() 事件的鉤子。

ConcreteBase 為類本身生成一個對映表。與 AbstractConcreteBase 相比,後者不會。

示例:

from sqlalchemy.ext.declarative import ConcreteBase

class Employee(ConcreteBase, Base):
    __tablename__ = 'employee'
    employee_id = Column(Integer, primary_key=True)
    name = Column(String(50))
    __mapper_args__ = {
                    'polymorphic_identity':'employee',
                    'concrete':True}

class Manager(Employee):
    __tablename__ = 'manager'
    employee_id = Column(Integer, primary_key=True)
    name = Column(String(50))
    manager_data = Column(String(40))
    __mapper_args__ = {
                    'polymorphic_identity':'manager',
                    'concrete':True}

polymorphic_union() 使用的鑑別器列的預設名稱為 type。為了適應對映的用例,其中對映表中的實際列已命名為 type,可以透過設定 _concrete_discriminator_name 屬性來配置鑑別器名稱:

class Employee(ConcreteBase, Base):
    _concrete_discriminator_name = '_concrete_discriminator'

自版本 1.3.19 中新增:為 ConcreteBase 新增了 _concrete_discriminator_name 屬性,以便自定義虛擬鑑別器列名稱。

自版本 1.4.2 中更改:只需將 _concrete_discriminator_name 屬性放置在最基類上即可使所有子類正確生效。如果對映列名稱與鑑別器名稱衝突,則現在會顯示顯式錯誤訊息,而在 1.3.x 系列中會有一些警告,然後生成一個無用的查詢。

另請參閱

AbstractConcreteBase

具體表繼承

class sqlalchemy.ext.declarative.DeferredReflection

一個用於基於延遲反射步驟構建對映的輔助類。

通常情況下,透過將一個 Table 物件設定為具有 autoload_with=engine 的 __table__ 屬性,可以使用反射來使用宣告。一個宣告性類。需要注意的是,在構建普通宣告性對映的時候,Table 必須是完全反映的,或者至少有一個主鍵列,這意味著在類宣告時必須可用 Engine

DeferredReflection mixin 將對映器的構建移動到稍後的時間點,在呼叫首先反射到目前為止建立的所有 Table 物件的特定方法之後。類可以定義如下:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import DeferredReflection
Base = declarative_base()

class MyClass(DeferredReflection, Base):
    __tablename__ = 'mytable'

在上面,MyClass 還沒有對映。在上述方式定義了一系列類之後,可以使用 prepare() 反射所有表並建立對映:

engine = create_engine("someengine://...")
DeferredReflection.prepare(engine)

DeferredReflection mixin 可以應用於單個類,用作宣告基類本身,或用於自定義抽象類。使用抽象基類允許僅為特定準備步驟準備一部分類,這對於使用多個引擎的應用程式是必要的。例如,如果一個應用程式有兩個引擎,您可能會使用兩個基類,並分別準備每個基類,例如:

class ReflectedOne(DeferredReflection, Base):
    __abstract__ = True

class ReflectedTwo(DeferredReflection, Base):
    __abstract__ = True

class MyClass(ReflectedOne):
    __tablename__ = 'mytable'

class MyOtherClass(ReflectedOne):
    __tablename__ = 'myothertable'

class YetAnotherClass(ReflectedTwo):
    __tablename__ = 'yetanothertable'

# ... etc.

在上面,ReflectedOneReflectedTwo 的類層次結構可以分別配置:

ReflectedOne.prepare(engine_one)
ReflectedTwo.prepare(engine_two)

成員

prepare()

另請參閱

使用 DeferredReflection - 在 使用宣告式配置表 部分。

classmethod prepare(bind: Engine | Connection, **reflect_kw: Any) → None

反射所有當前 DeferredReflection 子類的所有 Table 物件

引數:

  • bind

    EngineConnection 例項

    ..versionchanged:: 2.0.16 現在也接受 Connection

  • **reflect_kw

    傳遞給 MetaData.reflect() 的其他關鍵字引數,例如 MetaData.reflect.views

    新版本 2.0.16 中的內容。

Mypy / Pep-484 對 ORM 對映的支援

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

當使用直接引用 Column 物件而不是 SQLAlchemy 2.0 中引入的 mapped_column() 構造時,支援 PEP 484 型別註釋以及 MyPy 型別檢查工具。

自 2.0 版開始已被棄用:SQLAlchemy Mypy 外掛已棄用,並且可能在 SQLAlchemy 2.1 釋出時被移除。我們建議使用者儘快遷移。

無法跨不斷變化的 mypy 釋出維護此外掛,未來的穩定性不能保證。

現代 SQLAlchemy 現在提供了 完全符合 pep-484 的對映語法;請參閱連結的部分以獲取遷移詳情。

安裝

僅適用於 SQLAlchemy 2.0:不應安裝存根,並且應完全解除安裝諸如 sqlalchemy-stubssqlalchemy2-stubs 等軟體包。

Mypy 包本身是一個依賴項。

可以使用 pip 使用“mypy”額外鉤子安裝 Mypy:

pip install sqlalchemy[mypy]

外掛本身如 Configuring mypy to use Plugins 中描述的那樣配置,使用 sqlalchemy.ext.mypy.plugin 模組名,例如在 setup.cfg 中:

[mypy]
plugins = sqlalchemy.ext.mypy.plugin

外掛功能

Mypy 外掛的主要目的是攔截並修改 SQLAlchemy 宣告性對映 的靜態定義,使其與它們在被其 Mapper 物件 instrumented 後的結構相匹配。這允許類結構本身以及使用類的程式碼對 Mypy 工具有意義,否則基於當前宣告性對映的功能,這是不可能的。該外掛類似於需要為類似 dataclasses 這樣的庫修改類的動態外掛。

為了涵蓋這種情況經常發生的主要區域,考慮以下 ORM 對映,使用 User 類的典型示例:

from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base

# "Base" is a class that is created dynamically from the
# declarative_base() function
Base = declarative_base()

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)

# "some_user" is an instance of the User class, which
# accepts "id" and "name" kwargs based on the mapping
some_user = User(id=5, name="user")

# it has an attribute called .name that's a string
print(f"Username: {some_user.name}")

# a select() construct makes use of SQL expressions derived from the
# User class itself
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))

上述,Mypy 擴充套件可以執行的步驟包括:

  • 解釋由 declarative_base() 生成的 Base 動態類,以便從中繼承的類被認為是對映的。它還可以適應在使用裝飾器進行宣告式對映(無宣告式基類)中描述的類裝飾器方法。

  • 對在宣告式“內聯”樣式中定義的 ORM 對映屬性進行型別推斷,例如上面示例中 User 類的 idname 屬性。這包括 User 的例項將使用 int 型別的 idstr 型別的 name。還包括當訪問 User.idUser.name 類級屬性時,如上面的 select() 語句中所示,它們與 SQL 表示式行為相容,這是從 InstrumentedAttribute 屬性描述符類派生的。

  • __init__() 方法應用於尚未包含顯式建構函式的對映類,該建構函式接受檢測到的所有對映屬性的特定型別的關鍵字引數。

當 Mypy 外掛處理上述檔案時,傳遞給 Mypy 工具的結果靜態類定義和 Python 程式碼等效於以下內容:

from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm.decl_api import DeclarativeMeta

class Base(metaclass=DeclarativeMeta):
    __abstract__ = True

class User(Base):
    __tablename__ = "user"

    id: Mapped[Optional[int]] = Mapped._special_method(
        Column(Integer, primary_key=True)
    )
    name: Mapped[Optional[str]] = Mapped._special_method(Column(String))

    def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ...

some_user = User(id=5, name="user")

print(f"Username: {some_user.name}")

select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))

上述已經採取的關鍵步驟包括:

  • Base 類現在明確地是基於 DeclarativeMeta 類定義的,而不再是一個動態類。

  • idname 屬性是基於 Mapped 類定義的,該類代表一個在類和例項級別表現出不同行為的 Python 描述符。Mapped 類現在是用於所有 ORM 對映屬性的 InstrumentedAttribute 類的基類。

    Mapped 被定義為一個針對任意 Python 型別的通用類,這意味著特定的 Mapped 例項與特定的 Python 型別相關聯,例如上面的 Mapped[Optional[int]]Mapped[Optional[str]

  • 宣告性對映屬性賦值的右側被移除,因為這類似於Mapper類通常要執行的操作,即它將用InstrumentedAttribute](../internals.html#sqlalchemy.orm.InstrumentedAttribute "sqlalchemy.orm.InstrumentedAttribute")的特定例項替換這些屬性。原始表示式移動到一個函式呼叫中,這樣可以仍然進行型別檢查而不與表示式的左側衝突。對於 Mypy 來說,左側的型別註釋足以理解屬性的行為。

  • 新增了User.__init__()方法的型別存根,其中包括了正確的關鍵字和資料型別。

用法

以下各小節將討論到目前為止已經考慮到的符合 PEP-484 的各種使用情況。

基於 TypeEngine 的列的內省

對於包含顯式資料型別的對映列,當它們被對映為內聯屬性時,對映型別將被自動內省:

class MyClass(Base):
    # ...

    id = Column(Integer, primary_key=True)
    name = Column("employee_name", String(50), nullable=False)
    other_name = Column(String(50))

上述,idnameother_name的最終類級資料型別將被內省為Mapped[Optional[int]]Mapped[Optional[str]]Mapped[Optional[str]]。這些型別預設始終被認為是Optional,即使對於主鍵和非空列也是如此。原因是因為雖然資料庫列idname不能為 NULL,但 Python 屬性idname很可能是None,而不需要顯式的建構函式:

>>> m1 = MyClass()
>>> m1.id
None

上述列的型別可以被顯式地宣告,提供了更清晰的自我文件化以及能夠控制哪些型別是可選的兩個優點:

class MyClass(Base):
    # ...

    id: int = Column(Integer, primary_key=True)
    name: str = Column("employee_name", String(50), nullable=False)
    other_name: Optional[str] = Column(String(50))

Mypy 外掛將接受上述intstrOptional[str]並將它們轉換為包含在其周圍的Mapped[]型別。Mapped[]構造也可以被顯式使用:

from sqlalchemy.orm import Mapped

class MyClass(Base):
    # ...

    id: Mapped[int] = Column(Integer, primary_key=True)
    name: Mapped[str] = Column("employee_name", String(50), nullable=False)
    other_name: Mapped[Optional[str]] = Column(String(50))

當型別是非可選時,這意味著從MyClass的例項中訪問的屬性將被認為是非None的:

mc = MyClass(...)

# will pass mypy --strict
name: str = mc.name

對於可選屬性,Mypy 認為型別必須包含 None,否則就是Optional

mc = MyClass(...)

# will pass mypy --strict
other_name: Optional[str] = mc.name

無論對映的屬性是否被標記為Optional__init__()方法的生成都仍然認為所有關鍵字都是可選的。這再次與 SQLAlchemy ORM 在建立建構函式時實際執行的操作相匹配,不應與諸如 Python dataclasses之類的驗證系統的行為混淆,後者將生成一個根據註釋匹配的建構函式,包括可選和必需的屬性。

沒有明確型別的列

包含ForeignKey修飾符的列在 SQLAlchemy 宣告對映中不需要指定資料型別。對於這種型別的屬性,Mypy 外掛將通知使用者需要傳送明確的型別:

# .. other imports
from sqlalchemy.sql.schema import ForeignKey

Base = declarative_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)
    user_id = Column(ForeignKey("user.id"))

外掛將按以下方式傳遞訊息:

$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)

要解決問題,請對Address.user_id列應用明確的型別註釋:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

使用命令式表對映列

在命令式表樣式中,Column定義位於與對映屬性本身分開的Table構造內。Mypy 外掛不考慮這個Table,而是支援可以明確宣告屬性,並且必須使用Mapped類將其標識為對映屬性:

class MyClass(Base):
    __table__ = Table(
        "mytable",
        Base.metadata,
        Column(Integer, primary_key=True),
        Column("employee_name", String(50), nullable=False),
        Column(String(50)),
    )

    id: Mapped[int]
    name: Mapped[str]
    other_name: Mapped[Optional[str]]

上述Mapped註釋被視為對映列,並將包含在預設建構函式中,同時為MyClass在類級別和例項級別提供正確的型別配置檔案。

對映關係

該外掛對使用型別推斷來檢測關係型別有限支援。對於所有無法檢測型別的情況,它將發出資訊豐富的錯誤訊息,並且在所有情況下,可以明確提供適當的型別,要麼使用Mapped類,要麼選擇在內聯宣告中省略它。外掛還需要確定關係是指向集合還是標量,並且為此依賴於relationship.uselist和/或relationship.collection_class引數的顯式值。如果這些引數都不存在,則需要明確的型別,以及如果relationship()的目標型別是字串或可呼叫物件,而不是類:

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)
    user_id: int = Column(ForeignKey("user.id"))

    user = relationship(User)

上述對映將產生以下錯誤:

test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)

可以透過使用relationship(User, uselist=False)或提供型別來解決錯誤,在這種情況下是標量User物件:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: User = relationship(User)

對於集合,類似的模式也適用,即在沒有uselist=Truerelationship.collection_class的情況下,可以使用諸如List之類的集合註釋。還可以完全適當地使用類的字串名稱進行註釋,如 pep-484 所支援,確保根據需要在TYPE_CHECKING 塊中匯入類:

from typing import TYPE_CHECKING, List

from .mymodel import Base

if TYPE_CHECKING:
    # if the target of the relationship is in another module
    # that cannot normally be imported at runtime
    from .myaddressmodel import Address

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    addresses: List["Address"] = relationship("Address")

與列一樣,Mapped 類也可以顯式應用:

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: Mapped[User] = relationship(User, back_populates="addresses")

使用 @declared_attr 和宣告性混合類

declared_attr 類允許在類級別函式中宣告宣告性對映的屬性,並且在使用宣告性混合類時特別有用。對於這些函式,函式的返回型別應使用Mapped[]構造或指示函式返回的確切物件型別進行註釋。此外,“mixin”類(即不以declarative_base()類擴充套件,也不使用諸如registry.mapped()之類的方法對映的類)應該被裝飾上 declarative_mixin() 裝飾器,這為 Mypy 外掛提供了一個提示,指明特定的類意圖充當宣告性混合類:

from sqlalchemy.orm import declarative_mixin, declared_attr

@declarative_mixin
class HasUpdatedAt:
    @declared_attr
    def updated_at(cls) -> Column[DateTime]:  # uses Column
        return Column(DateTime)

@declarative_mixin
class HasCompany:
    @declared_attr
    def company_id(cls) -> Mapped[int]:  # uses Mapped
        return Column(ForeignKey("company.id"))

    @declared_attr
    def company(cls) -> Mapped["Company"]:
        return relationship("Company")

class Employee(HasUpdatedAt, HasCompany, Base):
    __tablename__ = "employee"

    id = Column(Integer, primary_key=True)
    name = Column(String)

注意方法HasCompany.company的實際返回型別與註釋之間的不匹配。Mypy 外掛將所有@declared_attr函式轉換為簡單的帶註釋的屬性,以避免這種複雜性:

# what Mypy sees
class HasCompany:
    company_id: Mapped[int]
    company: Mapped["Company"]

與資料類或其他型別敏感的屬性系統相結合

在 將 ORM 對映應用到現有資料類(遺留資料類用法) 中的 Python 資料類整合示例存在一個問題;Python 資料類期望明確的型別,它將用於構建類,並且每個賦值語句中給定的值都是重要的。也就是說,必須確切地宣告以下類才能被資料類接受:

mapper_registry: registry = registry()

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
    addresses: List[Address] = field(default_factory=list)

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

我們不能將我們的Mapped[]型別應用於屬性idname等,因為它們會被@dataclass裝飾器拒絕。此外,Mypy 還有另一個專門用於資料類的外掛,這也可能妨礙我們的操作。

上述類實際上會順利透過 Mypy 的型別檢查;我們唯一缺少的是在User上的屬性可用於 SQL 表示式,例如:

stmt = select(User.name).where(User.id.in_([1, 2, 3]))

為了提供一個解決方法,Mypy 外掛具有一個額外的功能,我們可以指定一個額外的屬性 _mypy_mapped_attrs,它是一個包含類級物件或它們的字串名稱的列表。這個屬性可以在 TYPE_CHECKING 變數內部是條件性的:

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str]
    nickname: Optional[str]
    addresses: List[Address] = field(default_factory=list)

    if TYPE_CHECKING:
        _mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

使用上述方法,列在 _mypy_mapped_attrs 中的屬性將應用 Mapped 型別資訊,以便在類繫結上下文中使用 User 類時,它將表現為一個 SQLAlchemy 對映類。

安裝

對於 僅適用於 SQLAlchemy 2.0:不應安裝存根,而應完全解除安裝像 sqlalchemy-stubssqlalchemy2-stubs 這樣的包。

Mypy 包本身是一個依賴項。

可以使用 pip 使用 “mypy” extras 鉤子安裝 Mypy:

pip install sqlalchemy[mypy]

外掛本身配置如 配置 mypy 使用外掛 中所述,使用 sqlalchemy.ext.mypy.plugin 模組名稱,例如在 setup.cfg 中:

[mypy]
plugins = sqlalchemy.ext.mypy.plugin

外掛的功能

Mypy 外掛的主要目的是攔截和修改 SQLAlchemy 宣告式對映 的靜態定義,以使其與它們在被 Mapper 物件 儀器化 後的結構相匹配。這使得類結構本身以及使用類的程式碼對 Mypy 工具有意義,否則根據當前宣告式對映的功能,這將不是情況。該外掛類似於為像 dataclasses 這樣的庫所需的類似外掛,這些外掛在執行時動態地修改類。

要涵蓋此類情況經常發生的主要領域,請考慮以下 ORM 對映,使用 User 類的典型示例:

from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base

# "Base" is a class that is created dynamically from the
# declarative_base() function
Base = declarative_base()

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)

# "some_user" is an instance of the User class, which
# accepts "id" and "name" kwargs based on the mapping
some_user = User(id=5, name="user")

# it has an attribute called .name that's a string
print(f"Username: {some_user.name}")

# a select() construct makes use of SQL expressions derived from the
# User class itself
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))

上面,Mypy 擴充套件可以執行的步驟包括:

  • 對由 declarative_base() 生成的 Base 動態類進行解釋,以便繼承它的類被知道是已對映的。它還可以適應 使用裝飾器進行宣告式對映(無宣告式基類) 中描述的類裝飾器方法。

  • 對於在宣告式“內聯”樣式中定義的 ORM 對映屬性的型別推斷,例如上面示例中 User 類的 idname 屬性。這包括 User 例項將使用 int 型別的 idstr 型別的 name。它還包括當訪問 User.idUser.name 類級屬性時,正如它們在上面的 select() 語句中那樣,它們與 SQL 表示式行為相容,這是從 InstrumentedAttribute 屬性描述符類派生的。

  • __init__() 方法應用於尚未包含顯式建構函式的對映類,該建構函式接受特定型別的關鍵字引數,用於檢測到的所有對映屬性。

當 Mypy 外掛處理上述檔案時,結果的靜態類定義和傳遞給 Mypy 工具的 Python 程式碼等效於以下內容:

from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm.decl_api import DeclarativeMeta

class Base(metaclass=DeclarativeMeta):
    __abstract__ = True

class User(Base):
    __tablename__ = "user"

    id: Mapped[Optional[int]] = Mapped._special_method(
        Column(Integer, primary_key=True)
    )
    name: Mapped[Optional[str]] = Mapped._special_method(Column(String))

    def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ...

some_user = User(id=5, name="user")

print(f"Username: {some_user.name}")

select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))

以上已經採取的關鍵步驟包括:

  • Base 類現在明確地以 DeclarativeMeta 類的形式定義,而不是動態類。

  • idname 屬性是以 Mapped 類的術語定義的,該類表示在類與例項級別上表現出不同行為的 Python 描述符。Mapped 類現在是用於所有 ORM 對映屬性的 InstrumentedAttribute 類的基類。

    Mapped 被定義為針對任意 Python 型別的通用類,這意味著 Mapped 的特定出現與特定的 Python 型別相關聯,例如上面的 Mapped[Optional[int]]Mapped[Optional[str]]

  • 宣告式對映屬性分配的右側 已移除,因為這類似於 Mapper 類通常會執行的操作,即它將這些屬性替換為 InstrumentedAttribute 的具體例項。原始表示式移到一個函式呼叫中,這將允許它仍然被型別檢查而不與表示式的左側發生衝突。對於 Mypy 來說,左側的型別註釋足以理解屬性的行為。

  • User.__init__() 方法新增了型別存根,其中包括正確的關鍵字和資料型別。

使用方法

以下各小節將討論迄今為止已考慮到的個別用例的 pep-484 符合性。

基於 TypeEngine 的列的自省

對於包含顯式資料型別的對映列,當它們作為內聯屬性對映時,對映型別將被自動解析:

class MyClass(Base):
    # ...

    id = Column(Integer, primary_key=True)
    name = Column("employee_name", String(50), nullable=False)
    other_name = Column(String(50))

在上面,idnameother_name 這些最終的類級別資料型別將被解析為 Mapped[Optional[int]]Mapped[Optional[str]]Mapped[Optional[str]]。這些型別預設情況下總是被認為是 Optional 的,即使對於主鍵和非空列也是如此。原因是因為雖然資料庫列idname不能為 NULL,但 Python 屬性 idname 在沒有顯式建構函式的情況下肯定可以是 None

>>> m1 = MyClass()
>>> m1.id
None

上述列的型別可以被顯式地宣告,提供了更清晰的自我文件說明以及能夠控制哪些型別是可選的兩個優點:

class MyClass(Base):
    # ...

    id: int = Column(Integer, primary_key=True)
    name: str = Column("employee_name", String(50), nullable=False)
    other_name: Optional[str] = Column(String(50))

Mypy 外掛將接受上述的 intstrOptional[str],並將它們轉換為包含在 Mapped[] 型別周圍的型別。Mapped[] 結構也可以被顯式使用:

from sqlalchemy.orm import Mapped

class MyClass(Base):
    # ...

    id: Mapped[int] = Column(Integer, primary_key=True)
    name: Mapped[str] = Column("employee_name", String(50), nullable=False)
    other_name: Mapped[Optional[str]] = Column(String(50))

當型別是非可選時,這意味著從 MyClass 例項訪問的屬性將被視為非 None:

mc = MyClass(...)

# will pass mypy --strict
name: str = mc.name

對於可選屬性,Mypy 認為型別必須包括 None,否則為 Optional

mc = MyClass(...)

# will pass mypy --strict
other_name: Optional[str] = mc.name

無論對映屬性是否被標記為 Optional,生成的 __init__() 方法仍然將所有關鍵字視為可選的。這再次與 SQLAlchemy ORM 實際建立建構函式時的行為相匹配,不應與諸如 Python dataclasses 之類的驗證系統的行為混淆,後者將生成一個與註釋匹配的建構函式,以確定可選 vs. 必需屬性的註解。

沒有明確型別的列

包含 ForeignKey 修改器的列在 SQLAlchemy 宣告式對映中不需要指定資料型別。對於這種型別的屬性,Mypy 外掛將通知使用者需要傳送一個顯式型別:

# .. other imports
from sqlalchemy.sql.schema import ForeignKey

Base = declarative_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)
    user_id = Column(ForeignKey("user.id"))

外掛將如下傳遞訊息:

$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)

要解決此問題,請為 Address.user_id 列應用顯式型別註釋:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

使用命令式表格對映列

在命令式表格風格中,Column 定義位於一個與對映屬性本身分離的 Table 結構內。Mypy 外掛不考慮這個 Table,而是支援可以顯式宣告屬性,必須使用 Mapped 類來標識它們為對映屬性:

class MyClass(Base):
    __table__ = Table(
        "mytable",
        Base.metadata,
        Column(Integer, primary_key=True),
        Column("employee_name", String(50), nullable=False),
        Column(String(50)),
    )

    id: Mapped[int]
    name: Mapped[str]
    other_name: Mapped[Optional[str]]

上述Mapped註釋被視為對映列,並將包含在預設建構函式中,以及在類級別和例項級別為MyClass提供正確的型別配置檔案。

對映關係

該外掛對使用型別推斷來檢測關係的型別有限支援。對於所有這些無法檢測到型別的情況,它都將發出一個資訊豐富的錯誤訊息,在所有情況下,可以明確提供適當的型別,要麼使用Mapped類,要麼選擇在內聯宣告中省略它。該外掛還需要確定關係是指向集合還是標量,並且為此依賴於relationship.uselist和/或relationship.collection_class引數的顯式值。如果這些引數都不存在,則需要明確的型別,以及如果relationship()的目標型別是字串或可呼叫物件而不是類,則也需要明確的型別:

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)
    user_id: int = Column(ForeignKey("user.id"))

    user = relationship(User)

上述對映將產生以下錯誤:

test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)

錯誤可以透過使用relationship(User, uselist=False)或者提供型別來解決,例如標量User物件:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: User = relationship(User)

對於集合,類似的模式也適用,如果沒有uselist=True或者relationship.collection_class,可以使用集合註釋,如List。在註釋中使用類的字串名稱也是完全合適的,這是由 pep-484 支援的,確保在適當的時候在TYPE_CHECKING 塊中匯入該類:

from typing import TYPE_CHECKING, List

from .mymodel import Base

if TYPE_CHECKING:
    # if the target of the relationship is in another module
    # that cannot normally be imported at runtime
    from .myaddressmodel import Address

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    addresses: List["Address"] = relationship("Address")

與列相似,Mapped類也可以顯式地應用:

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: Mapped[User] = relationship(User, back_populates="addresses")

使用 @declared_attr 和宣告性混合

declared_attr類允許在類級函式中宣告 Declarative 對映屬性,並且在使用宣告性混入時特別有用。對於這些函式,函式的返回型別應該使用Mapped[]構造進行註釋,或者指示函式返回的物件的確切型別。此外,未被對映的“混入”類(即不從declarative_base()類繼承,也不使用諸如registry.mapped()之類的方法進行對映)應該用declarative_mixin()裝飾器進行修飾,這為 Mypy 外掛提供了一個提示,表明特定類意圖作為宣告性混入:

from sqlalchemy.orm import declarative_mixin, declared_attr

@declarative_mixin
class HasUpdatedAt:
    @declared_attr
    def updated_at(cls) -> Column[DateTime]:  # uses Column
        return Column(DateTime)

@declarative_mixin
class HasCompany:
    @declared_attr
    def company_id(cls) -> Mapped[int]:  # uses Mapped
        return Column(ForeignKey("company.id"))

    @declared_attr
    def company(cls) -> Mapped["Company"]:
        return relationship("Company")

class Employee(HasUpdatedAt, HasCompany, Base):
    __tablename__ = "employee"

    id = Column(Integer, primary_key=True)
    name = Column(String)

注意像HasCompany.company這樣的方法的實際返回型別與註釋的不匹配。Mypy 外掛將所有@declared_attr函式轉換為簡單的註釋屬性,以避免這種複雜性:

# what Mypy sees
class HasCompany:
    company_id: Mapped[int]
    company: Mapped["Company"]

與 Dataclasses 或其他型別敏感的屬性系統結合

Python dataclasses 整合的示例在將 ORM 對映應用於現有資料類(傳統資料類用法)中提出了一個問題;Python dataclasses 期望一個明確的型別,它將用於構建類,並且每個賦值語句中給定的值是重要的。也就是說,一個如下所示的類必須要準確地宣告才能被 dataclasses 接受:

mapper_registry: registry = registry()

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
    addresses: List[Address] = field(default_factory=list)

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

我們無法將我們的Mapped[]型別應用於屬性idname等,因為它們將被@dataclass裝飾器拒絕。此外,Mypy 還有另一個專門用於 dataclasses 的外掛,這也可能影響我們的操作。

上述類實際上會透過 Mypy 的型別檢查而沒有問題;我們唯一缺少的是User上的屬效能夠在 SQL 表示式中使用,比如:

stmt = select(User.name).where(User.id.in_([1, 2, 3]))

為了解決這個問題,Mypy 外掛有一個額外的功能,我們可以指定一個額外的屬性_mypy_mapped_attrs,這是一個包含類級物件或它們的字串名稱的列表。這個屬性可以在TYPE_CHECKING變數內部進行條件判斷:

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str]
    nickname: Optional[str]
    addresses: List[Address] = field(default_factory=list)

    if TYPE_CHECKING:
        _mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

使用上述方法,列在_mypy_mapped_attrs中列出的屬性將應用於Mapped型別資訊,以便在類繫結上下文中使用User類時,它將表現為一個 SQLAlchemy 對映類。

基於 TypeEngine 的列的內省

對於包含顯式資料型別的對映列,當它們被對映為內聯屬性時,對映型別將自動進行內省:

class MyClass(Base):
    # ...

    id = Column(Integer, primary_key=True)
    name = Column("employee_name", String(50), nullable=False)
    other_name = Column(String(50))

在上面,idnameother_name的最終類級資料型別將被內省為Mapped[Optional[int]]Mapped[Optional[str]]Mapped[Optional[str]]。型別預設始終被視為可選,即使對於主鍵和非空列也是如此。原因是因為雖然資料庫列idname不能為 NULL,但 Python 屬性idname可以毫無疑問地是None,而不需要顯式建構函式:

>>> m1 = MyClass()
>>> m1.id
None

上述列的型別可以明確宣告,提供兩個優勢,即更清晰的自我文件化以及能夠控制哪些型別是可選的:

class MyClass(Base):
    # ...

    id: int = Column(Integer, primary_key=True)
    name: str = Column("employee_name", String(50), nullable=False)
    other_name: Optional[str] = Column(String(50))

Mypy 外掛將接受上述intstrOptional[str],並將它們轉換為包圍它們的Mapped[]型別。Mapped[]結構也可以明確使用:

from sqlalchemy.orm import Mapped

class MyClass(Base):
    # ...

    id: Mapped[int] = Column(Integer, primary_key=True)
    name: Mapped[str] = Column("employee_name", String(50), nullable=False)
    other_name: Mapped[Optional[str]] = Column(String(50))

當型別為非可選時,這意味著從MyClass例項中訪問的屬性將被視為非None

mc = MyClass(...)

# will pass mypy --strict
name: str = mc.name

對於可選屬性,Mypy 認為型別必須包含 None,否則為Optional

mc = MyClass(...)

# will pass mypy --strict
other_name: Optional[str] = mc.name

無論對映屬性是否被標記為Optional__init__()方法的生成仍然考慮所有關鍵字都是可選的。這再次與 SQLAlchemy ORM 實際建立建構函式時的行為相匹配,不應與驗證系統(如 Python dataclasses)的行為混淆,後者將根據註釋生成與可選與必需屬性相匹配的建構函式。

不具有顯式型別的列

包含 ForeignKey 修改器的列在 SQLAlchemy 宣告性對映中不需要指定資料型別。對於這種型別的屬性,Mypy 外掛將通知使用者需要傳送顯式型別:

# .. other imports
from sqlalchemy.sql.schema import ForeignKey

Base = declarative_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)
    user_id = Column(ForeignKey("user.id"))

外掛將以以下方式傳送訊息:

$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)

要解決此問題,請對Address.user_id列應用顯式型別註釋:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

使用命令式表對映列

在 命令式表風格 中,Column 定義放在一個獨立於對映屬性本身的 Table 結構中。Mypy 外掛不考慮這個Table,而是支援可以明確宣告屬性,並且必須使用 Mapped 類來標識它們為對映屬性:

class MyClass(Base):
    __table__ = Table(
        "mytable",
        Base.metadata,
        Column(Integer, primary_key=True),
        Column("employee_name", String(50), nullable=False),
        Column(String(50)),
    )

    id: Mapped[int]
    name: Mapped[str]
    other_name: Mapped[Optional[str]]

上述Mapped註釋被視為對映列,並將包含在預設建構函式中,同時為MyClass提供正確的型別配置檔案,無論是在類級別還是例項級別。

對映關係

該外掛對使用型別推斷來檢測關係型別有限支援。對於所有無法檢測型別的情況,它將發出資訊豐富的錯誤訊息,並且在所有情況下,可以明確提供適當的型別,可以使用Mapped類或選擇性地省略內聯宣告。外掛還需要確定關係是引用集合還是標量,為此依賴於relationship.uselist和/或relationship.collection_class引數的顯式值。如果這些引數都不存在,則需要明確型別,以及如果relationship()的目標型別是字串或可呼叫的,而不是類:

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)
    user_id: int = Column(ForeignKey("user.id"))

    user = relationship(User)

上述對映將產生以下錯誤:

test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)

可以透過使用relationship(User, uselist=False)或提供型別來解決錯誤,在這種情況下是標量User物件:

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: User = relationship(User)

對於集合,類似的模式適用,如果沒有uselist=Truerelationship.collection_class,可以使用List等集合註釋。在註釋中使用類的字串名稱也是完全適當的,支援 pep-484,確保類在TYPE_CHECKING block中適當匯入:

from typing import TYPE_CHECKING, List

from .mymodel import Base

if TYPE_CHECKING:
    # if the target of the relationship is in another module
    # that cannot normally be imported at runtime
    from .myaddressmodel import Address

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    addresses: List["Address"] = relationship("Address")

與列一樣,Mapped類也可以顯式應用:

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: Mapped[User] = relationship(User, back_populates="addresses")

使用@declared_attr 和宣告性混合

declared_attr 類允許在類級函式中宣告宣告性對映屬性,並且在使用宣告性混合時特別有用。對於這些函式,函式的返回型別應該使用Mapped[]構造進行註釋,或者指示函式返回的確切物件型別。另外,未以其他方式對映的“混合”類(即不從declarative_base()類擴充套件,也不使用諸如registry.mapped()之類的方法進行對映)應該用declarative_mixin()裝飾器進行裝飾,這為 Mypy 外掛提供了一個提示,表明特定的類打算作為宣告性混合使用:

from sqlalchemy.orm import declarative_mixin, declared_attr

@declarative_mixin
class HasUpdatedAt:
    @declared_attr
    def updated_at(cls) -> Column[DateTime]:  # uses Column
        return Column(DateTime)

@declarative_mixin
class HasCompany:
    @declared_attr
    def company_id(cls) -> Mapped[int]:  # uses Mapped
        return Column(ForeignKey("company.id"))

    @declared_attr
    def company(cls) -> Mapped["Company"]:
        return relationship("Company")

class Employee(HasUpdatedAt, HasCompany, Base):
    __tablename__ = "employee"

    id = Column(Integer, primary_key=True)
    name = Column(String)

請注意,像HasCompany.company這樣的方法的實際返回型別與其註釋之間存在不匹配。Mypy 外掛將所有@declared_attr函式轉換為簡單的註釋屬性,以避免這種複雜性:

# what Mypy sees
class HasCompany:
    company_id: Mapped[int]
    company: Mapped["Company"]

與資料類或其他型別敏感的屬性系統結合

Python 資料類整合示例中的將 ORM 對映應用到現有資料類(舊資料類使用)存在一個問題;Python 資料類期望一個明確的型別,它將用於構建類,並且在每個賦值語句中給定的值是重要的。也就是說,必須準確地宣告如下的類才能被資料類接受:

mapper_registry: registry = registry()

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
    addresses: List[Address] = field(default_factory=list)

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

我們無法將我們的Mapped[]型別應用於屬性idname等,因為它們將被@dataclass裝飾器拒絕。另外,Mypy 還有另一個專門針對資料類的外掛,這也可能妨礙我們的操作。

上述類實際上將無障礙地透過 Mypy 的型別檢查;我們唯一缺少的是User上屬性被用於 SQL 表示式的能力,例如:

stmt = select(User.name).where(User.id.in_([1, 2, 3]))

為此提供一種解決方案,Mypy 外掛具有一個額外的功能,我們可以指定一個額外的屬性_mypy_mapped_attrs,它是一個包含類級物件或它們的字串名稱的列表。該屬性可以在TYPE_CHECKING變數中條件化:

@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str]
    nickname: Optional[str]
    addresses: List[Address] = field(default_factory=list)

    if TYPE_CHECKING:
        _mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]

    __mapper_args__ = {  # type: ignore
        "properties": {"addresses": relationship("Address")}
    }

使用上述方法,將在_mypy_mapped_attrs中列出的屬性應用Mapped型別資訊,以便在類繫結上下文中使用User類時,它將表現為 SQLAlchemy 對映類。

突變跟蹤

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

提供對標量值的就地更改的跟蹤支援,這些更改傳播到擁有父物件上的 ORM 更改事件中。

在標量列值上建立可變性

“可變”結構的典型示例是 Python 字典。按照 SQL 資料型別物件 中介紹的示例,我們從一個自定義型別開始,該型別將 Python 字典編組為 JSON 字串,然後再進行持久化:

from sqlalchemy.types import TypeDecorator, VARCHAR
import json

class JSONEncodedDict(TypeDecorator):
    "Represents an immutable structure as a json-encoded string."

    impl = VARCHAR

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = json.dumps(value)
        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            value = json.loads(value)
        return value

僅出於示例目的使用 jsonsqlalchemy.ext.mutable 擴充套件可與任何目標 Python 型別可能是可變的型別一起使用,包括 PickleTypeARRAY 等。

使用 sqlalchemy.ext.mutable 擴充套件時,值本身會跟蹤所有引用它的父物件。下面,我們展示了 MutableDict 字典物件的簡單版本,它將 Mutable mixin 應用於普通 Python 字典:

from sqlalchemy.ext.mutable import Mutable

class MutableDict(Mutable, dict):
    @classmethod
    def coerce(cls, key, value):
        "Convert plain dictionaries to MutableDict."

        if not isinstance(value, MutableDict):
            if isinstance(value, dict):
                return MutableDict(value)

            # this call will raise ValueError
            return Mutable.coerce(key, value)
        else:
            return value

    def __setitem__(self, key, value):
        "Detect dictionary set events and emit change events."

        dict.__setitem__(self, key, value)
        self.changed()

    def __delitem__(self, key):
        "Detect dictionary del events and emit change events."

        dict.__delitem__(self, key)
        self.changed()

上述字典類採用了子類化 Python 內建的 dict 的方法,以生成一個 dict 子類,該子類透過 __setitem__ 將所有突變事件路由到。這種方法有其變體,例如子類化 UserDict.UserDictcollections.MutableMapping;對於此示例而言,重要的部分是當資料結構發生就地更改時,將呼叫 Mutable.changed() 方法。

我們還重新定義了 Mutable.coerce() 方法,該方法將用於將不是 MutableDict 例項的任何值轉換為適當的型別,例如 json 模組返回的普通字典。定義此方法是可選的;我們也可以建立我們的 JSONEncodedDict,使其始終返回 MutableDict 的例項,並且還確保所有呼叫程式碼都顯式使用 MutableDict。當未覆蓋 Mutable.coerce() 時,應用於父物件的任何不是可變型別例項的值都將引發 ValueError

我們的新 MutableDict 型別提供了一個類方法 Mutable.as_mutable(),我們可以在列後設資料中使用它來關聯型別。該方法獲取給定的型別物件或類,並關聯一個監聽器,該監聽器將檢測到該型別的所有未來對映,並對對映的屬性應用事件監聽儀器。例如,使用經典的表後設資料:

from sqlalchemy import Table, Column, Integer

my_data = Table('my_data', metadata,
    Column('id', Integer, primary_key=True),
    Column('data', MutableDict.as_mutable(JSONEncodedDict))
)

在上面,Mutable.as_mutable() 返回一個 JSONEncodedDict 例項(如果型別物件尚不是例項),該例項將攔截針對該型別對映的任何屬性。下面我們建立一個簡單的對映與 my_data 表:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))

MyDataClass.data 成員現在將收到對其值的原地更改的通知。

MyDataClass.data 成員的任何原地更改都會在父物件上標記屬性為“髒”:

>>> from sqlalchemy.orm import Session

>>> sess = Session(some_engine)
>>> m1 = MyDataClass(data={'value1':'foo'})
>>> sess.add(m1)
>>> sess.commit()

>>> m1.data['value1'] = 'bar'
>>> assert m1 in sess.dirty
True

MutableDict 可以透過一步關聯所有未來的 JSONEncodedDict 例項,使用 Mutable.associate_with()。這類似於 Mutable.as_mutable(),但它將無條件地攔截所有對映中所有 MutableDict 的出現,而無需單獨宣告它:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

MutableDict.associate_with(JSONEncodedDict)

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(JSONEncodedDict)

支援 Pickling

sqlalchemy.ext.mutable 擴充套件的關鍵在於在值物件上放置了一個 weakref.WeakKeyDictionary,它儲存了父對映物件到與該值相關聯的屬性名稱的對映。 WeakKeyDictionary 物件不可 pickle,因為它們包含 weakrefs 和函式回撥。在我們的情況下,這是件好事,因為如果這個字典是可 pickle 的,那麼它可能會導致我們的值物件的 pickle 大小過大,因為它們在不涉及父物件上下文的情況下被單獨 pickle。開發人員在這裡的責任只是提供一個 __getstate__ 方法,該方法將 MutableBase._parents() 集合從 pickle 流中排除:

class MyMutableType(Mutable):
    def __getstate__(self):
        d = self.__dict__.copy()
        d.pop('_parents', None)
        return d

對於我們的字典示例,我們需要返回字典本身的內容(並在 __setstate__ 上也進行恢復):

class MutableDict(Mutable, dict):
    # ....

    def __getstate__(self):
        return dict(self)

    def __setstate__(self, state):
        self.update(state)

如果我們的可變值物件作為它附加到的一個或多個父物件一起被 pickle,那麼 Mutable mixin 將在每個值物件上重新建立 Mutable._parents 集合,因為擁有父物件本身被 unpickle。

接收事件

AttributeEvents.modified() 事件處理程式可用於在可變標量發出更改事件時接收事件。 當從可變擴充套件內呼叫 flag_modified() 函式時,將呼叫此事件處理程式:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy import event

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))

@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance, initiator):
    print("json value modified:", instance.data)
```  ## 在複合上建立可變性

複合是一種特殊的 ORM 功能,允許將單個標量屬性分配給一個物件值,該物件值表示從底層對映表的一個或多個列中“組合”而成的資訊。 通常示例是幾何“點”,並在 複合列型別 中介紹。

與 `Mutable` 一樣,使用者定義的複合類將 `MutableComposite` 作為一個混合類,透過 `MutableComposite.changed()` 方法檢測並傳遞更改事件給其父物件。 在複合類的情況下,檢測通常透過特殊的 Python 方法 `__setattr__()` 進行。 在下面的示例中,我們擴充套件了 複合列型別 中介紹的 `Point` 類,以包括 `MutableComposite` 在其基類中,並透過 `__setattr__` 將屬性設定事件路由到 `MutableComposite.changed()` 方法:

```py
import dataclasses
from sqlalchemy.ext.mutable import MutableComposite

@dataclasses.dataclass
class Point(MutableComposite):
    x: int
    y: int

    def __setattr__(self, key, value):
        "Intercept set events"

        # set the attribute
        object.__setattr__(self, key, value)

        # alert all parents to the change
        self.changed()

MutableComposite 類利用類對映事件自動為任何使用指定我們的 Point 型別的 composite() 的地方建立監聽器。 下面,當 Point 對映到 Vertex 類時,將建立監聽器,這些監聽器將將來自 Point 物件的更改事件路由到每個 Vertex.startVertex.end 屬性:

from sqlalchemy.orm import DeclarativeBase, Mapped
from sqlalchemy.orm import composite, mapped_column

class Base(DeclarativeBase):
    pass

class Vertex(Base):
    __tablename__ = "vertices"

    id: Mapped[int] = mapped_column(primary_key=True)

    start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1"))
    end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2"))

    def __repr__(self):
        return f"Vertex(start={self.start}, end={self.end})"

Vertex.startVertex.end 成員的任何原地更改都將在父物件上標記該屬性為“髒”:

>>> from sqlalchemy.orm import Session
>>> sess = Session(engine)
>>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
>>> sess.add(v1)
sql>>> sess.flush()
BEGIN  (implicit)
INSERT  INTO  vertices  (x1,  y1,  x2,  y2)  VALUES  (?,  ?,  ?,  ?)
[...]  (3,  4,  12,  15)
>>> v1.end.x = 8
>>> assert v1 in sess.dirty
True
sql>>> sess.commit()
UPDATE  vertices  SET  x2=?  WHERE  vertices.id  =  ?
[...]  (8,  1)
COMMIT 

強制轉換可變組合

MutableBase.coerce() 方法也支援複合型別。對於 MutableCompositeMutableBase.coerce() 方法僅在屬性設定操作時呼叫,而不在載入操作中呼叫。覆蓋 MutableBase.coerce() 方法基本上等同於為使用自定義複合型別的所有屬性使用 validates() 驗證程式:

@dataclasses.dataclass
class Point(MutableComposite):
    # other Point methods
    # ...

    def coerce(cls, key, value):
        if isinstance(value, tuple):
            value = Point(*value)
        elif not isinstance(value, Point):
            raise ValueError("tuple or Point expected")
        return value

支援 Pickling

Mutable 類似,MutableComposite 輔助類使用 weakref.WeakKeyDictionary,可透過 MutableBase._parents() 屬性獲得,該屬性不可 picklable。如果我們需要 pickle Point 的例項或其所屬的類 Vertex,我們至少需要定義一個不包含 _parents 字典的 __getstate__。下面我們定義了 Point 類的最小形式的 __getstate____setstate__

@dataclasses.dataclass
class Point(MutableComposite):
    # ...

    def __getstate__(self):
        return self.x, self.y

    def __setstate__(self, state):
        self.x, self.y = state

Mutable 一樣,MutableComposite 增強了父物件的物件關係狀態的 pickling 過程,以便 MutableBase._parents() 集合被恢復到所有 Point 物件中。

API 參考

物件名稱 描述
Mutable 混合類,定義對父物件的變更事件的透明傳播。
MutableBase MutableMutableComposite 的通用基類。
MutableComposite 混合類,定義對 SQLAlchemy “composite” 物件的變更事件的透明傳播,傳播到其擁有的父物件或父物件。
MutableDict 實現了 Mutable 的字典型別。
MutableList 實現了 Mutable 的列表型別。
MutableSet 實現Mutable的集合型別。
class sqlalchemy.ext.mutable.MutableBase

成員

_parents,coerce()

公共基類,用於MutableMutableComposite

attribute _parents

父物件的InstanceState->父物件上的屬性名稱的字典。

該屬性是所謂的“記憶化”屬性。首次訪問時,它會使用一個新的weakref.WeakKeyDictionary進行初始化,並在後續訪問時返回相同的物件。

在 1.4 版本中更改:現在使用InstanceState作為弱字典中的鍵,而不是例項本身。

classmethod coerce(key: str, value: Any) → Any | None

給定一個值,將其強制轉換為目標型別。

可以被自定義子類重寫,將傳入資料強制轉換為特定型別。

預設情況下,引發ValueError

根據父類是Mutable型別還是MutableComposite型別,在不同情況下呼叫此方法。對於前者,它在屬性設定操作和 ORM 載入操作期間都會被呼叫。對於後者,它僅在屬性設定操作期間被呼叫;composite()構造的機制在載入操作期間處理強制轉換。

引數:

  • key – 正在設定的 ORM 對映屬性的字串名稱。

  • value – 輸入值。

返回:

如果無法完成強制轉換,則該方法應返回強制轉換後的值,或引發ValueError

class sqlalchemy.ext.mutable.Mutable

定義透明傳播更改事件到父物件的混入。

檢視在標量列值上建立可變性中的示例以獲取用法資訊。

成員

_get_listen_keys(),_listen_on_attribute(),_parents,as_mutable(),associate_with(),associate_with_attribute(),changed(),coerce()

類簽名

sqlalchemy.ext.mutable.Mutablesqlalchemy.ext.mutable.MutableBase)

classmethod _get_listen_keys(attribute: QueryableAttribute[Any]) → Set[str]

繼承自 MutableBase sqlalchemy.ext.mutable.MutableBase._get_listen_keys 方法

給定一個描述符屬性,返回一個指示此屬性狀態變化的屬性鍵的set()

這通常只是set([attribute.key]),但可以被覆蓋以提供額外的鍵。例如,MutableComposite會用與組成複合值的列相關聯的屬性鍵來增加這個集合。

在攔截InstanceEvents.refresh()InstanceEvents.refresh_flush()事件時,將查詢此集合,這些事件傳遞了已重新整理的屬性名稱列表;該列表與此集合進行比較,以確定是否需要採取行動。

classmethod _listen_on_attribute(attribute: QueryableAttribute[Any], coerce: bool, parent_cls: _ExternalEntityType[Any]) → None

繼承自 MutableBase sqlalchemy.ext.mutable.MutableBase._listen_on_attribute 方法

將此型別建立為給定對映描述符的變異監聽器。

attribute _parents

繼承自 MutableBase sqlalchemy.ext.mutable.MutableBase._parents 屬性

父物件的InstanceState->父物件上的屬性名的字典。

此屬性是所謂的“記憶化”屬性。它在第一次訪問時使用一個新的weakref.WeakKeyDictionary進行初始化,並在後續訪問時返回相同的物件。

自版本 1.4 更改:InstanceState現在作為弱字典中的鍵,而不是例項本身。

classmethod as_mutable(sqltype: TypeEngine) → TypeEngine

將 SQL 型別與此可變 Python 型別關聯起來。

這將建立偵聽器,以檢測針對給定型別的 ORM 對映,並向這些對映新增變異事件跟蹤器。

型別無條件地作為例項返回,因此可以內聯使用as_mutable()

Table('mytable', metadata,
    Column('id', Integer, primary_key=True),
    Column('data', MyMutableType.as_mutable(PickleType))
)

請注意,返回的型別始終是一個例項,即使給定一個類,也只有明確宣告瞭該型別例項的列才會接收到額外的儀器裝置。

要將特定的可變型別與所有特定型別的所有出現相關聯,請使用特定Mutable子類的Mutable.associate_with()類方法來建立全域性關聯。

警告

此方法建立的偵聽器對所有對映器都是全域性的,並且會被垃圾回收。只能對應用程式中永久的型別使用as_mutable(),不要與臨時型別一起使用,否則這將導致記憶體使用量無限增長。

classmethod associate_with(sqltype: type) → None

將此包裝器與未來的給定型別的對映列相關聯。

這是一個方便的方法,會自動呼叫associate_with_attribute

警告

此方法建立的偵聽器對所有對映器都是全域性的,並且會被垃圾回收。只能對應用程式中永久的型別使用associate_with(),不要與臨時型別一起使用,否則這將導致記憶體使用量無限增長。

classmethod associate_with_attribute(attribute: InstrumentedAttribute[_O]) → None

將此型別建立為給定對映描述符的變異偵聽器。

method changed() → None

子類應該在發生變更事件時呼叫此方法。

classmethod coerce(key: str, value: Any) → Any | None

繼承自 MutableBase.coerce() 方法的 MutableBase

給定一個值,將其強制轉換為目標型別。

可以由自定義子類重寫以將傳入資料強制轉換為特定型別。

預設情況下,引發ValueError

根據父類是Mutable型別還是MutableComposite型別,在不同的情況下呼叫此方法。對於前者,在屬性設定操作和 ORM 載入操作期間都會呼叫它。對於後者,在屬性設定操作期間才會呼叫它;composite()構造的機制處理載入操作期間的強制轉換。

引數:

  • key – 正在設定的 ORM 對映屬性的字串名稱。

  • value – 輸入值。

返回:

如果無法完成轉換,則該方法應返回轉換後的值,或引發ValueError

class sqlalchemy.ext.mutable.MutableComposite

混入,定義了將 SQLAlchemy“組合”物件上的變更事件透明傳播到其擁有的父物件的機制。

檢視在組合上建立可變性中的示例以獲取用法資訊。

成員

changed()

類簽名

sqlalchemy.ext.mutable.MutableCompositesqlalchemy.ext.mutable.MutableBase

method changed() → None

子類應在更改事件發生時呼叫此方法。

class sqlalchemy.ext.mutable.MutableDict

一種實現了 Mutable 的字典型別。

MutableDict 物件實現了一個字典,當更改字典的內容時會向底層對映傳送更改事件,包括新增或刪除值時。

請注意,MutableDict 不會將可變跟蹤應用於字典內部的值本身。因此,它不足以解決跟蹤對遞迴字典結構進行深層更改的用例,例如 JSON 結構。要支援此用例,請構建 MutableDict 的子類,該子類提供適當的強制轉換,以便將放置在字典中的值也“可變”,並將事件傳送到其父結構。

另請參閱

MutableList

MutableSet

成員

clear(), coerce(), pop(), popitem(), setdefault(), update()

類簽名

sqlalchemy.ext.mutable.MutableDictsqlalchemy.ext.mutable.Mutablebuiltins.dicttyping.Generic

method clear() → None.  Remove all items from D.
classmethod coerce(key: str, value: Any) → MutableDict[_KT, _VT] | None

將普通字典轉換為此類的例項。

method pop(k[, d]) → v, remove specified key and return the corresponding value.

如果找不到鍵,則在給定預設值的情況下返回;否則,引發 KeyError。

method popitem() → Tuple[_KT, _VT]

移除並返回一個(鍵,值)對作為 2 元組。

以 LIFO(後進先出)順序返回鍵值對。如果字典為空,則引發 KeyError。

method setdefault(*arg)

如果字典中沒有鍵,則將鍵插入並將其值設定為預設值。

如果字典中存在鍵,則返回鍵的值,否則返回預設值。

method update([E, ]**F) → None.  Update D from dict/iterable E and F.

如果 E 存在且具有 .keys() 方法,則執行以下操作:for k in E: D[k] = E[k] 如果 E 存在但缺少 .keys() 方法,則執行以下操作:for k, v in E: D[k] = v 在任一情況下,接下來執行以下操作:for k in F: D[k] = F[k]

class sqlalchemy.ext.mutable.MutableList

一種實現了 Mutable 的列表型別。

MutableList物件實現了一個列表,在修改列表內容時會向底層對映發出更改事件,包括新增或刪除值時。

注意MutableList不會對列表內部的值本身應用可變跟蹤。因此,它不能解決跟蹤遞迴可變結構(例如 JSON 結構)的深層更改的用例。要支援此用例,構建MutableList的子類,提供適當的強制轉換以使放置在字典中的值也是“可變的”,並將事件傳播到其父結構。

另請參見

MutableDict

MutableSet

成員

append(), clear(), coerce(), extend(), insert(), is_iterable(), is_scalar(), pop(), remove(), reverse(), sort()

類簽名

sqlalchemy.ext.mutable.MutableListsqlalchemy.ext.mutable.Mutablebuiltins.listtyping.Generic

method append(x: _T) → None

將物件追加到列表末尾。

method clear() → None

從列表中刪除所有項。

classmethod coerce(key: str, value: MutableList[_T] | _T) → MutableList[_T] | None

將普通列表轉換為此類的例項。

method extend(x: Iterable[_T]) → None

透過從可迭代物件中追加元素來擴充套件列表。

method insert(i: SupportsIndex, x: _T) → None

在索引之前插入物件。

method is_iterable(value: _T | Iterable[_T]) → TypeGuard[Iterable[_T]]
method is_scalar(value: _T | Iterable[_T]) → TypeGuard[_T]
method pop(*arg: SupportsIndex) → _T

移除並返回索引處的項(預設為最後一個)。

如果列表為空或索引超出範圍,則引發 IndexError。

method remove(i: _T) → None

移除第一次出現的值。

如果值不存在,則引發 ValueError。

method reverse() → None

原地反轉。

method sort(**kw: Any) → None

對列表進行升序排序並返回 None。

排序是原地進行的(即修改列表本身)並且是穩定的(即保持兩個相等元素的順序)。

如果給定了鍵函式,則將其一次應用於每個列表項並根據其函式值升序或降序排序。

反轉標誌可以設定為按降序排序。

class sqlalchemy.ext.mutable.MutableSet

實現了Mutable的集合型別。

MutableSet 物件實現了一個集合,當集合的內容發生更改時,將向底層對映發出更改事件,包括新增或刪除值時。

請注意,MutableSet 不會對集合中值本身應用可變跟蹤。因此,它不是跟蹤遞迴可變結構的深層更改的足夠解決方案。為了支援這種用例,請構建一個MutableSet的子類,該子類提供適當的強制轉換,使放置在字典中的值也是“可變的”,並向其父結構發出事件。

另請參閱

MutableDict

MutableList

成員

add(), clear(), coerce(), difference_update(), discard(), intersection_update(), pop(), remove(), symmetric_difference_update(), update()

類簽名

sqlalchemy.ext.mutable.MutableSetsqlalchemy.ext.mutable.Mutablebuiltins.settyping.Generic

method add(elem: _T) → None

向集合新增一個元素。

如果元素已經存在,則不起作用。

method clear() → None

從此集合中移除所有元素。

classmethod coerce(index: str, value: Any) → MutableSet[_T] | None

將普通集合轉換為此類的例項。

method difference_update(*arg: Iterable[Any]) → None

從此集合中刪除另一個集合的所有元素。

method discard(elem: _T) → None

如果元素是成員,則從集合中刪除一個元素。

如果元素不是成員,則不執行任何操作。

method intersection_update(*arg: Iterable[Any]) → None

使用自身與另一個集合的交集更新集合。

method pop(*arg: Any) → _T

移除並返回一個任意的集合元素。如果集合為空,則引發 KeyError。

method remove(elem: _T) → None

從集合中刪除一個元素;它必須是成員。

如果元素不是成員,則引發 KeyError。

method symmetric_difference_update(*arg: Iterable[_T]) → None

使用自身與另一個集合的對稱差更新集合。

method update(*arg: Iterable[_T]) → None

使用自身與其他集合的並集更新集合。

在標量列值上建立可變性

“可變”結構的典型示例是 Python 字典。在 SQL 資料型別物件中介紹的示例中,我們從自定義型別開始,該型別在持久化之前將 Python 字典編組為 JSON 字串:

from sqlalchemy.types import TypeDecorator, VARCHAR
import json

class JSONEncodedDict(TypeDecorator):
    "Represents an immutable structure as a json-encoded string."

    impl = VARCHAR

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = json.dumps(value)
        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            value = json.loads(value)
        return value

使用json僅用於示例目的。sqlalchemy.ext.mutable 擴充套件可以與任何目標 Python 型別可能是可變的型別一起使用,包括PickleTypeARRAY等。

當使用sqlalchemy.ext.mutable 擴充套件時,值本身跟蹤所有引用它的父物件。下面,我們展示了一個簡單版本的MutableDict字典物件,它將Mutable mixin 應用於普通的 Python 字典:

from sqlalchemy.ext.mutable import Mutable

class MutableDict(Mutable, dict):
    @classmethod
    def coerce(cls, key, value):
        "Convert plain dictionaries to MutableDict."

        if not isinstance(value, MutableDict):
            if isinstance(value, dict):
                return MutableDict(value)

            # this call will raise ValueError
            return Mutable.coerce(key, value)
        else:
            return value

    def __setitem__(self, key, value):
        "Detect dictionary set events and emit change events."

        dict.__setitem__(self, key, value)
        self.changed()

    def __delitem__(self, key):
        "Detect dictionary del events and emit change events."

        dict.__delitem__(self, key)
        self.changed()

上述字典類採用了子類化 Python 內建的dict的方法,以產生一個 dict 子類,透過__setitem__路由所有的變異事件。這種方法還有變體,比如子類化UserDict.UserDictcollections.MutableMapping;對於這個示例很重要的部分是,每當對資料結構進行就地更改時,都會呼叫Mutable.changed()方法。

我們還重新定義了Mutable.coerce() 方法,用於將不是MutableDict例項的任何值轉換為適當的型別,比如json模組返回的普通字典。定義這個方法是可選的;我們也可以建立我們的JSONEncodedDict,使其始終返回MutableDict的例項,並確保所有呼叫程式碼都明確使用MutableDict。當未覆蓋Mutable.coerce()時,應用於父物件的任何不是可變型別例項的值將引發ValueError

我們的新MutableDict型別提供了一個類方法Mutable.as_mutable(),我們可以在列後設資料中使用它與型別關聯。這個方法獲取給定的型別物件或類,並關聯一個監聽器,將檢測到所有將來對映到該型別的對映,應用事件監聽儀器到對映的屬性。例如,使用經典表後設資料:

from sqlalchemy import Table, Column, Integer

my_data = Table('my_data', metadata,
    Column('id', Integer, primary_key=True),
    Column('data', MutableDict.as_mutable(JSONEncodedDict))
)

上面,Mutable.as_mutable() 返回一個JSONEncodedDict的例項(如果型別物件尚未是一個例項),它將攔截任何對映到該型別的屬性。下面我們建立一個簡單的對映到my_data表:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))

MyDataClass.data 成員現在將被通知其值的原地更改。

MyDataClass.data 成員的任何原地更改都將標記父物件的屬性為“髒”:

>>> from sqlalchemy.orm import Session

>>> sess = Session(some_engine)
>>> m1 = MyDataClass(data={'value1':'foo'})
>>> sess.add(m1)
>>> sess.commit()

>>> m1.data['value1'] = 'bar'
>>> assert m1 in sess.dirty
True

MutableDict 可以透過一個步驟與所有未來的 JSONEncodedDict 例項關聯,使用 Mutable.associate_with()。這類似於 Mutable.as_mutable(),但它將無條件攔截所有對映中 MutableDict 的所有出現,而無需單獨宣告它:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

MutableDict.associate_with(JSONEncodedDict)

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(JSONEncodedDict)

支援 Pickling

sqlalchemy.ext.mutable 擴充套件的關鍵在於在值物件上放置一個 weakref.WeakKeyDictionary,它儲存了父對映物件的對映,鍵為它們與該值相關聯的屬性名。 WeakKeyDictionary 物件不可 pickle,因為它們包含弱引用和函式回撥。在我們的情況下,這是一件好事,因為如果這個字典是可 pickle 的,它可能會導致獨立於父物件上下文的值物件的 pickle 大小過大。在這裡,開發者的責任僅僅是提供一個 __getstate__ 方法,從 pickle 流中排除 MutableBase._parents() 集合:

class MyMutableType(Mutable):
    def __getstate__(self):
        d = self.__dict__.copy()
        d.pop('_parents', None)
        return d

對於我們的字典示例,我們需要返回字典本身的內容(並在 setstate 中還原它們):

class MutableDict(Mutable, dict):
    # ....

    def __getstate__(self):
        return dict(self)

    def __setstate__(self, state):
        self.update(state)

如果我們的可變值物件被 pickle,而它附加到一個或多個也是 pickle 的父物件上,Mutable mixin 將在每個值物件上重新建立 Mutable._parents 集合,因為擁有父物件本身被 unpickle。

接收事件

當一個可變標量發出變更事件時,AttributeEvents.modified() 事件處理程式可以用於接收事件。當在可變擴充套件內部呼叫 flag_modified() 函式時,將呼叫此事件處理程式:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy import event

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))

@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance, initiator):
    print("json value modified:", instance.data)

支援 Pickling

sqlalchemy.ext.mutable 擴充套件的關鍵在於在值物件上放置一個 weakref.WeakKeyDictionary,該字典儲存父對映物件的對映,以屬性名稱為鍵,這些父對映物件與該值相關聯。由於 WeakKeyDictionary 物件包含弱引用和函式回撥,因此它們不可 picklable。在我們的情況下,這是一件好事,因為如果這個字典是可 pickle 的,那麼它可能會導致我們的值物件的 pickle 大小過大,這些值物件是在不涉及父物件的情況下 pickle 的。開發者在這裡的責任只是提供一個 __getstate__ 方法,該方法從 pickle 流中排除了 MutableBase._parents() 集合:

class MyMutableType(Mutable):
    def __getstate__(self):
        d = self.__dict__.copy()
        d.pop('_parents', None)
        return d

對於我們的字典示例,我們需要返回字典本身的內容(並在 setstate 中還原它們):

class MutableDict(Mutable, dict):
    # ....

    def __getstate__(self):
        return dict(self)

    def __setstate__(self, state):
        self.update(state)

在我們可變的值物件作為 pickle 物件時,如果它附加在一個或多個也是 pickle 的父物件上,Mutable mixin 將在每個值物件上重新建立 Mutable._parents 集合,因為擁有父物件的本身會被 unpickle。

接收事件

AttributeEvents.modified() 事件處理程式可用於在可變標量發出更改事件時接收事件。當在可變擴充套件中呼叫 flag_modified() 函式時,將呼叫此事件處理程式:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy import event

class Base(DeclarativeBase):
    pass

class MyDataClass(Base):
    __tablename__ = 'my_data'
    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))

@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance, initiator):
    print("json value modified:", instance.data)

確立組合物的可變性

組合物是 ORM 的一種特殊功能,它允許將單個標量屬性分配給一個物件值,該物件值表示從底層對映表中的一個或多個列中“組合”出的資訊。通常的例子是幾何“點”,並在 Composite Column Types 中介紹。

Mutable類似,使用者定義的複合類作為一個混合類繼承MutableComposite,透過MutableComposite.changed()方法檢測並傳遞更改事件給其父類。對於複合類,通常是透過使用特殊的 Python 方法__setattr__()來進行檢測。在下面的示例中,我們擴充套件了複合列型別中介紹的Point類,將MutableComposite包含在其基類中,並透過__setattr__將屬性設定事件路由到MutableComposite.changed()方法:

import dataclasses
from sqlalchemy.ext.mutable import MutableComposite

@dataclasses.dataclass
class Point(MutableComposite):
    x: int
    y: int

    def __setattr__(self, key, value):
        "Intercept set events"

        # set the attribute
        object.__setattr__(self, key, value)

        # alert all parents to the change
        self.changed()

MutableComposite類利用類對映事件自動為任何指定我們的Point型別的composite()的使用建立監聽器。下面,當Point對映到Vertex類時,將建立監聽器,這些監聽器將把Point物件的更改事件路由到Vertex.startVertex.end屬性中的每一個:

from sqlalchemy.orm import DeclarativeBase, Mapped
from sqlalchemy.orm import composite, mapped_column

class Base(DeclarativeBase):
    pass

class Vertex(Base):
    __tablename__ = "vertices"

    id: Mapped[int] = mapped_column(primary_key=True)

    start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1"))
    end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2"))

    def __repr__(self):
        return f"Vertex(start={self.start}, end={self.end})"

Vertex.startVertex.end成員的任何就地更改都會在父物件上標記屬性為“髒”:

>>> from sqlalchemy.orm import Session
>>> sess = Session(engine)
>>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
>>> sess.add(v1)
sql>>> sess.flush()
BEGIN  (implicit)
INSERT  INTO  vertices  (x1,  y1,  x2,  y2)  VALUES  (?,  ?,  ?,  ?)
[...]  (3,  4,  12,  15)
>>> v1.end.x = 8
>>> assert v1 in sess.dirty
True
sql>>> sess.commit()
UPDATE  vertices  SET  x2=?  WHERE  vertices.id  =  ?
[...]  (8,  1)
COMMIT 

強制可變複合型別

在複合型別上也支援MutableBase.coerce()方法。對於MutableCompositeMutableBase.coerce()方法僅在屬性設定操作中呼叫,而不是在載入操作中呼叫。覆蓋MutableBase.coerce()方法基本上等同於為使用自定義複合型別的所有屬性使用validates()驗證程式:

@dataclasses.dataclass
class Point(MutableComposite):
    # other Point methods
    # ...

    def coerce(cls, key, value):
        if isinstance(value, tuple):
            value = Point(*value)
        elif not isinstance(value, Point):
            raise ValueError("tuple or Point expected")
        return value

支援 Pickling

Mutable類似,MutableComposite輔助類使用了透過MutableBase._parents()屬性獲得的weakref.WeakKeyDictionary,該字典不可 pickle。如果我們需要 pickle Point 或其擁有類 Vertex 的例項,至少需要定義一個不包含 _parents 字典的__getstate__。下面我們定義了Point類的最小形式的__getstate____setstate__

@dataclasses.dataclass
class Point(MutableComposite):
    # ...

    def __getstate__(self):
        return self.x, self.y

    def __setstate__(self, state):
        self.x, self.y = state

Mutable類似,MutableComposite增強了父物件的物件關係狀態的 pickling 過程,以便將MutableBase._parents()集合還原為所有Point物件。

強制轉換可變複合型別

MutableBase.coerce()方法也支援複合型別。在MutableComposite的情況下,MutableBase.coerce()方法僅在屬性設定操作而非載入操作時呼叫。覆蓋MutableBase.coerce()方法基本上等同於對使用自定義複合型別的所有屬性使用validates()驗證程式:

@dataclasses.dataclass
class Point(MutableComposite):
    # other Point methods
    # ...

    def coerce(cls, key, value):
        if isinstance(value, tuple):
            value = Point(*value)
        elif not isinstance(value, Point):
            raise ValueError("tuple or Point expected")
        return value

支援 Pickling

Mutable類似,MutableComposite輔助類使用了透過MutableBase._parents()屬性獲得的weakref.WeakKeyDictionary,該字典不可 pickle。如果我們需要 pickle Point 或其擁有類 Vertex 的例項,至少需要定義一個不包含 _parents 字典的__getstate__。下面我們定義了Point類的最小形式的__getstate____setstate__

@dataclasses.dataclass
class Point(MutableComposite):
    # ...

    def __getstate__(self):
        return self.x, self.y

    def __setstate__(self, state):
        self.x, self.y = state

Mutable一樣,MutableComposite增強了父物件的物件關係狀態的 pickling 過程,以便將MutableBase._parents()集合還原為所有Point物件。

API 參考

物件名稱 描述
Mutable 定義更改事件透明傳播到父物件的混合類。
MutableBase MutableMutableComposite的通用基類。
MutableComposite 定義 SQLAlchemy “composite” 物件上的更改事件透明傳播到其擁有的父物件或父物件的混合類。
MutableDict 一個實現Mutable的字典型別。
MutableList 一個實現Mutable的列表型別。
MutableSet 一個實現Mutable的集合型別。
class sqlalchemy.ext.mutable.MutableBase

成員

_parents, coerce()

MutableMutableComposite的通用基類。

attribute _parents

父物件的InstanceState字典->父物件上的屬性名稱。

此屬性是所謂的“記憶化”屬性。第一次訪問時,它會用一個新的weakref.WeakKeyDictionary初始化自己,並在後續訪問時返回相同的物件。

在 1.4 版中更改:InstanceState現在被用作弱字典中的鍵,而不是例項本身。

classmethod coerce(key: str, value: Any) → Any | None

給定一個值,將其強制轉換為目標型別。

可以被自定義子類覆蓋,將傳入資料強制轉換為特定型別。

預設情況下,引發ValueError

根據父類是Mutable型別還是MutableComposite型別,在不同的情況下呼叫此方法。對於前者,它在屬性集操作和 ORM 載入操作期間都會被呼叫。對於後者,它僅在屬性集操作期間被呼叫;composite()構造的機制在載入操作期間處理強制轉換。

引數:

  • key – 正在設定的 ORM 對映屬性的字串名稱。

  • value – 傳入的值。

返回:

如果無法完成強制轉換,該方法應返回強制轉換後的值,或引發ValueError

class sqlalchemy.ext.mutable.Mutable

定義將更改事件透明傳播到父物件的混合類。

檢視在標量列值上建立可變性中的示例以獲取用法資訊。

成員

_get_listen_keys(), _listen_on_attribute(), _parents, as_mutable(), associate_with(), associate_with_attribute(), changed(), coerce()

類簽名

sqlalchemy.ext.mutable.Mutablesqlalchemy.ext.mutable.MutableBase

classmethod _get_listen_keys(attribute: QueryableAttribute[Any]) → Set[str]

繼承自 MutableBase sqlalchemy.ext.mutable.MutableBase._get_listen_keys 方法

給定一個描述符屬性,返回指示此屬性狀態變化的屬性鍵的set()

通常只是set([attribute.key]),但可以被覆蓋以提供額外的鍵。例如,MutableComposite 會用包含組合值的列相關聯的屬性鍵來增加這個集合。

在攔截InstanceEvents.refresh()InstanceEvents.refresh_flush()事件時,會查詢此集合,這些事件會傳遞一個已重新整理的屬性名稱列表;該列表將與此集合進行比較,以確定是否需要採取行動。

classmethod _listen_on_attribute(attribute: QueryableAttribute[Any], coerce: bool, parent_cls: _ExternalEntityType[Any]) → None

繼承自 MutableBasesqlalchemy.ext.mutable.MutableBase._listen_on_attribute 方法

將此型別作為給定對映描述符的變異監聽器。

attribute _parents

繼承自 MutableBasesqlalchemy.ext.mutable.MutableBase._parents 屬性

父物件的InstanceState->父物件上的屬性名稱的字典。

此屬性是所謂的“記憶化”屬性。它在首次訪問時使用一個新的weakref.WeakKeyDictionary進行初始化,並在後續訪問時返回相同的物件。

在 1.4 版本中更改:現在使用InstanceState作為弱字典中的鍵,而不是例項本身。

classmethod as_mutable(sqltype: TypeEngine) → TypeEngine

將 SQL 型別與此可變 Python 型別關聯。

這將建立監聽器,用於檢測針對給定型別的 ORM 對映,向這些對映新增變異事件跟蹤器。

該型別無條件地作為一個例項返回,以便可以內聯使用as_mutable()

Table('mytable', metadata,
    Column('id', Integer, primary_key=True),
    Column('data', MyMutableType.as_mutable(PickleType))
)

請注意,返回的型別始終是一個例項,即使給定一個類,也只有明確宣告瞭該型別例項的列才會接收到額外的儀器化。

要將特定的可變型別與特定型別的所有出現關聯起來,請使用Mutable.associate_with()類方法的特定Mutable子類來建立全域性關聯。

警告

此方法建立的監聽器是全域性的,適用於所有對映器,並且會被垃圾回收。只能對應用程式中永久的型別使用as_mutable(),而不是臨時型別,否則會導致記憶體使用量無限增長。

classmethod associate_with(sqltype: type) → None

將此包裝器與將來的給定型別的對映列關聯起來。

這是一個方便的方法,會自動呼叫associate_with_attribute

警告

該方法建立的監聽器是全域性的,適用於所有對映器,並且會被垃圾回收。只能對應用程式中永久的型別使用associate_with(),而不是臨時型別,否則會導致記憶體使用量無限增長。

classmethod associate_with_attribute(attribute: InstrumentedAttribute[_O]) → None

將此型別作為給定對映描述符的變異監聽器。

method changed() → None

子類在發生更改事件時應呼叫此方法。

classmethod coerce(key: str, value: Any) → Any | None

繼承自 MutableBase.coerce() 方法MutableBase

給定一個值,將其強制轉換為目標型別。

可以被自定義子類重寫以將傳入的資料強制轉換為特定型別。

預設情況下,引發 ValueError

此方法在不同的情況下被呼叫,具體取決於父類是 Mutable 型別還是 MutableComposite 型別。在前者的情況下,它將在屬性設定操作以及 ORM 載入操作期間被呼叫。對於後者,它僅在屬性設定操作期間被呼叫;composite() 構造的機制在載入操作期間處理強制轉換。

引數:

  • key – 被設定的 ORM 對映屬性的字串名稱。

  • value – 傳入的值。

返回:

如果無法完成強制轉換,則該方法應返回強制轉換後的值,或引發 ValueError

class sqlalchemy.ext.mutable.MutableComposite

定義了對 SQLAlchemy “組合”物件的更改事件的透明傳播的混合類到其擁有的父物件。

請參閱 在組合上建立可變性 中的示例以獲取用法資訊。

成員

changed()

類簽名

class sqlalchemy.ext.mutable.MutableComposite (sqlalchemy.ext.mutable.MutableBase)

method changed() → None

子類應在更改事件發生時呼叫此方法。

class sqlalchemy.ext.mutable.MutableDict

實現了 Mutable 的字典型別。

MutableDict 物件實現了一個字典,在字典內容發生更改時將向基礎對映發出更改事件,包括新增或移除值時。

請注意,MutableDict 不會 對字典內部的值本身應用可變跟蹤。因此,它不足以解決跟蹤遞迴字典結構(例如 JSON 結構)的深層更改的用例。要支援此用例,請構建一個 MutableDict 的子類,以提供適當的強制轉換,以便放置在字典中的值也是“可變的”,並將事件傳播到其父結構。

另請參見

MutableList

MutableSet

成員

clear(), coerce(), pop(), popitem(), setdefault(), update()

類簽名

sqlalchemy.ext.mutable.MutableDict (sqlalchemy.ext.mutable.Mutable, builtins.dict, typing.Generic)

method clear() → None.  Remove all items from D.
classmethod coerce(key: str, value: Any) → MutableDict[_KT, _VT] | None

將普通字典轉換為此類的例項。

method pop(k[, d]) → v, remove specified key and return the corresponding value.

如果找不到鍵,則返回預設值(如果給定);否則,引發 KeyError。

method popitem() → Tuple[_KT, _VT]

移除並返回一個(key, value)對作為 2 元組。

鍵值對以 LIFO(後進先出)順序返回。如果字典為空,則引發 KeyError。

method setdefault(*arg)

如果鍵不在字典中,則將鍵插入並設定預設值。

如果鍵在字典中,則返回鍵的值,否則返回預設值。

method update([E, ]**F) → None.  Update D from dict/iterable E and F.

如果 E 存在並且具有.keys()方法,則執行: for k in E: D[k] = E[k] 如果 E 存在但缺少.keys()方法,則執行: for k, v in E: D[k] = v 在任一情況下,接下來執行: for k in F: D[k] = F[k]

class sqlalchemy.ext.mutable.MutableList

一個實現了Mutable的列表型別。

MutableList 物件實現了一個列表,當列表的內容被更改時,包括新增或刪除值時,將向底層對映傳送更改事件。

請注意,MutableList 不會對列表內部的值本身應用可變跟蹤。因此,它不是跟蹤對遞迴可變結構進行深層更改的使用案例的充分解決方案,例如 JSON 結構。為支援此使用案例,請構建MutableList的子類,該子類提供適當的強制轉換以使放置在字典中的值也是“可變的”,並將事件傳送到其父結構。

另請參閱

MutableDict

MutableSet

成員

append(), clear(), coerce(), extend(), insert(), is_iterable(), is_scalar(), pop(), remove(), reverse(), sort()

類簽名

sqlalchemy.ext.mutable.MutableListsqlalchemy.ext.mutable.Mutablebuiltins.listtyping.Generic

method append(x: _T) → None

將物件追加到列表末尾。

method clear() → None

從列表中刪除所有項。

classmethod coerce(key: str, value: MutableList[_T] | _T) → MutableList[_T] | None

將普通列表轉換為此類的例項。

method extend(x: Iterable[_T]) → None

透過將來自可迭代物件的元素附加到列表來擴充套件列表。

method insert(i: SupportsIndex, x: _T) → None

在索引之前插入物件。

method is_iterable(value: _T | Iterable[_T]) → TypeGuard[Iterable[_T]]
method is_scalar(value: _T | Iterable[_T]) → TypeGuard[_T]
method pop(*arg: SupportsIndex) → _T

刪除並返回索引處的項(預設為最後一個)。

如果列表為空或索引超出範圍,則引發 IndexError。

method remove(i: _T) → None

刪除值的第一個出現。

如果值不存在,則引發 ValueError。

method reverse() → None

就地反轉。

method sort(**kw: Any) → None

將列表按升序排序並返回 None。

排序是原地進行的(即列表本身被修改)並且穩定的(即保持兩個相等元素的順序不變)。

如果給定了鍵函式,則將其應用於每個列表項一次,並根據其函式值按升序或降序對它們進行排序。

反轉標誌可以設定為按降序排序。

class sqlalchemy.ext.mutable.MutableSet

實現了Mutable的集合型別。

MutableSet 物件實現了一個集合,當集合的內容發生變化時,包括新增或移除值時,會向底層對映傳送更改事件。

注意,MutableSet 不會對集合內部的值本身應用可變跟蹤。因此,它不是跟蹤對遞迴可變結構進行深層更改的足夠解決方案。為了支援這種用例,構建一個MutableSet的子類,提供適當的強制轉換,以便放置在字典中的值也是“可變的”,並向它們的父結構發出事件。

另請參閱

MutableDict

MutableList

成員

add(), clear(), coerce(), difference_update(), discard(), intersection_update(), pop(), remove(), symmetric_difference_update(), update()

類簽名

sqlalchemy.ext.mutable.MutableSetsqlalchemy.ext.mutable.Mutable, builtins.set, typing.Generic)

method add(elem: _T) → None

向集合新增一個元素。

如果元素已經存在,則不產生任何效果。

method clear() → None

從此集合中移除所有元素。

classmethod coerce(index: str, value: Any) → MutableSet[_T] | None

將普通集合轉換為此類的例項。

method difference_update(*arg: Iterable[Any]) → None

從此集合中移除另一個集合的所有元素。

method discard(elem: _T) → None

如果元素是集合的成員,則從集合中移除一個元素。

如果元素不是成員,則不執行任何操作。

method intersection_update(*arg: Iterable[Any]) → None

使用自身和另一個集合的交集更新集合。

method pop(*arg: Any) → _T

移除並返回任意集合元素。如果集合為空,則引發 KeyError。

method remove(elem: _T) → None

從集合中移除一個元素;它必須是成員。

如果元素不是成員,則引發 KeyError。

method symmetric_difference_update(*arg: Iterable[_T]) → None

使用自身和另一個集合的對稱差集更新集合。

method update(*arg: Iterable[_T]) → None

使用自身和其他集合的並集更新集合。

相關文章