Additional Persistence Techniques

Embedding SQL Insert/Update Expressions into a Flush

この機能を使用すると、データベース列の値をリテラル値ではなくSQL式に設定できます。これは特に、アトミックな更新やストアドプロシージャの呼び出しなどに便利です。属性に式を代入するだけです:

class SomeClass(Base):
    __tablename__ = "some_table"

    # ...

    value = mapped_column(Integer)

someobject = session.get(SomeClass, 5)

# set 'value' attribute to a SQL expression adding one
someobject.value = SomeClass.value + 1

# issues "UPDATE some_table SET value=value+1"
session.commit()

このテクニックはINSERT文とUPDATE文の両方に有効です。flush/commit操作の後、上記の someobjectvalue 属性は期限切れになり、次にアクセスされたときに、新しく生成された値がデータベースからロードされます。

この機能には、主キー列と連携して動作する条件付きサポートもあります。RETURNINGサポートを持つバックエンドでは、SQL式を主キー列に割り当てることもできます。これにより、SQL式を評価することも、INSERT時に主キー値を変更するサーバー側のトリガをオブジェクトの主キーの一部としてORMによって正常に取得することもできます。:

class Foo(Base):
    __tablename__ = "foo"
    pk = mapped_column(Integer, primary_key=True)
    bar = mapped_column(Integer)

e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)

session = Session(e)

foo = Foo(pk=sql.select(sql.func.coalesce(sql.func.max(Foo.pk) + 1, 1)))
session.add(foo)
session.commit()

PostgreSQLでは、上記の Session は以下のINSERTを発行します。:

INSERT INTO foo (foopk, bar) VALUES
((SELECT coalesce(max(foo.foopk) + %(max_1)s, %(coalesce_2)s) AS coalesce_1
FROM foo), %(bar)s) RETURNING foo.foopk

New in version 1.3: .. SQL expressions can now be passed to a primary key column during an ORM flush; if the database supports of the primary key attribute.

データベースがプライマリ・キー属性をサポートしている場合、ORMフラッシュ時にSQL式をプライマリ・キー列に渡せるようになりました。

Using SQL Expressions with Sessions

SQL式と文字列は、そのトランザクションコンテキスト内で Session を介して実行することができます。これは Session.execute() メソッドを使って最も簡単に実現できます。このメソッドは CursorResultEngineConnection と同じように返します:

Session = sessionmaker(bind=engine)
session = Session()

# execute a string statement
result = session.execute(text("select * from table where id=:id"), {"id": 7})

# execute a SQL expression construct
result = session.execute(select(mytable).where(mytable.c.id == 7))

Session が保持している現在の Connection は、 Session.connection() メソッドを使ってアクセスできます:

connection = session.connection()

上記の例では、単一の Engine または Connection にバインドされた Session を扱っています。複数のエンジンにバインドされているか、まったくバインドされていない(つまり、バインドされたメタデータに依存している) Session を使用して文を実行するには、 Session.execute()Session.connection() の両方がバインド引数の辞書を受け入れます。 Session.execute.bind_arguments には、マップされたクラスまたは Mapper インスタンスに渡される”mapper”を含めることができます。これは、目的のエンジンの適切なコンテキストを見つけるために使用されます:

Session = sessionmaker()
session = Session()

# need to specify mapper or class when executing
result = session.execute(
    text("select * from table where id=:id"),
    {"id": 7},
    bind_arguments={"mapper": MyMappedClass},
)

result = session.execute(
    select(mytable).where(mytable.c.id == 7), bind_arguments={"mapper": MyMappedClass}
)

connection = session.connection(MyMappedClass)

Changed in version 1.4: Session.execute() への mapper 引数と clause 引数が Session.execute.bind_arguments パラメータとして送られる辞書の一部として渡されるようになりました。以前の引数はまだ受け入れられますが、この使用法は廃止されました。

Forcing NULL on a column with a default

ORMは、オブジェクトに設定されていない属性を”デフォルト”と見なします。この属性はINSERT文から省略されます。:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True)

obj = MyObject(id=1)
session.add(obj)
session.commit()  # INSERT with the 'data' column omitted; the database
# itself will persist this as the NULL value

