SQLAlchemy: роздрукувати фактичний запит


165

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

Хтось вирішив цю проблему загальним способом?


1
Я цього не зробив, але ви, мабуть, могли б створити менш крихке рішення, натиснувши в sqlalchemy.engineжурнал SQLAlchemy . Він реєструє запити та параметри прив'язки, вам потрібно буде лише замінити заповнювачі заповнення на значення зі зручно побудованої рядки запитів SQL.
Саймон

@Simon: є дві проблеми з використанням реєстратора: 1) він друкується лише тоді, коли виконується оператор 2) мені все одно доведеться замінити рядок, за винятком випадків, я б точно не знав рядок bind-template. , і мені доведеться якось розбирати його з тексту запиту, роблячи рішення більш крихким.
bukzor

Нова URL-адреса виглядає як docs.sqlalchemy.org/en/latest/faq/… для поширених запитань @ zzzeek.
Jim DeLaHunt

Відповіді:


168

У переважній більшості випадків "строфікація" оператора або запиту SQLAlchemy така ж проста, як:

print str(statement)

Це стосується як ORM, Queryтак і будь-якого select()чи іншого твердження.

Примітка . Наступна детальна відповідь зберігається в документації sqlalchemy .

Щоб отримати оператор як скомпільований на певному діалекті чи двигуні, якщо висловлювання вже не пов'язане з одним, ви можете передати це у компілювати () :

print statement.compile(someengine)

або без двигуна:

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

Коли нам дають Queryоб'єкт ORM , для того, щоб отримати compile()метод, нам потрібен лише спочатку доступ до доступу .statement :

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

що стосується початкового положення про те, що зв'язані параметри мають бути "вкладені" в остаточний рядок, то проблема тут полягає в тому, що SQLAlchemy зазвичай не ставить перед собою завдання, оскільки це обробляється належним чином Python DBAPI, не кажучи вже про обхід пов'язаних параметрів. мабуть, найбільш широко використовувані дірки в безпеці в сучасних веб-додатках. 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})

вищевказаний підхід містить застереження, що він підтримується лише для базових типів, таких як ints та рядки, і, крім того, якщо значення 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)

2
Це не ставить лапки навколо рядків і не вирішує деякі зв'язані параметри.
bukzor

1
другу половину відповіді оновлено останньою інформацією.
zzzeek

2
@zzzeek Чому запити про гарне друкування не включені до sqlalchemy за замовчуванням? Як query.prettyprint(). Це дуже полегшує біль на налагодження з великими запитами.
jmagnusson

2
@jmagnusson, оскільки краса в очах спостерігача :) Є достатньо гачків (наприклад, подія cursor_execute, фільтри реєстрації Python @compilesтощо) для будь-якої кількості сторонніх пакетів для впровадження симпатичних систем друку.
zzzeek

1
@buzkor re: ліміт, який було зафіксовано у 1,0 bitbucket.org/zzzeek/sqlalchemy/issue/3034/…
zzzeek

66

Це працює в 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

2
Це приголомшливо ... Доведеться додати це до деяких налагоджувальних ліфтів, щоб ми могли легко отримати доступ до нього. Дякуємо за те, що ви працювали на цьому. Я вражений, що це мало бути таким складним.
Корі О.

5
Я майже впевнений, що це навмисно важко, тому що новачки спокушаються cursor.execute () цю рядок. Принцип згоди на дорослих зазвичай використовується в пітоні.
bukzor

Дуже корисний. Дякую!
clime

Справді дуже приємно. Я взяв на себе сміливість і включив це в stackoverflow.com/a/42066590/2127439 , який охоплює SQLAlchemy v0.7.9 - v1.1.15, включаючи заяви INSERT та UPDATE (PY2 / PY3).
wolfmanx

дуже хороша. але це перетворення, як показано нижче. 1) запит (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 ( Таблиця) .filter (Table.Column1 == func.any ([1,2,3])) WHERE Стовпець1 = будь-який ('[1,2,3]') вище конверсій неправильний у синтаксисі.
Сехар С

51

Зважаючи на те, що те, що ви хочете, має сенс лише при налагодженні, ви можете запустити 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

Якщо використовується з колбою, ви можете просто встановити

app.config["SQLALCHEMY_ECHO"] = True

щоб отримати таку саму поведінку.


6
Ця відповідь заслуговує на те, щоб бути набагато вищою. І для користувачів flask-sqlalchemyце має бути прийнятою відповіддю.
jso

25

Для цього ми можемо використовувати метод компіляції . З документів :

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'

Попередження від документів:

Ніколи не використовуйте цю техніку з рядковим вмістом, отриманим з ненадійного введення, наприклад, з веб-форм або інших програм для введення користувачем. Можливості SQLAlchemy для примусового значення Python у прямі значення рядка SQL не захищені від ненадійного введення та не підтверджують тип переданих даних. Завжди використовуйте прив'язані параметри, коли програмно викликати оператори SQL, що не є DDL, проти реляційної бази даних.


13

Отже, спираючись на коментарі @ 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.


@bukzor Всі значення працюють, крім datatime.now()одного, коли використовується python 3 + sqlalchemy 1.0. Вам доведеться дотримуватися порад @ zzzeek щодо створення користувальницького TypeDecorator для того, щоб він також працював.
jmagnusson

Це трохи занадто конкретно. Час дати не працює в жодному поєднанні пітона та пллалхімії. Також, у py27, uniccii unicode викликає вибух.
bukzor

Наскільки я міг бачити, маршрут TypeDecorator вимагає від мене зміни моїх таблиць, що не є розумною вимогою просто бачити мої запити. Я відредагував свою відповідь, щоб бути трохи ближче до вашої та zzzeek, ​​але я взяв маршрут власного діалекту, який є ортогональним до визначень таблиці.
bukzor

11

Цей код заснований на блискучій відповіді від @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)

22
Я не бачу, чому народ, який вважає С.А., вважає, що така проста операція є такою важкою .
bukzor

Дякую! render_literal_value добре працював для мене. Єдиною моєю зміною було: return "%s" % valueзамість return repr(value)float, int, довгий розділ, тому що Python виводив longs, 22Lа не просто22
OrganicPanda

Цей рецепт (як і оригінал) підвищує UnicodeDecodeError, якщо будь-яке значення рядка bindparam не відображається в ascii. Я розмістив суть, яка це виправляє.
gsakkis

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

1
@bukzor - Я не пам'ятаю, щоб мене запитували, чи сказане вище "розумне", тому ви не можете реально стверджувати, що я "вірю", що це - FWIW, це не так! :) будь ласка, дивіться мою відповідь.
zzzeek

8

Я хотів би зазначити, що рішення, наведені вище, не «працюють» лише з нетривіальними запитами. Одне питання, на яке я натрапив, були складнішими типами, наприклад, PGSQL ARRAY, що викликає проблеми. Я знайшов рішення, яке для мене просто працювало навіть з pgsql ARRAY:

запозичено у: https://gist.github.com/gsakkis/4572159

Здається, пов'язаний код базується на більш старій версії SQLAlchemy. Ви отримаєте помилку про те, що атрибут _mapper_zero_or_none не існує. Ось оновлена ​​версія, яка буде працювати з більш новою версією, ви просто заміните _mapper_zero_or_none на прив'язку. Крім того, ця підтримка має масиви 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)

Тестується на двох рівнях вкладених масивів.


Покажіть, будь ласка, приклад, як ним користуватися? Дякую
slashdottir

from file import render_query; print(render_query(query))
Альфонсо Перес

Це єдиний приклад цілої сторінки, яка працювала на мене! Дякую !
fougerejo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.