Як я можу отримати необроблений, складений SQL-запит із виразу SQLAlchemy?


103

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

Виклик str()запиту виявляє приблизно таке:

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

Я намагався шукати в query._params, але це порожній dict. Я написав власний компілятор, використовуючи цей приклад sqlalchemy.ext.compiler.compilesдекоратора, але навіть у заяві все ще є місця, %sде я хочу отримати дані.

Я не можу зрозуміти, коли мої параметри змішуються для створення запиту; при дослідженні об'єкта запиту вони завжди є порожнім словником (хоча запит виконується нормально, і механізм друкує його, коли ви вмикаєте реєстрацію ехо-сигналів).

Я починаю отримувати повідомлення про те, що SQLAlchemy не хоче, щоб я знав базовий запит, оскільки це порушує загальний характер інтерфейсу API виразу всіх різних DB-API. Я не проти, якщо запит виконується до того, як я дізнався, що це було; Я лише хочу знати!

Відповіді:


107

Цей блог пропонує оновлену відповідь.

Цитуючи повідомлення у блозі, це пропонується і працює для мене.

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

Де q визначається як:

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

Або просто будь-який тип session.query ().

Дякуємо Ніколя Каду за відповідь! Сподіваюся, це допоможе іншим, хто сюди шукає.


2
Чи є простий спосіб отримати значення як словник?
Демієн

6
@Damien даний c = q.statement.compile(...), ви можете просто отриматиc.params
Hannele

1
Публікація позначена тегом mysql, тому деталі postgresql у цій відповіді насправді не актуальні.
Ганнеле

4
Якщо я правильно розумію ОП, він хоче остаточного запиту. Друк із зазначенням діалекту (тут постгреси) все ще дає мені місця замість буквальних значень. @ Відповідь Метта робить роботу. Отримати SQL за допомогою заповнювачів можна простіше, досягнувши за допомогою as_scalar()-методу Query.
Патрік Б.

1
@PatrickB. Я згоден. Відповідь Метта слід вважати "правильною" відповіддю. Я отримую такий самий результат, як це, просто роблячи str(q).
Андре К. Андерсен

91

Документація використовується literal_bindsдля друку запиту , qвключаючи параметри:

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

наведений вище підхід має застереження, що він підтримується лише для базових типів, таких як ints та рядки, і крім того, якщо безпосередньо використовується bindparam () без попередньо встановленого значення, він також не зможе це строгіфікувати.

Документація також видає це попередження:

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


Дякую! Це було надзвичайно корисно, дозволило безболісно використовувати функцію pandas read_sql!
Джастін Палмер,

24

Це повинно працювати з 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
Дякую за це! На жаль, я використовую MySQL, тому мій діалект є "позиційним", і він повинен мати список парам, а не словник. В даний час намагаються змусити ваш приклад працювати з цим ..
cce

Будь ласка, не використовуйте adaptтаким чином. При мінімальному виклику щоразу готуйте () повернення від нього значення, надаючи з'єднання як аргумент, щоб воно могло правильно цитувати.
Алекс Гейнор

@Alex: Який би був правильний спосіб зробити правильне цитування з psycopg? (окрім дзвінка priprav () на повернене значення, яке, начебто, ви маєте на увазі, не є оптимальним)
albertov

Вибачте, я думаю, що моє словосполучення було поганим, якщо ви зателефонували obj.prepare (з'єднання), ви повинні бути в порядку. Це тому, що "хороші" API, які libpq передбачає для цитування, вимагають з'єднання (і вони надають такі речі, як кодування рядків Unicode).
Алекс Гейнор

1
Дякую. Я намагався додзвонитися prepareна яке значення , але , здається , що він не має такої метод: AttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'. Я використовую psycopg2 2.2.1 BTW
albertov

18

Для бекенду MySQLdb я трохи змінив чудову відповідь Альбертова (велике спасибі!). Я впевнений, що їх можна об'єднати, щоб перевірити, чи comp.positionalбуло, Trueале це трохи виходить за рамки цього питання.

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)

Дивовижно! Мені просто потрібно було пересланий список параметрів надіслати в MySQL і змінити вищезгаданий, щоб він return tuple(params)працював просто як шарм! Ти врятував мені незліченну кількість часу, коли треба спуститися надзвичайно болісною дорогою.
horcle_buzz

15

Справа в тому, що sqlalchemy ніколи не змішує дані з вашим запитом. Запит та дані передаються окремо вашому базовому драйверу бази даних - інтерполяція даних відбувається у вашій базі даних.

Sqlalchemy передає запит, як ви бачили в str(myquery)базі даних, і значення будуть надходити в окремий кортеж.