INSERT文で列を省略すると、その列にNULL値が設定されることになります。ただし、列にデフォルトが設定されている場合は、デフォルト値が保持されます。これは、純粋なSQLの観点から見ると、サーバ側のデフォルトの場合にも、クライアント側とサーバ側のデフォルトの場合のSQLAlchemyの挿入動作の場合にも当てはまります。:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True, server_default="default")

obj = MyObject(id=1)
session.add(obj)
session.commit()  # INSERT with the 'data' column omitted; the database
# itself will persist this as the value 'default'

しかしORMでは、Pythonの値 None を明示的にオブジェクトに代入しても、値が代入されなかったかのように 同じ として扱われます:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True, server_default="default")

obj = MyObject(id=1, data=None)
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set to None;
# the ORM still omits it from the statement and the
# database will still persist this as the value 'default'

上記の操作は、たとえ None が渡されたとしても、 data 列にはSQL NULLではなく、サーバのデフォルト値である default が保持されます。これは、多くのアプリケーションが想定しているORMの長年の動作です。

では、列にデフォルト値があっても、この列に実際にNULLを入れたい場合はどうすればよいでしょうか?2つの方法があります。1つは、インスタンスごとのレベルで、 null SQL構文を使って属性を割り当てる方法です:

from sqlalchemy import null

obj = MyObject(id=1, data=null())
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set as null();
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value

null SQL構文は、常にターゲットのINSERT文に直接存在するSQL NULL値に変換されます。

Pythonの値 None を使用し、列のデフォルトが存在してもこれをNULLとして保持したい場合は、コアレベルの修飾子 TypeEngine.evaluates_none() を使用してORMにこれを設定できます。これは、ORMが値 None を”欠落した”値として省略するのではなく、他の値と同じように処理して通過させる型を示します:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(
        String(50).evaluates_none(),  # indicate that None should always be passed
        nullable=True,
        server_default="default",
    )

obj = MyObject(id=1, data=None)
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set to None;
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value

Fetching Server-Generated Defaults

Server-invoked DDL-Explicit Default ExpressionsMarking Implicitly Generated Values, timestamps, and Triggered Columns で紹介されているように、Coreは、データベース自身がINSERT時に値を生成し、あまり一般的ではありませんが、UPDATE文時に値を生成するデータベース列の概念をサポートしています。ORMは、フラッシュ時にこれらの新しく生成された値をフェッチできることに関して、そのような列をサポートしています。この動作は、サーバによって生成される主キー列の場合に必要です。なぜなら、ORMはオブジェクトが永続化された後にその主キーを知る必要があるからです。

ほとんどの場合、データベースによって自動的に生成された値を持つ主キー列は単純な整数列であり、いわゆる「オートインクリメント」列として、または列に関連付けられたシーケンスからデータベースによって実装されます。SQLAlchemy Core内のすべてのデータベース言語は、これらの主キー値を取得するメソッドをサポートしています。これは多くの場合、Python DBAPIにネイティブであり、一般にこのプロセスは自動的に行われます。これに関する詳細なドキュメントは Column.autoincrement にあります。

サーバが生成した列がプライマリ・キー列でない場合、または単純なオートインクリメント整数列でない場合、ORMはこれらの列に適切な server_default ディレクティブを付けて、ORMがこの値を取得できるようにする必要があります。ただし、すべてのメソッドがすべてのバックエンドでサポートされているわけではないので、適切なメソッドを使用するように注意する必要があります。回答すべき2つの質問は、1. この列がプライマリ・キーの一部であるかどうか、2. データベースがRETURNINGまたは”OUTPUT inserted”などの同等のものをサポートしているか、です。これらは、INSERT文またはUPDATE文が呼び出されると同時にサーバが生成した値を返すSQL句です。RETURNINGは現在、PostgreSQL、Oracle、MariaDB 10.5、SQLite 3.35、およびSQL Serverでサポートされています。

Case 1: non primary key, RETURNING or equivalent is supported

