SQLAlchemy: stampa la query effettiva


165

Mi piacerebbe davvero poter stampare un codice SQL valido per la mia applicazione, inclusi i valori, anziché i parametri di bind, ma non è ovvio come farlo in SQLAlchemy (in base alla progettazione, ne sono abbastanza sicuro).

Qualcuno ha risolto questo problema in modo generale?


1
Non l'ho fatto, ma probabilmente potresti creare una soluzione meno fragile toccando il sqlalchemy.engineregistro di SQLAlchemy . Registra query e parametri di bind, dovresti solo sostituire i segnaposto di bind con i valori su una stringa di query SQL facilmente costruita.
Simon,

@Simon: ci sono due problemi con l'utilizzo del logger: 1) viene stampato solo quando viene eseguita un'istruzione 2) Dovrei comunque fare una sostituzione di stringa, tranne che in quel caso, non saprei esattamente la stringa bind-template e dovrei in qualche modo analizzarlo dal testo della query, rendendo la soluzione più fragile.
bukzor,

Il nuovo URL sembra essere docs.sqlalchemy.org/en/latest/faq/… per le FAQ di @ zzzeek.
Jim DeLaHunt,

Risposte:


168

Nella stragrande maggioranza dei casi, la "stringa" di un'istruzione o una query SQLAlchemy è semplice come:

print str(statement)

Questo vale sia per un ORM Queryche per qualsiasi select()altra istruzione.

Nota : la seguente risposta dettagliata viene mantenuta nella documentazione di sqlalchemy .

Per ottenere l'istruzione come compilata per un dialetto o un motore specifico, se l'istruzione stessa non è già associata a una, è possibile passarla a compile () :

print statement.compile(someengine)

o senza motore:

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

Quando viene assegnato un Queryoggetto ORM , per ottenere il compile()metodo è necessario prima accedere solo all'accessorio .statement :

statement = query.statement
print statement.compile(someengine)

per quanto riguarda la clausola originale secondo cui i parametri associati devono essere "incorporati" nella stringa finale, la sfida qui è che SQLAlchemy normalmente non ha questo compito, poiché questo è gestito in modo appropriato da Python DBAPI, per non parlare del bypassare i parametri associati è probabilmente le falle di sicurezza più ampiamente sfruttate nelle moderne applicazioni web. SQLAlchemy ha una capacità limitata di eseguire questa stringa in determinate circostanze come quella dell'emissione DDL. Per accedere a questa funzionalità è possibile utilizzare il flag 'literal_binds', passato a compile_kwargs:

from sqlalchemy.sql import table, column, select

t = table('t', column('x'))

s = select([t]).where(t.c.x == 5)

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

l'approccio sopra ha le avvertenze che è supportato solo per tipi di base, come ints e stringhe, e inoltre se un valore bindparam senza un valore preimpostato viene utilizzato direttamente, non sarà in grado di precisare neanche quello.

Per supportare il rendering letterale inline per tipi non supportati, implementare a TypeDecoratorper il tipo di destinazione che include un TypeDecorator.process_literal_parammetodo:

from sqlalchemy import TypeDecorator, Integer


class MyFancyType(TypeDecorator):
    impl = Integer

    def process_literal_param(self, value, dialect):
        return "my_fancy_formatting(%s)" % value

from sqlalchemy import Table, Column, MetaData

tab = Table('mytable', MetaData(), Column('x', MyFancyType()))

print(
    tab.select().where(tab.c.x > 5).compile(
        compile_kwargs={"literal_binds": True})
)

produrre output come:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)

2
Questo non mette le virgolette attorno alle stringhe e non risolve alcuni parametri associati.
bukzor,

1
la seconda metà della risposta è stata aggiornata con le ultime informazioni.
zzzeek,

2
@zzzeek Perché le query di stampa carina non sono incluse in sqlalchemy per impostazione predefinita? Come query.prettyprint(). Allevia immensamente il dolore del debug con grandi query.
Jmagnusson,

2
@jmagnusson perché la bellezza è negli occhi di chi guarda :) Ci sono ampi hook (ad es. evento cursor_execute, filtri di registrazione Python @compiles, ecc.) per qualsiasi numero di pacchetti di terze parti per implementare sistemi di stampa piuttosto.
zzzeek,

1
@buzkor re: limite fissato in 1.0 bitbucket.org/zzzeek/sqlalchemy/issue/3034/…
zzzeek

