Come ottengo una query SQL non elaborata e compilata da un'espressione SQLAlchemy?


103

Ho un oggetto query SQLAlchemy e voglio ottenere il testo dell'istruzione SQL compilata, con tutti i suoi parametri vincolati (es. No %so altre variabili in attesa di essere vincolate dal compilatore di istruzioni o dal motore dialettale MySQLdb, ecc.).

Il richiamo str()della query rivela qualcosa del genere:

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

Ho provato a cercare in query._params ma è un dict vuoto. Ho scritto il mio compilatore usando questo esempio del sqlalchemy.ext.compiler.compilesdecoratore, ma anche l'istruzione lì ha ancora %sdove voglio i dati.

Non riesco a capire quando i miei parametri vengono mescolati per creare la query; quando esaminano l'oggetto query, sono sempre un dizionario vuoto (sebbene la query venga eseguita correttamente e il motore la stampi quando si attiva la registrazione echo).

Sto iniziando a ricevere il messaggio che SQLAlchemy non vuole che io conosca la query sottostante, poiché interrompe la natura generale dell'interfaccia dell'API dell'espressione con tutte le diverse API DB. Non mi importa se la query viene eseguita prima che io abbia scoperto di cosa si trattava; Voglio solo sapere!

Risposte:


106

Questo blog fornisce una risposta aggiornata.

Citando dal post del blog, questo è suggerito e ha funzionato per me.

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

Dove q è definito come:

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

O semplicemente qualsiasi tipo di session.query ().

Grazie a Nicolas Cadou per la risposta! Spero che aiuti gli altri che vengono a cercare qui.


2
C'è un modo semplice per ottenere i valori come un dizionario?
Damien

6
@Damien dato c = q.statement.compile(...), puoi semplicemente ottenerec.params
Hannele

1
Il post è taggato con mysql, quindi i dettagli di postgresql in questa risposta non sono molto rilevanti.
Hannele

4
Se capisco correttamente l'OP, vuole la domanda finale. La stampa con la specificazione di un dialetto (qui postgres) mi dà ancora i segnaposto invece dei valori letterali. @ La risposta di Matt fa il lavoro. Ottenere l'SQL con i segnaposto può essere ottenuto in modo più semplice con il as_scalar()metodo di Query.
Patrick B.

1
@PatrickB. Sono d'accordo. La risposta di Matt dovrebbe essere considerata la risposta "corretta". Ottengo lo stesso risultato di questo semplicemente facendo str(q).
André C. Andersen

91

La documentazione utilizza literal_bindsper stampare una query che qinclude i parametri:

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

l'approccio di cui sopra ha l'avvertenza che è supportato solo per i tipi di base, come int e stringhe, e inoltre se un bindparam () senza un valore preimpostato viene utilizzato direttamente, non sarà in grado di stringere neanche quello.

La documentazione emette anche questo avviso:

Non utilizzare mai questa tecnica con contenuto di stringa ricevuto da input non attendibili, ad esempio da moduli Web o altre applicazioni di input dell'utente. Le funzionalità di SQLAlchemy per forzare i valori Python in valori stringa SQL diretti non sono protette da input non attendibili e non convalidano il tipo di dati passati. Utilizzare sempre parametri associati quando si richiamano a livello di codice istruzioni SQL non DDL su un database relazionale.


Grazie! Questo è stato estremamente utile, mi ha permesso di usare la funzione read_sql dei panda in modo indolore!
Justin Palmer,

24

Questo dovrebbe funzionare con 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)

2
Grazie per questo! Purtroppo sto usando MySQL, quindi il mio dialetto è "posizionale" e deve avere un elenco di parametri piuttosto che un dizionario. Attualmente
sto

Si prega di non utilizzare adaptin questo modo. Come minimo, chiama prepare () sul valore restituito ogni volta, fornendo la connessione come argomento, in modo che possa fare la quotazione corretta.
Alex Gaynor

@Alex: quale sarebbe il modo corretto di fare una citazione corretta con psycopg? (oltre a chiamare prepare () sul valore di ritorno, che sembri implicare non è ottimale)
albertov

Scusa, penso che il mio fraseggio fosse pessimo, fintanto che chiami obj.prepare (connessione) dovresti stare bene. Questo perché le API "buone" fornite da libpq per le virgolette richiedono la connessione (e fornisce cose come la codifica per le stringhe Unicode).
Alex Gaynor

1
Grazie. Ho provato a chiamare preparesul valore di ritorno, ma è sembra che non ha quel metodo: AttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'. Sto usando psycopg2 2.2.1 BTW
albertov

18

Per il backend MySQLdb ho modificato un po 'la fantastica risposta di albertov (grazie mille!). Sono sicuro che potrebbero essere uniti per verificare se lo comp.positionalerano, Truema questo è leggermente oltre lo scopo di questa domanda.

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)

Eccezionale! Avevo solo bisogno che l'elenco dei parametri associati venisse inviato a MySQL e la modifica di quanto sopra return tuple(params)funzionasse a meraviglia! Mi hai risparmiato innumerevoli ore di dover percorrere una strada estremamente dolorosa.
horcle_buzz

15

Il fatto è che sqlalchemy non combina mai i dati con la tua query. La query e i dati vengono passati separatamente al driver del database sottostante: l'interpolazione dei dati avviene nel database.

Sqlalchemy passa la query come hai visto str(myquery)al database e i valori andranno in una tupla separata.

