SQLAlchemy: вывести фактический запрос - PullRequest
128 голосов
/ 12 апреля 2011

Мне бы очень хотелось иметь возможность распечатывать действительный SQL для моего приложения, включая значения, а не параметры привязки, но не совсем очевидно, как это сделать в SQLAlchemy (я уверен, что это дизайн).

Кто-нибудь решил эту проблему в общем виде?

Ответы [ 7 ]

125 голосов
/ 23 мая 2014

В подавляющем большинстве случаев «stringification» оператора или запроса SQLAlchemy так же просто, как:

print str(statement)

Это относится как к ORM Query, так и к любому select()или другой оператор.

Примечание : следующий подробный ответ сохраняется в документации sqlalchemy .

Чтобы получить оператор как скомпилированный вконкретный диалект или механизм, если само утверждение еще не связано с одним, вы можете передать его в compile () :

print statement.compile(someengine)

или без движка:

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

При получении объекта ORM Query, чтобы получить метод compile(), нам нужен только доступ к элементу доступа .statement сначала:

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

в отношениик исходному условию, что связанные параметры должны быть «встроены» в конечную строку, проблема в том, что SQLAlchemy обычно не выполняет эту задачу, поскольку это обрабатывается соответствующим образом DBAPI Python, не говоря уже о том, что обход связанных параметровочевидно, наиболее широко используемые дыры в безопасности в современных веб-приложениях.SQLAlchemy имеет ограниченные возможности для выполнения этой последовательности при определенных обстоятельствах, таких как испускание DDL.Чтобы получить доступ к этой функциональности, можно использовать флаг literal_binds, передаваемый в 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})

. Приведенный выше подход имеет оговорки, что он поддерживается только для базовых типов, таких как int и strings.и, кроме того, если bindparam без предварительно заданного значения используется напрямую, он также не сможет его упорядочить.

Для поддержки встроенного рендеринга литералов для типов, которые не поддерживаются, реализуйте TypeDecoratorдля целевого типа, который включает в себя метод TypeDecorator.process_literal_param:

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

, выдающий вывод, например:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
57 голосов
/ 18 апреля 2011

Это работает в python 2 и 3 и немного чище, чем раньше, но требует 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

Демо-версия:

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

Дает этот вывод: (протестировано в python 2.7 и 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
19 голосов
/ 16 июня 2018

Учитывая, что то, что вы хотите, имеет смысл только при отладке, вы можете запустить SQLAlchemy с echo=True, чтобы регистрировать все запросы SQL.Например:

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

Это также можно изменить только для одного запроса:

echo=False - если True, Engine будет регистрировать все операторы, а такжеrepr() их списков параметров для регистратора двигателей, по умолчанию sys.stdout.Атрибут echo Engine может быть изменен в любое время для включения и выключения регистрации.Если задана строка "debug", строки результатов будут также выводиться на стандартный вывод.Этот флаг в конечном счете управляет регистратором Python;см. Настройка ведения журнала для получения информации о том, как напрямую настроить ведение журнала.

Источник: Конфигурация механизма SQLAlchemy

При использовании с Flaskможно просто установить

app.config["SQLALCHEMY_ECHO"] = True

, чтобы получить то же поведение.

12 голосов
/ 12 мая 2015

Поэтому, опираясь на комментарии @zzzeek к коду @ bukzor, я придумал это, чтобы легко получить «симпатичный для печати» запрос:

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)

Лично мне трудно читать код без отступапоэтому я использовал sqlparse, чтобы переопределить SQL.Может быть установлен с pip install sqlparse.

10 голосов
/ 07 августа 2017

Мы можем использовать метод компиляции для этой цели.Из документов :

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

Результат:

SELECT * FROM users WHERE users.name BETWEEN 'm' AND 'z'
10 голосов
/ 28 марта 2012

Этот код основан на блестящем существующем ответе от @bukzor.Я только что добавил пользовательский рендер для datetime.datetime типа в Oracle TO_DATE().

Не стесняйтесь обновлять код в соответствии с вашей базой данных:

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)
5 голосов
/ 25 сентября 2015

Я хотел бы отметить, что приведенные выше решения не "просто работают" с нетривиальными запросами. Одна проблема, с которой я столкнулся, была более сложные типы, такие как массивы pgsql, вызывающие проблемы. Я нашел решение, которое для меня, просто работал даже с массивами pgsql:

заимствовано из: https://gist.github.com/gsakkis/4572159

Похоже, что связанный код основан на более старой версии SQLAlchemy. Вы получите сообщение о том, что атрибут _mapper_zero_or_none не существует. Вот обновленная версия, которая будет работать с более новой версией, вы просто замените _mapper_zero_or_none на bind. Кроме того, здесь есть поддержка массивов 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 /5430530/sqlalchemy-vyvesti-fakticheskii-zapros
    """
    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)

Проверено на двух уровнях вложенных массивов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...