この場合、列は FetchedValue として、または明示的な Column.server_default でマークされる必要があります。ORMは、INSERT文を実行するときにこれらの列を自動的にRETURNING句に追加します。ただし、RETURNINGと insertmanyvalues の両方をサポートするダイアレクトでは、 Mapper.eager_defaults パラメータが True に設定されているか、デフォルトの設定である "auto" のままであると仮定します:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)

    # server-side SQL date function generates a new timestamp
    timestamp = mapped_column(DateTime(), server_default=func.now())

    # some other server-side function not named here, such as a trigger,
    # populates a value into this column during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

    # set eager defaults to True.  This is usually optional, as if the
    # backend supports RETURNING + insertmanyvalues, eager defaults
    # will take place regardless on INSERT
    __mapper_args__ = {"eager_defaults": True}

上記の例では、クライアント側から”timestamp”または”special_identifier”の値を明示的に指定しないINSERT文は、RETURNING句内に”timestamp”および”special_identifier”列を含むため、これらはすぐに使用可能になります。PostgreSQLデータベースでは、上記のテーブルに対するINSERTは以下のようになります。

INSERT INTO my_table DEFAULT VALUES RETURNING my_table.id, my_table.timestamp, my_table.special_identifier

Changed in version 2.0.0rc1: Mapper.eager_defaults パラメータのデフォルトが新しい設定である "auto" になりました。これは、配下のデータベースがRETURNINGと insertmanyvalues の両方をサポートしている場合、INSERT時にサーバが生成したデフォルト値を取得するために自動的にRETURNINGを使用します。

Note

Mapper.eager_defaults"auto" 値はINSERT文にのみ適用されます。UPDATE文は、たとえ使用可能であっても、 Mapper.eager_defaultsTrue に設定されていない限り、RETURNINGを使用しません。これは、UPDATEには同等の「insertmanyvalues」機能がないためです。そのため、UPDATE RETURNINGは、更新される各行に対して個別にUPDATE文を生成する必要があります。

Case 2: Table includes trigger-generated values which are not compatible with RETURNING

Mapper.eager_defaults"auto" 設定は、RETURNINGをサポートするバックエンドが、新しく生成されたデフォルト値を取得するために、通常INSERT文でRETURNINGを使用することを意味します。しかし、トリガを使用して生成されるサーバ生成値には制限があるため、RETURNINGは使用できません。

  • SQL Serverでは、トリガによって生成された値を取得するためにINSERT文でRETURNINGを使用することはできません。この文は失敗します。

  • SQLiteでは、RETURNINGとトリガを組み合わせて使用することには制限があります。たとえば、RETURNING句には使用可能なINSERTed値がありません。

  • 他のバックエンドでは、トリガや他の種類のサーバ生成値と組み合わせたRETURNINGに制限がある場合があります。

サーバが生成したデフォルト値だけでなく、ORMが特定のテーブルに対してRETURNINGを使用しないようにするためにも、このような値に対するRETURNINGの使用を無効にするには、マップされた Table に対して Table.implicit_returningFalse として指定します。宣言型マッピングを使用すると、次のようになります:

class MyModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

    # assume a database trigger populates a value into this column
    # during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

    # disable all use of RETURNING for the table
    __table_args__ = {"implicit_returning": False}

pyodbcドライバを使用するSQL Serverでは、上記のテーブルに対するINSERTはRETURNINGを使用せず、SQL Serverの scope_identity() 関数を使用して新しく生成されたプライマリ・キー値を取得します。

INSERT INTO my_table (data) VALUES (?); select scope_identity()

See also

INSERT behavior - 新しく生成されたプライマリキー値を取得するSQL Serverダイアレクトのメソッドの背景

Case 3: non primary key, RETURNING or equivalent is not supported or not needed

この場合は上記のケース1と同じですが、通常は Mapper.eager_defaults を使用しないようにします。これは、RETURNINGサポートがない場合の現在の実装では、行ごとにSE LE CTを出力するため、パフォーマンスが低下するためです。したがって、このパラメータは以下のマッピングでは省略されています。:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    timestamp = mapped_column(DateTime(), server_default=func.now())

    # assume a database trigger populates a value into this column
    # during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

