¿Cómo obtengo una consulta SQL compilada y sin procesar a partir de una expresión SQLAlchemy?

103

Tengo un objeto de consulta SQLAlchemy y quiero obtener el texto de la declaración SQL compilada, con todos sus parámetros vinculados (por ejemplo, ninguna %su otras variables esperando ser vinculadas por el compilador de declaraciones o el motor de dialecto MySQLdb, etc.).

Invocar str()la consulta revela algo como esto:

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

Intenté buscar en query._params pero es un dict vacío. Escribí mi propio compilador usando este ejemplo del sqlalchemy.ext.compiler.compilesdecorador, pero incluso la declaración allí todavía tiene %sdónde quiero datos.

No puedo averiguar cuándo se mezclan mis parámetros para crear la consulta; al examinar el objeto de consulta, siempre son un diccionario vacío (aunque la consulta se ejecuta bien y el motor lo imprime cuando activa el registro de eco).

Estoy empezando a recibir el mensaje de que SQLAlchemy no quiere que conozca la consulta subyacente, ya que rompe la naturaleza general de la interfaz de la API de expresión en todas las diferentes DB-API. No me importa si la consulta se ejecuta antes de saber cuál era; ¡Sólo quiero saber!

cce
fuente

Respuestas:

106

Este blog proporciona una respuesta actualizada.

Citando la publicación del blog, esto se sugiere y funcionó para mí.

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

Donde q se define como:

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

O cualquier tipo de session.query ().

¡Gracias a Nicolas Cadou por la respuesta! Espero que ayude a otros que vienen a buscar aquí.

AndyBarr
fuente
2
¿Existe una manera fácil de obtener los valores como diccionario?
Damien
6
@Damien dado c = q.statement.compile(...), puedes conseguirloc.params
Hannele
1
La publicación está etiquetada con mysql, por lo que los detalles de postgresql en esta respuesta no son realmente relevantes.
Hannele
4
Si entiendo el OP correctamente, quiere la consulta final. Imprimir con la especificación de un dialecto (aquí postgres) todavía me da los marcadores de posición en lugar de los valores literales. La respuesta de @ Matt hace el trabajo. Obtener el SQL con marcadores de posición se puede lograr de forma más sencilla con el as_scalar()método -de Query.
Patrick B.
1
@PatrickB. Estoy de acuerdo. La respuesta de Matt debe considerarse la respuesta "correcta". Obtengo el mismo resultado que este con solo hacer str(q).
André C. Andersen
91

La documentación utiliza literal_bindspara imprimir una consulta que qincluye parámetros:

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

el enfoque anterior tiene las advertencias de que solo es compatible con tipos básicos, como ints y strings, y además, si un bindparam () sin un valor preestablecido se usa directamente, tampoco podrá clasificar eso.

La documentación también emite esta advertencia:

Nunca use esta técnica con contenido de cadena recibido de entradas no confiables, como formularios web u otras aplicaciones de entrada de usuarios. Las facilidades de SQLAlchemy para convertir los valores de Python en valores de cadena SQL directos no son seguras contra entradas no confiables y no validan el tipo de datos que se pasan. Utilice siempre parámetros enlazados cuando invoque mediante programación sentencias SQL que no sean DDL en una base de datos relacional.

Mate
fuente
¡Gracias! ¡Esto fue extremadamente útil, me permitió usar la función pandas read_sql sin dolor!
Justin Palmer
24

Esto debería funcionar 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)
albertov
fuente
2
¡Gracias por esto! Lamentablemente, estoy usando MySQL, por lo que mi dialecto es "posicional" y necesita tener una lista de parámetros en lugar de un diccionario. Actualmente tratando de hacer que su ejemplo funcione con eso ..
cce
No lo utilice adaptde esta manera. Como mínimo, llame a prepare () en el valor de retorno de él cada vez, proporcionando la conexión como un argumento, para que pueda hacer las citas adecuadas.
Alex Gaynor
@Alex: ¿Cuál sería la forma correcta de cotizar correctamente con psycopg? (además de llamar a prepare () en el valor de retorno, que parece implicar que no es óptimo)
albertov
Lo siento, creo que mi redacción fue mala, siempre que llames a obj.prepare (conexión), deberías estar bien. Esto se debe a que las API "buenas" que libpq proporciona para las citas requieren la conexión (y proporciona cosas como la codificación para cadenas Unicode).
Alex Gaynor
1
Gracias. He tratado de llamar prepareen el valor de retorno, sino que se parece que no tiene ese método: AttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'. Estoy usando psycopg2 2.2.1 BTW
albertov
18

Para el backend de MySQLdb modifiqué un poco la increíble respuesta de albertov (¡muchas gracias!). Estoy seguro de que podrían fusionarse para verificar si lo comp.positionalfue, Truepero eso está un poco más allá del alcance de esta pregunta.

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
fuente
¡Increíble! ¡Solo necesitaba que la lista de parámetros enlazados se enviara a MySQL y modificara lo anterior para que return tuple(params)funcionara como un encanto! Me salvaste incontables horas de tener que recorrer un camino extremadamente doloroso.
horcle_buzz
15

La cosa es que sqlalchemy nunca mezcla los datos con su consulta. La consulta y los datos se pasan por separado a su controlador de base de datos subyacente; la interpolación de datos ocurre en su base de datos.

Sqlalchemy pasa la consulta como ha visto en str(myquery)la base de datos, y los valores irán en una tupla separada.