Potresti usare un approccio in cui interpolare i dati con la query da solo (come suggerito da albertov di seguito), ma non è la stessa cosa che sqlalchemy sta eseguendo.


perché non è la stessa cosa? Capisco che l'API DB stia effettuando transazioni, forse riordinando le query, ecc., Ma potrebbe modificare la mia query più di questo?
cce

1
@cce: stai cercando di trovare la query finale. SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC È l'ultima domanda. Questi %svengono inviati al database da sqlalchemy - sqlalchemy non inserisce MAI i dati effettivi al posto di% s
nosklo

@cce: Alcuni moduli dbapi non lo fanno neanche - questo è spesso fatto dal database stesso
nosklo

1
aha capisco cosa stai dicendo, grazie - scavando più a fondo sqlalchemy.dialects.mysql.mysqldb, do_executemany()passa separatamente l'istruzione e i parametri al cursore MySQLdb. yay indirection!
cce

11

Per prima cosa, lasciatemi una prefazione dicendo che presumo che lo stiate facendo principalmente per scopi di debug: non consiglierei di provare a modificare l'istruzione al di fuori dell'API fluente di SQLAlchemy.

Sfortunatamente non sembra esserci un modo semplice per mostrare l'istruzione compilata con i parametri di query inclusi. SQLAlchemy non inserisce effettivamente i parametri nell'istruzione, ma vengono passati al motore di database come dizionario . Ciò consente alla libreria specifica del database di gestire cose come l'escape di caratteri speciali per evitare l'iniezione SQL.

Ma puoi farlo in due fasi abbastanza facilmente. Per ottenere la dichiarazione, puoi fare come hai già mostrato e stampare semplicemente la query:

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

Puoi fare un passo avanti con query.statement, per vedere i nomi dei parametri. Nota :id_1sotto e %ssopra: non è un problema in questo esempio molto semplice, ma potrebbe essere la chiave in un'affermazione più complicata.

>>> 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;

Quindi, puoi ottenere i valori effettivi dei parametri ottenendo la paramsproprietà dell'istruzione compilata:

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

Questo ha funzionato almeno per un backend MySQL; Mi aspetto che sia anche abbastanza generale per PostgreSQL senza bisogno di usarlo psycopg2.


Dall'interno del debugger PyCharm, quanto segue ha funzionato per me ... qry.compile (). Params
Ben

Interessante, potrebbe essere che SQLAlchemy sia leggermente cambiato da quando ho scritto questa risposta.
Hannele

10

Per il backend postgresql che utilizza psycopg2, è possibile ascoltare l' do_executeevento, quindi utilizzare il cursore, l'istruzione e digitare i parametri forzati insieme a Cursor.mogrify()per incorporare i parametri. È possibile restituire True per impedire l'esecuzione effettiva della query.

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

Utilizzo del campione:

>>> 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"}}'}

4

La seguente soluzione utilizza SQLAlchemy Expression Language e funziona con SQLAlchemy 1.1. Questa soluzione non combina i parametri con la query (come richiesto dall'autore originale), ma fornisce un modo di utilizzare i modelli SQLAlchemy per generare stringhe di query SQL e dizionari di parametri per diversi dialetti SQL. L'esempio è basato sul tutorial http://docs.sqlalchemy.org/en/rel_1_0/core/tutorial.html

Data 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())

possiamo produrre un'istruzione di query utilizzando la funzione select .

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

Successivamente, possiamo compilare l'istruzione in un oggetto query.

query = statement.compile()

Per impostazione predefinita, l'istruzione viene compilata utilizzando un'implementazione "denominata" di base compatibile con i database SQL come SQLite e Oracle. Se hai bisogno di specificare un dialetto come PostgreSQL, puoi farlo

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

Oppure, se vuoi specificare esplicitamente il dialetto come SQLite, puoi cambiare il paramstyle da "qmark" a "named".

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

Dall'oggetto query, possiamo estrarre la stringa della query e i parametri della query

query_str = str(query)
query_params = query.params

e infine eseguire la query.

conn.execute( query_str, query_params )

In che modo questa risposta è migliore / diversa da quella di AndyBarr pubblicata 2 anni prima?
Piotr Dobrogost

La risposta di AndyBarr include un esempio di generazione di un'istruzione di query con una DBSession mentre questa risposta include un esempio che utilizza l'API dichiarativa e il metodo di selezione. Per quanto riguarda la compilazione della dichiarazione di query con un certo dialetto, le risposte sono le stesse. Uso SQLAlchemy per generare query non elaborate e quindi eseguirle con l'adbapi di Twister. Per questo caso d'uso, è utile sapere come compilare la query senza una sessione ed estrarre la stringa ei parametri della query.
eric

3

È possibile utilizzare eventi della famiglia ConnectionEvents : after_cursor_executeo before_cursor_execute.

In sqlalchemy UsageRecipes di @zzzeek puoi trovare questo esempio:

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)
...

Qui puoi accedere al tuo estratto conto


2

Quindi, mettendo insieme un sacco di piccole parti di queste diverse risposte, ho trovato ciò di cui avevo bisogno: un semplice set di codice da inserire e occasionalmente ma in modo affidabile (cioè gestisce tutti i tipi di dati) afferrare l'esatto SQL compilato inviato al mio Backend Postgres semplicemente interrogando la query stessa:

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())

0

Penso che .statement potrebbe fare il trucco: 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

L'istruzione non mostra quali sono i parametri, se sono stati impostati alcuni tipi di filtri.
Hannele
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.