上記のマッピングを持つレコードが、RETURNINGまたは”insertmanyvalues”サポートを含まないバックエンドに挿入された後、”timestamp”列と”special_identifier”列は空のままになり、フラッシュ後に最初にアクセスされたときに2番目のSELECT文を介してフェッチされます。たとえば、”expired”とマークされます。

Mapper.eager_defaults に明示的に True の値が指定されていて、バックエンドデータベースがRETURNINGまたは同等のものをサポートしていない場合、ORMは新しく生成された値を取得するためにINSERT文の直後にSELECT文を発行します。現在、ORMには、RETURNINGが使用できない場合に、新しく挿入された多くの行をバッチでSELECTする機能はありません。これは、フラッシュプロセスに必要のない追加のSELECT文を追加するため、通常は望ましくありません。上記のマッピングを使用し、(MariaDBではなく)MySQLに対して Mapper.eager_defaults フラグをTrueに設定すると、フラッシュ時に次のようなSQLが生成されます。

INSERT INTO my_table () VALUES ()

-- when eager_defaults **is** used, but RETURNING is not supported
SELECT my_table.timestamp AS my_table_timestamp, my_table.special_identifier AS my_table_special_identifier
FROM my_table WHERE my_table.id = %s

SQLAlchemyの将来のリリースでは、単一のSELECT文の中で多数のローをバッチ処理するRETURNINGを使用して、Eager Defaultsの効率を改善する可能性があります。

Case 4: primary key, RETURNING or equivalent is supported

サーバが生成した値を持つプライマリ・キー・カラムは、INSERTの直後にフェッチする必要があります。ORMはプライマリ・キー値を持つローにしかアクセスできないため、プライマリ・キーがサーバによって生成された場合、ORMはINSERTの直後に新しい値を取得する手段を必要とします。

前述したように、整数の”オートインクリメント”列や、 Identity でマークされた列、PostgreSQLのSERIALのような特殊な構成体の場合、これらの型はコアによって自動的に処理されます。データベースには、RETURNINGがサポートされていない”最後に挿入されたid”を取得する関数が含まれており、RETURNINGがサポートされている場合、SQLAlchemyはそれを使用します。

例えば、 Identity とマークされた列を持つOracleを使用すると、RETURNINGが自動的に新しい主キー値をフェッチするために使用されます:

class MyOracleModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(Identity(), primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

The INSERT for a model as above on Oracle looks like:

Oracle上での上記モデルのINSERTは次のようになります。

INSERT INTO my_table (data) VALUES (:data) RETURNING my_table.id INTO :ret_0

SQLAlchemyは”data”フィールドに対してINSERTを行いますが、RETURNING句には”id”のみを含めます。これにより、サーバ側で「id」が生成され、新しい値が即座に返されます。

サーバー側の関数またはトリガによって生成された整数以外の値の場合、および明示的なシーケンスやトリガを含むテーブル自体の外部の構成から生成された整数値の場合、サーバのデフォルト生成はテーブルのメタデータ内でマークされる必要があります。もう一度Oracleを例として使用すると、 Sequence 構成体を使用して明示的なシーケンスに名前を付けて、上記と同様のテーブルを説明できます:

class MyOracleModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(Sequence("my_oracle_seq"), primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

Oracle上のこのバージョンのモデルのINSERTは次のようになります。:

INSERT INTO my_table (id, data) VALUES (my_oracle_seq.nextval, :data) RETURNING my_table.id INTO :ret_0

上記の場合、SQLAlchemyは新しいプライマリ・キーの生成に使用されるようにプライマリ・キー・カラムに対して my_sequence.nextval をレンダリングし、RETURNINGを使用して新しい値をすぐに返します。

新しい値を生成するトリガやデータベース固有のデータ型を使用する場合など、データのソースが単純なSQL関数や Sequence で表現されない場合、値を生成するデフォルトの存在は、列定義内で FetchedValue を使用して示すことができます。以下は、SQL ServerのTIMESTAMP列を主キーとして使用するモデルです。SQL Serverでは、このデータ型は新しい値を自動的に生成します。したがって、これは、 Column.server_default パラメータに FetchedValue を指定することによって、テーブルメタデータ内で示されます。:

class MySQLServerModel(Base):
    __tablename__ = "my_table"

    timestamp: Mapped[datetime.datetime] = mapped_column(
        TIMESTAMP(), server_default=FetchedValue(), primary_key=True
    )
    data: Mapped[str] = mapped_column(String(50))

SQL Serverでの上記のテーブルのINSERTは、次のようになります。:

INSERT INTO my_table (data) OUTPUT inserted.timestamp VALUES (?)

Case 5: primary key, RETURNING or equivalent is not supported

この領域では、デフォルトを生成する何らかの手段がサーバ上で実行されているが、データベースの通常のオートインクリメント・ルーチンの外部にあるMySQLなどのデータベースのローを生成します。この場合、SQLAlchemyがデフォルトを「事前実行」できることを確認する必要があります。つまり、明示的なSQL式である必要があります。

Note

このセクションでは、MySQLの日時値を含む複数のレシピについて説明します。これは、このバックエンドの日時データ型には、説明するのに役立つ特異な要件が追加されているためです。ただし、MySQLでは、通常の単一列の自動インクリメント整数値以外に、主キーとして使用される*任意の*自動生成データ型に対して、明示的な「事前実行された」デフォルトジェネレータが必要であることに注意してください。

MySQL with DateTime primary key

MySQLの DateTime 列の例では、”NOW()”SQL関数を使って、明示的にpre-executeでサポートされるデフォルトを追加しています:

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)

上記では、”NOW()”関数を選択して日時の値を列に渡します。上記で生成されるSQLは次のとおりです。:

SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)

