3 votes

Est-ce qu'une colonne ORM peut déclencher un vidage de session dans SQLAlchemy ?

Question

L'accès à une propriété peut-il déclencher un vidage de session dans SQLAlchemy ? Je m'attendrais à ce que, par exemple, les requêtes attachées à un objet via column_property() ou @hybrid_property provoquent un autoflush de la session, de la même manière que les requêtes effectuées via session.Query(). Cela ne semble pas être le cas.

Dans l'exemple simple ci-dessous, un compte contient une collection d'entrées. Il fournit également une propriété "balance", construite avec column_property(), qui expose une requête select-sum. Les nouvelles entrées n'apparaissent dans le solde d'un compte que si session.flush() est appelée explicitement.

Ce comportement semble sous-optimal : les utilisateurs de la classe Account doivent saupoudrer des appels flush() dans leur code en se basant sur la connaissance des aspects internes de l'implémentation de la balance. Si l'implémentation change - par exemple, si "balance" était auparavant une propriété Python @property - des bogues peuvent être introduits même si l'interface Account est essentiellement identique. Existe-t-il une alternative ?

Exemple complet

import sys
import sqlalchemy as sa
import sqlalchemy.sql
import sqlalchemy.orm
import sqlalchemy.ext.declarative

Base = sa.ext.declarative.declarative_base()

class Entry(Base):
    __tablename__ = "entries"

    id = sa.Column(sa.Integer, primary_key=True)
    value = sa.Column(sa.Numeric, primary_key=True)
    account_id = sa.Column(sa.Integer, sa.ForeignKey("accounts.id"))
    account = sa.orm.relationship("Account", backref="entries")

class Account(Base):
    __tablename__ = "accounts"

    id = sa.Column(sa.Integer, primary_key=True)
    balance = sa.orm.column_property(
        sa.sql.select([sa.sql.func.sum(Entry.value)])
            .where(Entry.account_id == id)
        )

def example(database_url):
    # connect to the database and prepare the schema
    engine = sa.create_engine(database_url)
    session = sa.orm.sessionmaker(bind=engine)()

    Base.metadata.create_all(bind = engine)

    # add an entry to an account
    account = Account()

    account.entries.append(Entry(value = 42))

    session.add(account)

    # and look for that entry in the balance
    print "account.balance:", account.balance

    assert account.balance == 42

if __name__ == "__main__":
    example(sys.argv[1])

Production observée

$ python sa_column_property_example.py postgres:///za_test
account.balance: None
Traceback (most recent call last):
  File "sa_column_property_example.py", line 46, in <module>
    example(sys.argv[1])
  File "sa_column_property_example.py", line 43, in example
    assert account.balance == 42
AssertionError

Sortie préférée

J'aimerais voir "account.balance : 42", sans ajouter un appel explicite à session.flush().

4voto

zzzeek Points 22617

Une propriété de colonne n'est évaluée qu'au moment de la requête, c'est-à-dire lorsque vous dites query(Account), ainsi que lorsque l'attribut est expiré, c'est-à-dire si vous dites session.expire("account", ['balance']).

Pour qu'un attribut invoque une requête à chaque fois, nous utilisons une @propriété (quelques petites modifications ici pour que le script fonctionne avec sqlite) :

import sys
import sqlalchemy as sa
import sqlalchemy.sql
import sqlalchemy.orm
import sqlalchemy.ext.declarative

Base = sa.ext.declarative.declarative_base()

class Entry(Base):
    __tablename__ = "entries"

    id = sa.Column(sa.Integer, primary_key=True)
    value = sa.Column(sa.Numeric)
    account_id = sa.Column(sa.Integer, sa.ForeignKey("accounts.id"))
    account = sa.orm.relationship("Account", backref="entries")

class Account(Base):
    __tablename__ = "accounts"

    id = sa.Column(sa.Integer, primary_key=True)

    @property
    def balance(self):
        return sqlalchemy.orm.object_session(self).query(
                    sa.sql.func.sum(Entry.value)
                ).filter(Entry.account_id == self.id).scalar()

def example(database_url):
    # connect to the database and prepare the schema
    engine = sa.create_engine(database_url, echo=True)
    session = sa.orm.sessionmaker(bind=engine)()

    Base.metadata.create_all(bind = engine)

    # add an entry to an account
    account = Account()

    account.entries.append(Entry(value = 42))

    session.add(account)

    # and look for that entry in the balance
    print "account.balance:", account.balance

    assert account.balance == 42

if __name__ == "__main__":
    example("sqlite://")

Notez que le "flush" lui-même n'est généralement pas quelque chose dont nous devons nous préoccuper ; la fonction autoflush s'assurera que le flush est appelé à chaque fois que query() va chercher des résultats dans la base de données, donc c'est vraiment s'assurer qu'une requête a lieu qui est ce que nous recherchons.

Une autre approche consiste à utiliser des hybrides. Je vous recommande de lire l'aperçu de ces trois méthodes à l'adresse suivante Expressions SQL en tant qu'attributs mappés qui énumère les avantages et inconvénients de chaque approche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X