Comment obtenir une requête SQL brute et compilée à partir d'une expression SQLAlchemy?

103

J'ai un objet de requête SQLAlchemy et je veux obtenir le texte de l'instruction SQL compilée, avec tous ses paramètres liés (par exemple, aucune %sou d'autres variables en attente d'être liées par le compilateur d'instructions ou le moteur de dialecte MySQLdb, etc.).

L'appel str()sur la requête révèle quelque chose comme ceci:

SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC

J'ai essayé de chercher dans query._params mais c'est un dict vide. J'ai écrit mon propre compilateur en utilisant cet exemple du sqlalchemy.ext.compiler.compilesdécorateur, mais même la déclaration contient toujours %soù je veux des données.

Je ne peux pas vraiment comprendre quand mes paramètres sont mélangés pour créer la requête; lors de l'examen de l'objet de requête, il s'agit toujours d'un dictionnaire vide (bien que la requête s'exécute correctement et que le moteur l'imprime lorsque vous activez la journalisation d'écho).

Je commence à recevoir le message que SQLAlchemy ne veut pas que je connaisse la requête sous-jacente, car elle rompt la nature générale de l'interface de l'API d'expression avec toutes les différentes DB-API. Cela ne me dérange pas si la requête est exécutée avant que je découvre ce que c'était; Je veux juste savoir!

cce
la source

Réponses:

108

Ce blog fournit une réponse mise à jour.

Citant l'article du blog, cela est suggéré et a fonctionné pour moi.

>>> from sqlalchemy.dialects import postgresql
>>> print str(q.statement.compile(dialect=postgresql.dialect()))

Où q est défini comme:

>>> q = DBSession.query(model.Name).distinct(model.Name.value) \
             .order_by(model.Name.value)

Ou n'importe quel type de session.query ().

Merci à Nicolas Cadou pour la réponse! J'espère que cela aidera les autres qui viennent chercher ici.

AndyBarr
la source
2
Existe-t-il un moyen simple d'obtenir les valeurs sous forme de dictionnaire?
Damien
6
@Damien donné c = q.statement.compile(...), vous pouvez juste obtenirc.params
Hannele
1
Le message est étiqueté avec mysql, donc les détails postgresql dans cette réponse ne sont pas vraiment pertinents.
Hannele
4
Si je comprends bien le PO, il veut la dernière question. L'impression avec la spécification d'un dialecte (ici postgres) me donne toujours les espaces réservés au lieu des valeurs littérales. La réponse de @ Matt fait le travail. Obtenir le SQL avec des espaces réservés peut être plus simple avec la as_scalar()méthode de Query.
Patrick B. du
1
@PatrickB. Je suis d'accord. La réponse de Matt doit être considérée comme la réponse «correcte». J'obtiens le même résultat en faisant simplement str(q).
André C. Andersen
92

La documentation utilise literal_bindspour imprimer une requête qcomprenant des paramètres:

print(q.statement.compile(compile_kwargs={"literal_binds": True}))

l'approche ci-dessus a la mise en garde qu'elle n'est prise en charge que pour les types de base, tels que les ints et les chaînes, et de plus si un bindparam () sans valeur prédéfinie est utilisé directement, il ne pourra pas non plus le stringify.

La documentation émet également cet avertissement:

N'utilisez jamais cette technique avec un contenu de chaîne reçu à partir d'une entrée non approuvée, telle que des formulaires Web ou d'autres applications d'entrée utilisateur. Les fonctionnalités de SQLAlchemy pour contraindre les valeurs Python en valeurs de chaîne SQL directes ne sont pas protégées contre les entrées non approuvées et ne valident pas le type de données transmises. Utilisez toujours des paramètres liés lors de l'appel par programme d'instructions SQL non DDL sur une base de données relationnelle.

Mat
la source
Je vous remercie! Cela a été extrêmement utile, m'a permis d'utiliser la fonction pandas read_sql sans douleur!
Justin Palmer le
24

Cela devrait fonctionner avec Sqlalchemy> = 0.6

from sqlalchemy.sql import compiler

from psycopg2.extensions import adapt as sqlescape
# or use the appropiate escape function from your db driver

def compile_query(query):
    dialect = query.session.bind.dialect
    statement = query.statement
    comp = compiler.SQLCompiler(dialect, statement)
    comp.compile()
    enc = dialect.encoding
    params = {}
    for k,v in comp.params.iteritems():
        if isinstance(v, unicode):
            v = v.encode(enc)
        params[k] = sqlescape(v)
    return (comp.string.encode(enc) % params).decode(enc)
Albertov
la source
2
Merci pour cela! Malheureusement, j'utilise MySQL donc mon dialecte est «positionnel» et doit avoir une liste de paramètres plutôt qu'un dictionnaire. J'essaie actuellement de faire en sorte que votre exemple fonctionne avec cela ..
cce
Veuillez ne pas utiliser adaptde cette manière. Au minimum, appelez prepare () sur la valeur de retour de celui-ci à chaque fois, en fournissant la connexion comme argument, afin qu'il puisse faire des guillemets appropriés.
Alex Gaynor le
@Alex: Quelle serait la bonne façon de faire des citations appropriées avec psycopg? (en plus d'appeler prepare () sur la valeur de retour, ce que vous semblez impliquer n'est pas optimal)
albertov
Désolé, je pense que mon phrasé était mauvais, tant que vous appelez obj.prepare (connexion), vous devriez être d'accord. Ceci est dû au fait que les "bonnes" API fournies par la libpq pour les guillemets nécessitent la connexion (et elle fournit des éléments comme l'encodage des chaînes Unicode).
Alex Gaynor
1
Merci. Je l' ai essayé d' appeler preparesur la valeur de retour , mais est semble qu'il n'a pas cette méthode: AttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'. J'utilise psycopg2 2.2.1 BTW
albertov
18

Pour le backend MySQLdb, j'ai un peu modifié la réponse géniale d'albertov (merci beaucoup!). Je suis sûr qu'ils pourraient être fusionnés pour vérifier si comp.positionalc'était le cas , Truemais cela dépasse légèrement la portée de cette question.

def compile_query(query):
    from sqlalchemy.sql import compiler
    from MySQLdb.converters import conversions, escape

    dialect = query.session.bind.dialect
    statement = query.statement
    comp = compiler.SQLCompiler(dialect, statement)
    comp.compile()
    enc = dialect.encoding
    params = []
    for k in comp.positiontup:
        v = comp.params[k]
        if isinstance(v, unicode):
            v = v.encode(enc)
        params.append( escape(v, conversions) )
    return (comp.string.encode(enc) % tuple(params)).decode(enc)
cce
la source
Impressionnant! J'avais juste besoin que la liste des paramètres liés soit envoyée à MySQL et que la modification de ce qui précède return tuple(params)fonctionne comme un charme! Vous m'avez évité d'innombrables heures de devoir emprunter une route extrêmement pénible.
horcle_buzz
16

Le fait est que sqlalchemy ne mélange jamais les données avec votre requête. La requête et les données sont transmises séparément à votre pilote de base de données sous-jacent - l'interpolation des données se produit dans votre base de données.

Sqlalchemy transmet la requête comme vous l'avez vu str(myquery)à la base de données, et les valeurs iront dans un tuple séparé.

Vous pouvez utiliser une approche où vous interpolez vous-même les données avec la requête (comme albertov suggéré ci-dessous), mais ce n'est pas la même chose que sqlalchemy exécute.

nosklo
la source
pourquoi n'est-ce pas la même chose? Je comprends que DB-API effectue des transactions, peut-être réorganise les requêtes, etc., mais pourrait-il modifier ma requête plus que cela?
cce
1
@cce: vous essayez de trouver la requête finale. SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC EST la dernière question. Ceux-ci %ssont envoyés à la base de données par sqlalchemy - sqlalchemy ne met JAMAIS les données réelles à la place du% s
nosklo
@cce: Certains modules dbapi ne le font pas non plus - cela est souvent fait par la base de données elle
nosklo
1
aha je vois ce que vous dites, merci - creuser plus loin sqlalchemy.dialects.mysql.mysqldb, do_executemany()transmet l'instruction et les paramètres séparément au curseur MySQLdb. yay indirection!
cce
11

Permettez-moi d'abord de dire que je suppose que vous faites cela principalement à des fins de débogage - je ne recommanderais pas d'essayer de modifier l'instruction en dehors de l'API SQLAlchemy fluent.

Malheureusement, il ne semble pas y avoir de moyen simple d'afficher l'instruction compilée avec les paramètres de requête inclus. SQLAlchemy ne met pas réellement les paramètres dans l'instruction - ils sont transmis au moteur de base de données sous forme de dictionnaire . Cela permet à la bibliothèque spécifique à la base de données de gérer des choses comme l'échappement des caractères spéciaux pour éviter l'injection SQL.

Mais vous pouvez le faire en deux étapes assez facilement. Pour obtenir l'instruction, vous pouvez faire comme vous l'avez déjà montré, et simplement imprimer la requête:

>>> print(query)
SELECT field_1, field_2 FROM table WHERE id=%s;

Vous pouvez faire un pas de plus avec query.statement, pour voir les noms des paramètres. Remarque :id_1ci-dessous vs %sci-dessus - pas vraiment un problème dans cet exemple très simple, mais pourrait être la clé dans une déclaration plus compliquée.

>>> print(query.statement)
>>> print(query.statement.compile()) # seems to be equivalent, you can also
                                     # pass in a dialect if you want
SELECT field_1, field_2 FROM table WHERE id=:id_1;

Ensuite, vous pouvez obtenir les valeurs réelles des paramètres en obtenant la paramspropriété de l'instruction compilée:

>>> print(query.statement.compile().params)
{u'id_1': 1} 

Cela a fonctionné pour un backend MySQL au moins; Je m'attendrais à ce que ce soit également assez général pour PostgreSQL sans avoir besoin d'utiliser psycopg2.

Hannele
la source
Depuis le débogueur PyCharm, ce qui suit a fonctionné pour moi ... qry.compile (). Params
Ben
Intéressant, pourrait être SQLAlchemy a un peu changé depuis que j'ai écrit cette réponse.
Hannele
10

Pour le backend postgresql utilisant psycopg2, vous pouvez écouter l' do_executeévénement, puis utiliser le curseur, l'instruction et le type de paramètres coercés avec Cursor.mogrify()pour incorporer les paramètres. Vous pouvez renvoyer True pour empêcher l'exécution réelle de la requête.

import sqlalchemy

class QueryDebugger(object):
    def __init__(self, engine, query):
        with engine.connect() as connection:
            try:
                sqlalchemy.event.listen(engine, "do_execute", self.receive_do_execute)
                connection.execute(query)
            finally:
                sqlalchemy.event.remove(engine, "do_execute", self.receive_do_execute)

    def receive_do_execute(self, cursor, statement, parameters, context):
        self.statement = statement
        self.parameters = parameters
        self.query = cursor.mogrify(statement, parameters)
        # Don't actually execute
        return True

Exemple d'utilisation:

>>> engine = sqlalchemy.create_engine("postgresql://postgres@localhost/test")
>>> metadata = sqlalchemy.MetaData()
>>> users = sqlalchemy.Table('users', metadata, sqlalchemy.Column("_id", sqlalchemy.String, primary_key=True), sqlalchemy.Column("document", sqlalchemy.dialects.postgresql.JSONB))
>>> s = sqlalchemy.select([users.c.document.label("foobar")]).where(users.c.document.contains({"profile": {"iid": "something"}}))
>>> q = QueryDebugger(engine, s)
>>> q.query
'SELECT users.document AS foobar \nFROM users \nWHERE users.document @> \'{"profile": {"iid": "something"}}\''
>>> q.statement
'SELECT users.document AS foobar \nFROM users \nWHERE users.document @> %(document_1)s'
>>> q.parameters
{'document_1': '{"profile": {"iid": "something"}}'}
rectalogique
la source
4

La solution suivante utilise le langage d'expression SQLAlchemy et fonctionne avec SQLAlchemy 1.1. Cette solution ne mélange pas les paramètres avec la requête (comme demandé par l'auteur d'origine), mais fournit un moyen d'utiliser les modèles SQLAlchemy pour générer des chaînes de requête SQL et des dictionnaires de paramètres pour différents dialectes SQL. L'exemple est basé sur le tutoriel http://docs.sqlalchemy.org/en/rel_1_0/core/tutorial.html

Compte tenu de la classe,

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class foo(Base):
    __tablename__ = 'foo'
    id = Column(Integer(), primary_key=True)
    name = Column(String(80), unique=True)
    value = Column(Integer())

nous pouvons produire une instruction de requête en utilisant la fonction de sélection .

from sqlalchemy.sql import select    
statement = select([foo.name, foo.value]).where(foo.value > 0)

Ensuite, nous pouvons compiler l'instruction dans un objet de requête.

query = statement.compile()

Par défaut, l'instruction est compilée à l'aide d'une implémentation «nommée» de base compatible avec les bases de données SQL telles que SQLite et Oracle. Si vous devez spécifier un dialecte tel que PostgreSQL, vous pouvez faire

from sqlalchemy.dialects import postgresql
query = statement.compile(dialect=postgresql.dialect())

Ou si vous souhaitez spécifier explicitement le dialecte en tant que SQLite, vous pouvez changer le style de paramètre de «qmark» à «nommé».

from sqlalchemy.dialects import sqlite
query = statement.compile(dialect=sqlite.dialect(paramstyle="named"))

À partir de l'objet de requête, nous pouvons extraire la chaîne de requête et les paramètres de requête

query_str = str(query)
query_params = query.params

et enfin exécutez la requête.

conn.execute( query_str, query_params )
Eric
la source
En quoi cette réponse est-elle meilleure / différente de celle d'AndyBarr publiée 2 ans plus tôt?
Piotr Dobrogost
La réponse d'AndyBarr inclut un exemple de génération d'une instruction de requête avec une DBSession alors que cette réponse comprend un exemple utilisant l'API déclarative et la méthode select. En ce qui concerne la compilation de l'instruction de requête avec un certain dialecte, les réponses sont les mêmes. J'utilise SQLAlchemy pour générer des requêtes brutes, puis les exécuter avec l'adbapi de Twister. Pour ce cas d'utilisation, savoir comment compiler la requête sans session et extraire la chaîne de requête et les paramètres est utile.
eric
3

Vous pouvez utiliser les événements de la famille ConnectionEvents : after_cursor_executeou before_cursor_execute.

Dans sqlalchemy UsageRecipes by @zzzeek, ​​vous pouvez trouver cet exemple:

Profiling

...
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement,
                        parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())
    logger.debug("Start Query: %s" % statement % parameters)
