web-dev-qa-db-fra.com

Comment faire de SQLAlchemy dans Tornado pour être asynchrone?

Comment faire que SQLAlchemy dans Tornado soit async? J'ai trouvé un exemple pour MongoDB sur exemple async mongo mais je n'ai rien trouvé comme motor pour SQLAlchemy. Quelqu'un sait-il comment exécuter des requêtes SQLAlchemy à exécuter avec tornado.gen (J'utilise MySQL ci-dessous SQLAlchemy, au moment où mes gestionnaires lisent à partir de la base de données et renvoient le résultat, je voudrais faire ceci asynchrone).

43
Damir

Les ORM sont mal adaptés à la programmation asynchrone explicite, c'est-à-dire que le programmeur doit produire des rappels explicites à chaque fois que quelque chose qui utilise l'accès au réseau se produit. Une raison principale à cela est que les ORM utilisent largement le modèle chargement paresseux , qui est plus ou moins incompatible avec l'async explicite. Code qui ressemble à ceci:

user = Session.query(User).first()
print user.addresses

émettra en fait deux requêtes distinctes - une lorsque vous dites first() pour charger une ligne et la suivante lorsque vous dites user.addresses, dans le cas où la collection .addresses n'est pas ' t déjà présent ou a expiré. Essentiellement, presque toutes les lignes de code qui traitent des constructions ORM peuvent bloquer sur IO, donc vous seriez dans des spaghettis de rappel étendus en quelques secondes - et pour aggraver les choses, la grande majorité de ces lignes de code ne en fait bloquer sur les E/S, donc tous les frais généraux de connexion des rappels pour ce qui serait autrement de simples opérations d'accès aux attributs rendront votre programme beaucoup moins efficace aussi.

Un problème majeur avec les modèles asynchrones explicites est qu'ils ajoutent une surcharge énorme d'appels de fonction Python aux systèmes complexes - pas seulement du côté de l'utilisateur comme vous le faites avec le chargement paresseux, mais aussi du côté interne en ce qui concerne la façon dont le système fournit une abstraction autour de l'API de base de données Python (DBAPI). Pour SQLAlchemy, même avoir une prise en charge asynchrone de base imposerait une pénalité de performances sévère à la grande majorité des programmes qui n'utilisent pas de modèles asynchrones, et même à ces programmes asynchrones qui ne sont pas hautement concurrents. Considérez SQLAlchemy, ou toute autre couche ORM ou abstraction, peut avoir du code comme celui-ci:

def execute(connection, statement):
     cursor = connection.cursor()
     cursor.execute(statement)
     results = cursor.fetchall()
     cursor.close()
     return results

Le code ci-dessus effectue ce qui semble être une opération simple, exécutant une instruction SQL sur une connexion. Mais en utilisant une DBAPI entièrement asynchrone comme l'extension asynchrone de psycopg2, le code ci-dessus se bloque sur IO au moins trois fois. Donc, pour écrire le code ci-dessus dans un style asynchrone explicite, même lorsqu'il n'y a pas de moteur asynchrone utilisé et que les rappels ne sont pas réellement bloquants, cela signifie que l'appel de fonction externe ci-dessus devient au moins trois appels de fonction, au lieu d'un, sans inclure la surcharge imposée par le système asynchrone explicite ou les appels DBAPI eux-mêmes. Ainsi, une application simple reçoit automatiquement une pénalité de 3 fois la surcharge d'appel de fonction entourant une abstraction simple autour de l'exécution des instructions. Et en Python, la surcharge d'appel de fonction est tout .

Pour ces raisons, je continue d'être peu enthousiasmé par le battage médiatique entourant les systèmes asynchrones explicites, au moins dans la mesure où certaines personnes semblent vouloir tout asynchroniser pour tout, comme la livraison de pages Web (voir node.js). Je recommanderais d'utiliser des systèmes asynchrones implicites à la place, notamment gevent , où vous obtenez tous les avantages non bloquants IO d'un modèle asynchrone et aucun des détails/inconvénients structurels de rappels explicites. Je continue d'essayer de comprendre les cas d'utilisation de ces deux approches, donc je suis intrigué par l'attrait de l'approche asynchrone explicite comme solution à tous les problèmes, c'est-à-dire comme vous le voyez avec node.js - nous utilisons des langages de script dans le le premier endroit pour réduire la verbosité et la complexité du code, et l'async explicite pour des choses simples comme la livraison de pages Web ne semble rien faire d'autre que d'ajouter un passe-partout qui peut tout aussi bien être automatisé par gevent ou similaire, si le blocage IO est même un tel problème dans un cas comme celui-ci (de nombreux sites Web à volume élevé conviennent très bien avec un modèle IO synchrone). Les systèmes basés sur Gevent ont fait leurs preuves en production et leur popularité augmente, donc si vous aimez l'automatisation de code fournie par les ORM, vous voudrez peut-être également adopter l'automatisation de la planification async-IO qu'offre un système comme gevent.