MySQL with TIMESTAMP primary key

MySQLで TIMESTAMP データ型を使用する場合、MySQLは通常、このデータ型にサーバ側のデフォルトを自動的に関連付けます。しかし、主キーとして使用する場合、Coreは関数を実行しない限り、新しく生成された値を取得できません。MySQLの TIMESTAMP は実際にバイナリ値を格納しているので、列に永続化できるバイナリ値を取得できるように、”NOW()”の使用法に追加の”CAST”を追加する必要があります:

from sqlalchemy import cast, Binary

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = mapped_column(
        TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
    )

上記では、”NOW()”関数を選択することに加えて、戻り値がバイナリになるように、 cast() と組み合わせて Binary データ型を追加で使用しています。INSERT内で上記からレンダリングされたSQLは次のようになります。:

SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)

Notes on eagerly fetching client invoked SQL expressions used for INSERT or UPDATE

上記の例は、 Column.server_default を使用して、DDL内にデフォルト生成関数を含むテーブルを作成する方法を示しています。

SQLAlchemyは、 Client-Invoked SQL Expressions で文書化されているように、DDL以外のサーバ側のデフォルトもサポートしています。これらの”クライアントが呼び出すSQL式”は、 Column.default および Column.onupdate パラメータを使用して設定されます。

これらのSQL式は現在、ORM内で真のサーバ側のデフォルトと同じ制限を受けます。 Mapper.eager_defaults"auto" または True に設定されている場合、 FetchedValue ディレクティブが Column に関連付けられていない限り、これらの式はDDLサーバのデフォルトではなく、SQLAlchemy自身によってアクティブにレンダリングされますが、RETURNINGでは積極的にフェッチされません。この制限は将来のSQLAlchemyリリースで対処される可能性があります。

FetchedValue 構文は Column.server_default または Column.server_onupdate に適用することができ、同時にSQL式が Column.default および Column.onupdate と共に使用されます。例えば、以下の例では、クライアントが呼び出すSQL式として、 Column.default および Column.onupdate に対して、 func.now() 構文が使用されています。 Mapper.eager_defaults の動作に、利用可能な場合にRETURNINGを使用してこれらの値を取得することを含めるために、 Column.server_default および Column.server_onupdateFetchedValue と共に使用して、取得が確実に行われるようにします:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)

    created = mapped_column(
        DateTime(), default=func.now(), server_default=FetchedValue()
    )
    updated = mapped_column(
        DateTime(),
        onupdate=func.now(),
        server_default=FetchedValue(),
        server_onupdate=FetchedValue(),
    )

    __mapper_args__ = {"eager_defaults": True}