...

Ici vous pouvez accéder à votre relevé

Alex Bender
la source
2

Donc, en rassemblant beaucoup de petits morceaux de ces différentes réponses, j'ai trouvé ce dont j'avais besoin: un simple ensemble de code à insérer et de temps en temps mais de manière fiable (c'est-à-dire gère tous les types de données) saisir le SQL compilé exact envoyé à mon Postgres backend en interrogeant simplement la requête elle-même:

from sqlalchemy.dialects import postgresql

query = [ .... some ORM query .... ]

compiled_query = query.statement.compile(
    dialect=postgresql.dialect()
)
mogrified_query = session.connection().connection.cursor().mogrify(
    str(compiled_query),
    compiled_query.params
)

print("compiled SQL = {s}".format(mogrified_query.decode())
David K. Hess
la source
0

Je pense que .statement ferait probablement l'affaire: http://docs.sqlalchemy.org/en/latest/orm/query.html?highlight=query

>>> local_session.query(sqlalchemy_declarative.SomeTable.text).statement
<sqlalchemy.sql.annotation.AnnotatedSelect at 0x6c75a20; AnnotatedSelectobject>
>>> x=local_session.query(sqlalchemy_declarative.SomeTable.text).statement
>>> print(x)
SELECT sometable.text 
FROM sometable
user2757902
la source
L'instruction ne vous montre pas quels sont les paramètres, si vous avez configuré certains types de filtres.
Hannele