66

Funziona con Python 2 e 3 ed è un po 'più pulito di prima, ma richiede SA> = 1.0.

from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.sql.sqltypes import String, DateTime, NullType

# python2/3 compatible.
PY3 = str is not bytes
text = str if PY3 else unicode
int_type = int if PY3 else (int, long)
str_type = str if PY3 else (str, unicode)


class StringLiteral(String):
    """Teach SA how to literalize various things."""
    def literal_processor(self, dialect):
        super_processor = super(StringLiteral, self).literal_processor(dialect)

        def process(value):
            if isinstance(value, int_type):
                return text(value)
            if not isinstance(value, str_type):
                value = text(value)
            result = super_processor(value)
            if isinstance(result, bytes):
                result = result.decode(dialect.encoding)
            return result
        return process


class LiteralDialect(DefaultDialect):
    colspecs = {
        # prevent various encoding explosions
        String: StringLiteral,
        # teach SA about how to literalize a datetime
        DateTime: StringLiteral,
        # don't format py2 long integers to NULL
        NullType: StringLiteral,
    }


def literalquery(statement):
    """NOTE: This is entirely insecure. DO NOT execute the resulting strings."""
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        statement = statement.statement
    return statement.compile(
        dialect=LiteralDialect(),
        compile_kwargs={'literal_binds': True},
    ).string

demo:

# coding: UTF-8
from datetime import datetime
from decimal import Decimal

from literalquery import literalquery


def test():
    from sqlalchemy.sql import table, column, select

    mytable = table('mytable', column('mycol'))
    values = (
        5,
        u'snowman: ☃',
        b'UTF-8 snowman: \xe2\x98\x83',
        datetime.now(),
        Decimal('3.14159'),
        10 ** 20,  # a long integer
    )

    statement = select([mytable]).where(mytable.c.mycol.in_(values)).limit(1)
    print(literalquery(statement))


if __name__ == '__main__':
    test()

Fornisce questo output: (testato in Python 2.7 e 3.4)

SELECT mytable.mycol
FROM mytable
WHERE mytable.mycol IN (5, 'snowman: ☃', 'UTF-8 snowman: ☃',
      '2015-06-24 18:09:29.042517', 3.14159, 100000000000000000000)
 LIMIT 1

2
Questo è fantastico ... Dovremo aggiungere questo ad alcune librerie di debug in modo che possiamo accedervi facilmente. Grazie per fare il footwork su questo. Sono sorpreso che debba essere così complicato.
Corey O.

5
Sono abbastanza sicuro che questo sia intenzionalmente difficile, perché i principianti sono tentati di cursore.execute () quella stringa. Tuttavia, il principio del consenso degli adulti è comunemente usato in Python.
bukzor,

Molto utile. Grazie!
Clime

Davvero molto bello. Ho preso la libertà e incorporati in questo stackoverflow.com/a/42066590/2127439 , che copre SQLAlchemy v0.7.9 - v1.1.15, tra INSERT e UPDATE (PY2 / PY3).
Wolfmanx,

molto bella. ma sta convertendo come di seguito. 1) query (Table) .filter (Table.Column1.is_ (False) to WHERE Column1 IS 0. 2) query (Table) .filter (Table.Column1.is_ (True) to WHERE Column1 IS 1. 3) query ( Tabella) .filter (Table.Column1 == func.any ([1,2,3])) a WHERE Column1 = any ('[1,2,3]') sopra le conversioni non sono corretti nella sintassi.
Sekhar C,

52

Dato che ciò che si desidera ha senso solo durante il debug, è possibile avviare SQLAlchemy echo=Trueper registrare tutte le query SQL. Per esempio:

engine = create_engine(
    "mysql://scott:tiger@hostname/dbname",
    encoding="latin1",
    echo=True,
)

Questo può anche essere modificato per una sola richiesta:

echo=False- in caso Trueaffermativo, il motore registrerà tutte le istruzioni nonché una repr()delle loro liste di parametri nel logger del motore, che per impostazione predefinita è sys.stdout. L' echoattributo di Enginepuò essere modificato in qualsiasi momento per attivare e disattivare la registrazione. Se impostato sulla stringa "debug", anche le righe dei risultati verranno stampate sullo standard output. Questo flag alla fine controlla un logger Python; vedere Configurazione della registrazione per informazioni su come configurare direttamente la registrazione.