上記と同様のマッピングでは、ORMによってINSERTとUPDATEのためにレンダリングされたSQLは、RETURNING句の中に createdupdated を含みます。:

INSERT INTO my_table (created) VALUES (now()) RETURNING my_table.id, my_table.created, my_table.updated

UPDATE my_table SET updated=now() WHERE my_table.id = %(my_table_id)s RETURNING my_table.updated

Using INSERT, UPDATE and ON CONFLICT (i.e. upsert) to return ORM Objects

SQLAlchemy 2.0には、ORM対応のINSERT、UPDATE、upsert文のいくつかの種類を出力する拡張機能が含まれています。ドキュメントについては ORM-Enabled INSERT, UPDATE, and DELETE statements のドキュメントを参照してください。upsertについては ORM “upsert” Statements を参照してください。

Using PostgreSQL ON CONFLICT with RETURNING to return upserted ORM objects

このセクションは ORM “upsert” Statements に移動しました。

Partitioning Strategies (e.g. multiple database backends per Session)

Simple Vertical Partitioning

垂直分割は、 Session.binds 引数で Session を設定することで、複数のデータベースにまたがって、異なるクラス、クラス階層、またはマップされたテーブルを配置します。この引数は、ORMマップクラス、マップされた階層内の任意のクラス(宣言的な基底クラスやミックスインなど)、 Table オブジェクト、および Mapper オブジェクトの任意の組み合わせをキーとして含む辞書を受け取ります。これらは通常 Engine または、あまり一般的ではありませんが Connection オブジェクトをターゲットとして参照します。辞書は、データベース接続の適切なソースを見つけるために、 Session が特定の種類のマップされたクラスに代わってSQLを出力する必要があるときに参照されます:

engine1 = create_engine("postgresql+psycopg2://db1")
engine2 = create_engine("postgresql+psycopg2://db2")

Session = sessionmaker()

# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})

session = Session()

上記では、いずれかのクラスに対するSQL操作は、そのクラスにリンクされた Engine を使用します。この機能は読み取りと書き込みの両方の操作にわたって包括的です。つまり、(要求された項目のリストの最初のエンティティを調べることによって決定される) engine1 にマップされたエンティティに対する Query は、 engine1 を使用してクエリを実行します。フラッシュ操作は、 User 型と Account 型のオブジェクトをフラッシュするときに、クラスごとに**両方**のエンジンを使用します。

より一般的なケースでは、異なるデータベース接続を対象とした操作を区別するために使用できるベースクラスまたはミックスインクラスが一般的に存在します。 Session.binds 引数は、任意のPythonクラスをキーとして受け入れることができ、特定のマップされたクラスの「__mro__」(Pythonメソッドの解決順序)にあることが判明した場合に使用されます。2つの宣言ベースが2つの異なるデータベース接続を表していると仮定します:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Session

class BaseA(DeclarativeBase):
    pass

class BaseB(DeclarativeBase):
    pass

class User(BaseA): ...

class Address(BaseA): ...

class GameInfo(BaseB): ...

class GameStats(BaseB): ...

Session = sessionmaker()

# all User/Address operations will be on engine 1, all
# Game operations will be on engine 2
Session.configure(binds={BaseA: engine1, BaseB: engine2})

上の例では、 BaseABaseB から派生したクラスのSQL操作は、どのスーパークラスから派生したか(存在する場合)に基づいて、2つのエンジンのいずれかに送られます。複数の「束縛された」スーパークラスから派生したクラスの場合、ターゲットクラスの階層の最上位にあるスーパークラスが、どのエンジンを使うべきかを表すために選ばれます。

See also

Session.binds

Coordination of Transactions for a multiple-engine Session

複数のバウンドエンジンを使用する場合の注意点の1つは、あるバックエンドでコミットが成功した後に、別のバックエンドでコミット操作が失敗する可能性がある場合である。これは、リレーショナルデータベースでは「2フェーズトランザクション」を使用して解決される不整合の問題であり、これは、実際にトランザクションを完了する前に複数のデータベースがコミットに同意することを可能にする追加の「準備」ステップをコミットシーケンスに追加する。