Ви можете використати певний підхід, коли ви самостійно інтерполюєте дані із запитом (як пропонував Альбертов нижче), але це не те саме, що виконує sqlalchemy.


чому це не те саме? Я розумію, що DB-API робить транзакції, можливо, впорядковує запити тощо, але чи може це змінити мій запит більше, ніж це?
cce

1
@cce: ви намагаєтеся знайти остаточний запит. SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC Є остаточним запитом. Їх %sнадсилає до бази даних sqlalchemy - sqlalchemy НІКОЛИ не ставить фактичні дані замість% s
nosklo

@cce: Деякі модулі dbapi теж цього не роблять - це часто робить сама база даних
nosklo

1
ага, я бачу, що ви говорите, дякую - копаючи далі sqlalchemy.dialects.mysql.mysqldb, do_executemany()передає оператор та параметри окремо курсору MySQLdb. так, непрямість!
cce

11

Спочатку дозвольте мені передмову сказати, що я припускаю, що ви робите це головним чином з метою налагодження - я б не рекомендував намагатися модифікувати вираз поза межами вільного API SQLAlchemy.

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

Але зробити це можна в двоетапному процесі досить легко. Щоб отримати заяву, ви можете зробити так, як ви вже показали, і просто надрукувати запит:

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

Ви можете наблизитися до кроку query.statement, щоб побачити назви параметрів. Примітка :id_1нижче та %sвище - насправді не є проблемою в цьому дуже простому прикладі, але може бути ключовим у більш складному викладі.

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

Тоді ви можете отримати фактичні значення параметрів, отримавши paramsвластивість складеного оператора:

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

Це працювало принаймні для сервера MySQL; Я б очікував, що це також досить загально для PostgreSQL, не потребуючи використання psycopg2.


З налагоджувача PyCharm для мене працювало наступне ... qry.compile (). Парами
Бен

Цікаво, може бути, SQLAlchemy трохи змінився з моменту написання цієї відповіді.
Ганнеле

10

Для бекенда postgresql за допомогою psycopg2 ви можете прослухати do_executeподію, потім використовувати курсор, оператор та ввести примусові параметри разом із Cursor.mogrify()вбудованими параметрами. Ви можете повернути True, щоб запобігти фактичному виконанню запиту.

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

Використання зразка:

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

Наступне рішення використовує мову виразу SQLAlchemy і працює з SQLAlchemy 1.1. Це рішення не змішує параметри із запитом (як цього вимагає оригінальний автор), але забезпечує спосіб використання моделей SQLAlchemy для генерації рядків запитів SQL та словників параметрів для різних діалектів SQL. Приклад заснований на підручнику http://docs.sqlalchemy.org/en/rel_1_0/core/tutorial.html

Враховуючи клас,

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

ми можемо створити оператор запиту за допомогою функції select .

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

Далі ми можемо скласти оператор у об’єкт запиту.

query = statement.compile()

За замовчуванням вираз компілюється з використанням базової реалізації з іменем, сумісної з базами даних SQL, такими як SQLite та Oracle. Якщо вам потрібно вказати діалект, такий як PostgreSQL, можна зробити

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

Або якщо ви хочете явно вказати діалект як SQLite, ви можете змінити параметр із 'qmark' на 'named'.

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

З об’єкта запиту ми можемо витягти рядок запиту та параметри запиту

query_str = str(query)
query_params = query.params

і нарешті виконати запит.

conn.execute( query_str, query_params )

Чим ця відповідь краща / відмінна, ніж відповідь ЕндіБарра, опублікована на 2 роки раніше?
Piotr Dobrogost

Відповідь AndyBarr включає приклад генерації запиту запиту за допомогою DBSession, тоді як ця відповідь включає приклад з використанням декларативного API та методу select. Що стосується складання запиту запиту з певним діалектом, відповіді однакові. Я використовую SQLAlchemy для генерації необроблених запитів, а потім виконую їх за допомогою adbapi Twister. У цьому випадку корисно знати, як скомпілювати запит без сеансу та витягти рядок та параметри запиту.
eric

3

Ви можете використовувати події з сім'ї ConnectionEvents : after_cursor_executeабо before_cursor_execute.

У sqlalchemy UsageRecipes від @zzzeek ви можете знайти цей приклад:

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

Тут ви можете отримати доступ до своєї заяви


2

Таким чином, зібравши безліч маленьких шматочків цих різних відповідей, я придумав те, що мені потрібно: простий набір коду, який потрібно скидати, а іноді, але надійно (тобто обробляє всі типи даних), захоплює точний, складений SQL, надісланий моєму Попередній постгрес, просто запитуючи сам запит:

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

Я думаю, .statement, можливо, зробить трюк: 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

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