Fonte: Configurazione del motore SQLAlchemy

Se usato con Flask, puoi semplicemente impostare

app.config["SQLALCHEMY_ECHO"] = True

per ottenere lo stesso comportamento.


6
Questa risposta merita di essere molto più alta .. e per gli utenti di flask-sqlalchemyquesto dovrebbe essere la risposta accettata.
jso,

25

Possiamo usare il metodo di compilazione per questo scopo. Dai documenti :

from sqlalchemy.sql import text
from sqlalchemy.dialects import postgresql

stmt = text("SELECT * FROM users WHERE users.name BETWEEN :x AND :y")
stmt = stmt.bindparams(x="m", y="z")

print(stmt.compile(dialect=postgresql.dialect(),compile_kwargs={"literal_binds": True}))

Risultato:

SELECT * FROM users WHERE users.name BETWEEN 'm' AND 'z'

Avviso dai documenti:

Non utilizzare mai questa tecnica con il contenuto di stringhe ricevuto da input non attendibile, 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 sicure contro l'input non attendibile e non convalidano il tipo di dati trasmessi. Utilizzare sempre i parametri associati quando si richiama a livello di codice istruzioni SQL non DDL su un database relazionale.


13

Quindi, basandomi sui commenti di @ zzzeek sul codice di @ bukzor, ho pensato a questo per ottenere facilmente una query "piuttosto stampabile":

def prettyprintable(statement, dialect=None, reindent=True):
    """Generate an SQL expression string with bound parameters rendered inline
    for the given SQLAlchemy statement. The function can also receive a
    `sqlalchemy.orm.Query` object instead of statement.
    can 

    WARNING: Should only be used for debugging. Inlining parameters is not
             safe when handling user created data.
    """
    import sqlparse
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        if dialect is None:
            dialect = statement.session.get_bind().dialect
        statement = statement.statement
    compiled = statement.compile(dialect=dialect,
                                 compile_kwargs={'literal_binds': True})
    return sqlparse.format(str(compiled), reindent=reindent)

Personalmente ho difficoltà a leggere il codice che non è rientrato, quindi ho usato sqlparseper reindirizzare l'SQL. Può essere installato con pip install sqlparse.


@bukzor Tutti i valori funzionano tranne datatime.now()quello quando si usa python 3 + sqlalchemy 1.0. Dovresti seguire i consigli di @ zzzeek sulla creazione di un TypeDecorator personalizzato affinché anche quello funzioni.
jmagnusson,

È un po 'troppo specifico. Il datetime non funziona in nessuna combinazione di pitone e sqlalchemy. Inoltre, in py27, l'unicode non ascii provoca un'esplosione.
bukzor,

Per quanto ho potuto vedere, il percorso TypeDecorator mi richiede di modificare le definizioni della mia tabella, il che non è un requisito ragionevole per vedere semplicemente le mie query. Ho modificato la mia risposta per essere un po 'più vicina alla tua e a quella di zzzeek, ​​ma ho preso la strada di un dialetto personalizzato, che è propriamente ortogonale alle definizioni della tabella.
bukzor,

11

Questo codice si basa sulla brillante risposta esistente di @bukzor. Ho appena aggiunto il rendering personalizzato per il datetime.datetimetipo in Oracle TO_DATE().

Sentiti libero di aggiornare il codice per adattarlo al tuo database:

import decimal
import datetime

def printquery(statement, bind=None):
    """
    print a query, with values filled in
    for debugging purposes *only*
    for security, you should always separate queries from their values
    please also note that this function is quite slow
    """
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        if bind is None:
            bind = statement.session.get_bind(
                    statement._mapper_zero_or_none()
            )
        statement = statement.statement
    elif bind is None:
        bind = statement.bind 

    dialect = bind.dialect
    compiler = statement._compiler(dialect)
    class LiteralCompiler(compiler.__class__):
        def visit_bindparam(
                self, bindparam, within_columns_clause=False, 
                literal_binds=False, **kwargs
        ):
            return super(LiteralCompiler, self).render_literal_bindparam(
                    bindparam, within_columns_clause=within_columns_clause,
                    literal_binds=literal_binds, **kwargs
            )
        def render_literal_value(self, value, type_):
            """Render the value of a bind parameter as a quoted literal.

            This is used for statement sections that do not accept bind paramters
            on the target driver/database.

            This should be implemented by subclasses using the quoting services
            of the DBAPI.

            """
            if isinstance(value, basestring):
                value = value.replace("'", "''")
                return "'%s'" % value
            elif value is None:
                return "NULL"
            elif isinstance(value, (float, int, long)):
                return repr(value)
            elif isinstance(value, decimal.Decimal):
                return str(value)
            elif isinstance(value, datetime.datetime):
                return "TO_DATE('%s','YYYY-MM-DD HH24:MI:SS')" % value.strftime("%Y-%m-%d %H:%M:%S")

            else:
                raise NotImplementedError(
                            "Don't know how to literal-quote value %r" % value)            

    compiler = LiteralCompiler(dialect, statement)
    print compiler.process(statement)