Mise à jour : Nick Coghlan a souligné son excellent article sur le sujet de l'asynchrone explicite vs implicite qui est également une lecture incontournable ici . Et j'ai également été mis à jour sur le fait que pep-3156 accueille maintenant l'interopérabilité avec gevent , inversant son désintérêt précédemment déclaré pour gevent, en grande partie grâce à l'article de Nick. Donc, à l'avenir, je recommanderais un hybride de Tornado utilisant gevent pour la logique de la base de données, une fois que le système d'intégration de ces approches sera disponible.

74
zzzeek

J'ai eu ce même problème dans le passé et je n'ai pas pu trouver de bibliothèque Async-MySQL fiable. Cependant, il existe une solution intéressante utilisant Asyncio + Postgres . Vous avez juste besoin d'utiliser la bibliothèque aiopg , qui vient avec le support SQLAlchemy prêt à l'emploi:

import asyncio
from aiopg.sa import create_engine
import sqlalchemy as sa


metadata = sa.MetaData()

tbl = sa.Table('tbl', metadata,
           sa.Column('id', sa.Integer, primary_key=True),
           sa.Column('val', sa.String(255)))

@asyncio.coroutine
def go():
    engine = yield from create_engine(user='aiopg',
                                      database='aiopg',
                                      Host='127.0.0.1',
                                      password='passwd')

    with (yield from engine) as conn:
        yield from conn.execute(tbl.insert().values(val='abc'))

        res = yield from conn.execute(tbl.select().where(tbl.c.val=='abc'))
        for row in res:
            print(row.id, row.val)


loop = asyncio.get_event_loop()
loop.run_until_complete(go())
24
Ander

Pas une tornade, mais nous sorte de avons fait asynchroniser SQLAlchemy dans asyncio dans le projet GINO :

import asyncio
from gino import Gino, enable_task_local
from sqlalchemy import Column, Integer, Unicode, cast

db = Gino()


class User(db.Model):
    __table= 'users'

    id = Column(Integer(), primary_key=True)
    nickname = Column(Unicode(), default='noname')


async def main():
    await db.create_pool('postgresql://localhost/gino')

    # Create object, `id` is assigned by database
    u1 = await User.create(nickname='fantix')
    print(u1.id, u1.nickname)  # 1 fantix

    # Retrieve the same row, as a different object
    u2 = await User.get(u1.id)
    print(u2.nickname)  # fantix

    # Update affects only database row and the operating object
    await u2.update(nickname='daisy')
    print(u2.nickname)  # daisy
    print(u1.nickname)  # fantix

    # Returns all user objects with "d" in their nicknames
    users = await User.query.where(User.nickname.contains('d')).gino.all()

    # Find one user object, None if not found
    user = await User.query.where(User.nickname == 'daisy').gino.first()

    # Execute complex statement and return command status
    status = await User.update.values(
        nickname='No.' + cast(User.id, Unicode),
    ).where(
        User.id > 10,
    ).gino.status()

    # Iterate over the results of a large query in a transaction as required
    async with db.transaction():
        async for u in User.query.order_by(User.id).gino.iterate():
            print(u.id, u.nickname)


loop = asyncio.get_event_loop()
enable_task_local(loop)
loop.run_until_complete(main())

Il ressemble un peu, mais en fait assez différent à SQLAlchemy ORM. Parce que nous n'avons utilisé qu'une partie du noyau SQLAlchemy et avons construit un ORM simple par-dessus. Il utilise asyncpg en dessous, donc c'est pour PostgreSQL uniquement .

Mise à jour : GINO prend désormais en charge Tornado, grâce à la contribution de Vladimir Goncharov. Voir docs ici

4
Fantix King

J'utilise tornado avec sqlalchemy de la manière suivante:


from tornado_mysql import pools
from sqlalchemy.sql import table, column, select, join
from sqlalchemy.dialects import postgresql, mysql

# from models import M, M2

t = table(...)
t2 = table(...)

xxx_id = 10

j = join(t, t2, t.c.t_id == t2.c.id)
s = select([t]).select_from(j).where(t.c.xxx == xxx_id)

sql_str = s.compile(dialect=mysql.dialect(),compile_kwargs={"literal_binds": True})


pool = pools.Pool(conn_data...)
cur = yield pool.execute(sql_str)
data = cur.fetchone()

Dans ce cas, nous pouvons utiliser des modèles sqlalchemy et des outils sqlalchemy pour construire des requêtes.

3
Mykola Kharechko