Podría usar algún enfoque en el que interpole los datos con la consulta usted mismo (como albertov sugirió a continuación), pero eso no es lo mismo que está ejecutando sqlalchemy.

nosklo
fuente
¿Por qué no es lo mismo? Entiendo que DB-API está realizando transacciones, posiblemente reordenando consultas, etc., pero ¿podría estar modificando mi consulta más que esto?
cce
1
@cce: estás intentando encontrar la consulta final. SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC ES la consulta final. Esos %sson enviados a la base de datos por sqlalchemy - sqlalchemy NUNCA coloca los datos reales en lugar de% s
nosklo
@cce: Algunos módulos dbapi tampoco hacen eso, eso a menudo lo hace la propia base de datos
nosklo
1
Ajá, veo lo que está diciendo, gracias. Profundizando más sqlalchemy.dialects.mysql.mysqldb, do_executemany()pasa la declaración y los parámetros por separado al cursor MySQLdb. yay indirección!
cce
11

Primero permítame un prefacio diciendo que supongo que está haciendo esto principalmente con fines de depuración; no recomendaría intentar modificar la declaración fuera de la API fluida de SQLAlchemy.

Desafortunadamente, no parece haber una forma sencilla de mostrar la declaración compilada con los parámetros de consulta incluidos. SQLAlchemy en realidad no pone los parámetros en la declaración, se pasan al motor de la base de datos como un diccionario . Esto permite que la biblioteca específica de la base de datos maneje cosas como escapar de caracteres especiales para evitar la inyección de SQL.

Pero puede hacer esto en un proceso de dos pasos con razonable facilidad. Para obtener la declaración, puede hacer lo que ya ha mostrado y simplemente imprimir la consulta:

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

Puede acercarse un paso más con query.statement, para ver los nombres de los parámetros. Nota a :id_1continuación frente a %sarriba: no es realmente un problema en este ejemplo muy simple, pero podría ser clave en una declaración más complicada.

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

Luego, puede obtener los valores reales de los parámetros obteniendo la paramspropiedad de la declaración compilada:

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

Esto funcionó para un backend MySQL al menos; Esperaría que también sea lo suficientemente general para PostgreSQL sin necesidad de usar psycopg2.

Hannele
fuente
Desde dentro del depurador de PyCharm, lo siguiente funcionó para mí ... qry.compile (). Params
Ben
Interesante, podría ser que SQLAlchemy haya cambiado un poco desde que escribí esta respuesta.
Hannele
10

Para el backend de postgresql que usa psycopg2, puede escuchar el do_executeevento, luego usar el cursor, la declaración y escribir los parámetros forzados junto con Cursor.mogrify()para insertar los parámetros. Puede devolver True para evitar la ejecución real de la consulta.

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

Uso de muestra:

>>> 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"}}'}
rectalogica
fuente
4

La siguiente solución utiliza el lenguaje de expresión SQLAlchemy y funciona con SQLAlchemy 1.1. Esta solución no mezcla los parámetros con la consulta (como lo solicitó el autor original), pero proporciona una forma de utilizar modelos SQLAlchemy para generar cadenas de consulta SQL y diccionarios de parámetros para diferentes dialectos SQL. El ejemplo se basa en el tutorial. http://docs.sqlalchemy.org/en/rel_1_0/core/tutorial.html

Dada la clase,

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

podemos producir una declaración de consulta usando la función de selección .

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

A continuación, podemos compilar la declaración en un objeto de consulta.

query = statement.compile()

De forma predeterminada, la declaración se compila utilizando una implementación básica 'nombrada' que es compatible con bases de datos SQL como SQLite y Oracle. Si necesita especificar un dialecto como PostgreSQL, puede hacer

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

O si desea especificar explícitamente el dialecto como SQLite, puede cambiar el estilo de parámetro de 'qmark' a 'named'.

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

Desde el objeto de consulta, podemos extraer la cadena de consulta y los parámetros de consulta.

query_str = str(query)
query_params = query.params

y finalmente ejecutar la consulta.

conn.execute( query_str, query_params )
Eric
fuente
¿Cómo es esta respuesta mejor / diferente a la de AndyBarr publicada 2 años antes?
Piotr Dobrogost
La respuesta de AndyBarr incluye un ejemplo de cómo generar una declaración de consulta con una DBSession, mientras que esta respuesta incluye un ejemplo que usa la API declarativa y el método select. Con respecto a la compilación de la declaración de consulta con un determinado dialecto, las respuestas son las mismas. Utilizo SQLAlchemy para generar consultas sin procesar y luego ejecutarlas con adbapi de Twister. Para este caso de uso, es útil saber cómo compilar la consulta sin una sesión y extraer la cadena de consulta y los parámetros.
Eric
3

Puede utilizar eventos de la familia ConnectionEvents : after_cursor_executeo before_cursor_execute.

En sqlalchemy UsageRecipes de @zzzeek puede encontrar este ejemplo:

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

Aquí puede acceder a su estado de cuenta

Alex Bender
fuente
2

Entonces, juntando un montón de pequeños fragmentos de estas diferentes respuestas, se me ocurrió lo que necesitaba: un conjunto simple de código para ingresar y ocasionalmente pero de manera confiable (es decir, maneja todos los tipos de datos) tomar el SQL compilado exacto enviado a mi Postgres backend con solo interrogar la consulta en sí:

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
fuente
0

Creo que .statement posiblemente haría el truco: 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
usuario2757902
fuente
La declaración no le muestra cuáles son los parámetros, si tiene configurados algunos tipos de filtros.
Hannele