22
Non vedo perché la gente del SA crede che sia ragionevole per un'operazione così semplice essere così difficile .
bukzor,

Grazie! render_literal_value ha funzionato bene per me. Il mio unico cambiamento è stato: return "%s" % valueinvece che return repr(value)nella sezione float, int, long perché Python stava producendo long come 22Linvece che solo22
OrganicPanda

Questa ricetta (così come l'originale) genera UnicodeDecodeError se un valore di stringa bindparam non è rappresentabile in ascii. Ho pubblicato una sintesi che risolve questo problema.
gsakkis,

1
"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")in mysql
Zitrax

1
@bukzor - Non ricordo di avermi chiesto se quanto sopra è "ragionevole", quindi non puoi davvero affermare che io "credo" che lo sia - FWIW, non lo è! :) vedi la mia risposta.
zzzeek,

8

Vorrei sottolineare che le soluzioni sopra indicate non "funzionano" con query non banali. Un problema che ho riscontrato sono stati tipi più complicati, come ARRAY pgsql che causano problemi. Ho trovato una soluzione che per me ha funzionato anche con pgsql ARRAY:

preso in prestito da: https://gist.github.com/gsakkis/4572159

Il codice collegato sembra essere basato su una versione precedente di SQLAlchemy. Verrà visualizzato un errore che indica che l'attributo _mapper_zero_or_none non esiste. Ecco una versione aggiornata che funzionerà con una versione più recente, è sufficiente sostituire _mapper_zero_or_none con bind. Inoltre, questo supporta gli array pgsql:

# adapted from:
# https://gist.github.com/gsakkis/4572159
from datetime import date, timedelta
from datetime import datetime

from sqlalchemy.orm import Query


try:
    basestring
except NameError:
    basestring = str


def render_query(statement, dialect=None):
    """
    Generate an SQL expression string with bound parameters rendered inline
    for the given SQLAlchemy statement.
    WARNING: This method of escaping is insecure, incomplete, and for debugging
    purposes only. Executing SQL statements with inline-rendered user values is
    extremely insecure.
    Based on http://stackoverflow.com/questions/5631078/sqlalchemy-print-the-actual-query
    """
    if isinstance(statement, Query):
        if dialect is None:
            dialect = statement.session.bind.dialect
        statement = statement.statement
    elif dialect is None:
        dialect = statement.bind.dialect

    class LiteralCompiler(dialect.statement_compiler):

        def visit_bindparam(self, bindparam, within_columns_clause=False,
                            literal_binds=False, **kwargs):
            return self.render_literal_value(bindparam.value, bindparam.type)

        def render_array_value(self, val, item_type):
            if isinstance(val, list):
                return "{%s}" % ",".join([self.render_array_value(x, item_type) for x in val])
            return self.render_literal_value(val, item_type)

        def render_literal_value(self, value, type_):
            if isinstance(value, long):
                return str(value)
            elif isinstance(value, (basestring, date, datetime, timedelta)):
                return "'%s'" % str(value).replace("'", "''")
            elif isinstance(value, list):
                return "'{%s}'" % (",".join([self.render_array_value(x, type_.item_type) for x in value]))
            return super(LiteralCompiler, self).render_literal_value(value, type_)

    return LiteralCompiler(dialect, statement).process(statement)

Testato su due livelli di array nidificati.


Si prega di mostrare un esempio di come usarlo? Grazie
slashdottir il

from file import render_query; print(render_query(query))
Alfonso Pérez,

Questo è l'unico esempio di questa intera pagina che ha funzionato per me! Grazie !
Fougerejo
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.