DB API内でのサポートが制限されているため、SQLAlchemyはバックエンド間の2フェーズトランザクションのサポートが制限されています。最も一般的には、PostgreSQLバックエンドでうまく機能することが知られていますが、MySQLバックエンドではそれほど機能しません。ただし、バックエンドが2フェーズトランザクション機能をサポートしている場合、 Session は、 sessionmaker または Session 内で Session.use_twophase フラグを設定することで、2フェーズトランザクション機能を十分に活用できます。例については、 Enabling Two-Phase Commit を参照してください。

Custom Vertical Partitioning

Session.get_bind() メソッドをオーバーライドすることで、より包括的なルールベースのクラスレベルのパーティショニングを構築できます。以下に、以下のルールを提供するカスタム Session を示します。

  1. フラッシュ操作とバルク 更新 および 削除 操作は、 leader という名前のエンジンに配信されます。

  2. MyOtherClass をサブクラスとするオブジェクトに対する操作は、すべて other エンジンで行われます。

  3. 他のすべてのクラスの読み込み操作は、 follower1 または follower2 データベースのランダムな選択で行われます。

engines = {
    "leader": create_engine("sqlite:///leader.db"),
    "other": create_engine("sqlite:///other.db"),
    "follower1": create_engine("sqlite:///follower1.db"),
    "follower2": create_engine("sqlite:///follower2.db"),
}

from sqlalchemy.sql import Update, Delete
from sqlalchemy.orm import Session, sessionmaker
import random

class RoutingSession(Session):
    def get_bind(self, mapper=None, clause=None):
        if mapper and issubclass(mapper.class_, MyOtherClass):
            return engines["other"]
        elif self._flushing or isinstance(clause, (Update, Delete)):
            # NOTE: this is for example, however in practice reader/writer
            # splits are likely more straightforward by using two distinct
            # Sessions at the top of a "reader" or "writer" operation.
            # See note below
            return engines["leader"]
        else:
            return engines[random.choice(["follower1", "follower2"])]

上の Session クラスは、 sessionmaker への class_ 引数を使ってプラグインされています:

Session = sessionmaker(class_=RoutingSession)

このアプローチは、 __abstract__ で説明されている宣言的な __abstract__ キーワードを使用するなどのアプローチを使用して、複数の MetaData オブジェクトと組み合わせることができます。

Note

上記の例では、特定のSQL文がデータを書き込むかどうかに基づいて、いわゆる”リーダー”または”フォロワー”データベースにルーティングすることを示していますが、これは、同じ操作内での読み取りと書き込みの間のトランザクション動作が調整されないことになるため、実用的なアプローチではない可能性があります。実際には、進行中の操作/トランザクション全体に基づいて、 Session を”リーダー”または”ライター”セッションとして事前に構築するのが最善です。そうすれば、データを書き込む操作も、同じトランザクションスコープ内で読み取りクエリを発行します。 Setting Isolation For A Sessionmaker / Engine Wide の例を参照して、オートコミット接続を使用した”読み取り専用”操作用に1つの sessionmaker を設定し、DML/COMMITを含む”書き込み”操作用に別の sessionmaker を設定するレシピを確認してください。

See also

Django-style Database Routers in SQLAlchemy - Session.get_bind() のより包括的な例に関するブログ記事

Horizontal Partitioning

水平分割は、1つのテーブル(またはテーブルのセット)の行を複数のデータベースに分割します。SQLAlchemy Session にはこの概念のサポートが含まれていますが、完全に使用するには SessionQuery のサブクラスを使用する必要があります。これらのサブクラスの基本バージョンは Horizontal Sharding ORM拡張で利用できます。使用例は: Horizontal Sharding にあります。

Bulk Operations

Legacy Feature

SQLAlchemy 2.0は、 Session の”バルクインサート”と”バルクアップデート”機能を2.0スタイルの Session.execute() メソッドに統合し、 InsertUpdate 構文を直接使用します。ドキュメントについては ORM-Enabled INSERT, UPDATE, and DELETE statements のドキュメントを参照してください。これには、古いメソッドから新しいメソッドへの移行を示す Legacy Session Bulk INSERT Methods が含まれます。