2017-08-27 2 views
0

J'ai créé un « gestionnaire » objet autour d'un moteur de base de données SQLAlchemy/connexion/session:Créer une transaction à DB opérations Rollback Autour de ce `manager` Structure

Base = declarative_base() 

class Manager(object): 
    def __init__(self, connection: str = 'sqlite://'): 
     self.engine = create_engine(connection, echo=True) 
     Base.metadata.create_all(self.engine) 
     self.sessionmaker = sessionmaker(bind=self.engine) 
     self.session = scoped_session(self.sessionmaker) 

    def do_db_stuff(self): 
     self.session.query(Whatever).all() 

    def ensure_thing(self): 
     thing = Thing() 
     self.session.add(thing) 
     self.session.commit() 

Je voudrais créer deux py.test fixtures: une pour instancier le gestionnaire et une pour envelopper et annuler les transactions dans les tests qui peuvent appeler commit. This is the pattern J'ai essayé de suivre, sans succès:

@pytest.fixture(scope='session') 
def manager(): 
    m = Manager() 
    return m 


@pytest.fixture(scope='function') 
def manager_session(manager): 
    connection = manager.session.connection() 
    transaction = connection.begin() 

    yield manager 

    manager.session.close() 
    transaction.rollback() 
    connection.close() 

Malheureusement, les objets créés par le gestionnaire, même quand ils sont entourés par le bâton au-dessus autour après l'appel à transaction.rollback().

Quelle est la bonne façon d'encapsuler une transaction autour d'une session existante comme celle-ci?

EDIT:

Une autre, différente tentative:

@pytest.fixture(scope='function') 
def manager_session(manager): 
    connection = manager.engine.connect() 
    transaction = connection.begin() 
    manager.sessionmaker.configure(bind=connection) 

    yield manager 

    manager.session.close() 
    transaction.rollback() 

EDIT 2:

Une troisième tentative qui semble fonctionner, avec la mise en garde mentionnée dans Ilja Everilä's answer below ce code enfilée causerait des ennuis.

@pytest.fixture(scope='session') 
def manager(): 
    return Manager() 


@pytest.fixture(scope='function') 
def manager_transaction(manager): 
    connection = manager.engine.connect() 
    transaction = connection.begin() 
    manager.session_maker.configure(bind=connection) 

    yield manager 

    manager.session_maker.configure(bind=manager.engine) 
    manager.session.remove() 
    transaction.rollback() 
    connection.close() 
+0

Comment la 2ème tentative a-t-elle échoué? –

+0

La panne est survenue après le premier test en utilisant l'appareil terminé et le deuxième test a commencé. Je pense (mais je ne suis pas sûr) que le deuxième test a eu la même session malgré que je l'aie fermée. Passer à 'session.remove' a corrigé cela, semble-t-il. –

Répondre

1

La première tentative échoue car la session contrôle réellement la connexion et sa transaction. Vous pouvez vérifier cela en regardant la journalisation produite. La session commence une transaction implicite lorsque vous appelez manager.session.connection() et votre appel explicite à begin() après est une opération no qui renvoie l'objet de transaction en cours. Ainsi, lorsque vous validez dans les méthodes du gestionnaire, vous vous engagez pour de vrai, et l'objet de transaction désormais obsolète ne fait rien lorsque vous annulez.

La 2ème tentative fonctionne pour moi comme, si vous utilisez SQLite DB en mémoire, mais cela ne fonctionnerait pas si votre code réel est légèrement différent de ce que vous avez présenté. Vous définissez la connexion créée comme lier sur le self.sessionmaker, pas sur une session dans le scoped session registryself.session déjà créé, et donc si vous avez touché le registre de session d'une manière ou d'une autre avant de configurer le fabricant, vous avez en fait créé une session en utilisant le moteur comme lier dans le thread courant:

In [7]: m = Manager() 

In [8]: m.session.bind 
Out[8]: Engine(sqlite://) 

In [9]: connection = m.engine.connect() 

In [10]: transaction = connection.begin() 
2017-08-28 14:24:02,584 INFO sqlalchemy.engine.base.Engine BEGIN (implicit) 

In [11]: m.sessionmaker.configure(bind=connection) 

In [12]: m.session.bind 
Out[12]: Engine(sqlite://) 

donc, en plus de la configuration du sessionmaker, vous devez vous assurer qu'une session n'a pas été enregistrée dans le registre avant. Notez également que si vous avez du code qui utilise des threads, le registre partagera la connexion entre eux, et cela va causer des problèmes.

+0

Merci pour les commentaires perspicaces. J'ai remarqué quelques avertissements au cours de ma marche aléatoire vers une solution sur la configuration d'un gestionnaire de session avec des sessions existantes. Je n'utilise pas de threads dans ce code, mais avez-vous des idées sur la façon de contourner ce problème spécifique? Peut-être un atelier de couture personnalisé pourrait-il être en ordre ... –

+0

Serait-il préférable de faire quelque chose comme '... transaction de configuration; manager.session (bind = connexion); gestionnaire de rendement; ... 'et ignorer la configuration du Sessionmaker entièrement? –