SQLAlchemy式から生のコンパイル済みSQLクエリを取得するにはどうすればよいですか?


101

SQLAlchemyクエリオブジェクトがあり、すべてのパラメーターがバインドされた(たとえば%s、ステートメントコンパイラーまたはMySQLdbダイアレクトエンジンによってバインドされるのを待っている変数がないなど)、コンパイルされたSQLステートメントのテキストを取得します。

str()クエリを呼び出すと、次のようなことがわかります。

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

query._paramsを調べてみましたが、空の辞書です。私はこのsqlalchemy.ext.compiler.compilesデコレータの例を使用して独自のコンパイラを作成しましたが、そこにあるステートメントでさえ、%sデータが必要な場所にまだあります。

クエリを作成するためにパラメーターが混在するタイミングがよくわかりません。クエリオブジェクトを調べるとき、それらは常に空のディクショナリです(ただし、クエリは正常に実行され、エコーロギングをオンにするとエンジンが出力します)。

SQLAlchemyは、基になるクエリを知りたくないというメッセージを受け取り始めています。これは、さまざまなDB-APIのすべての式APIのインターフェイスの一般的な性質を破壊するためです。クエリが何であるかがわかる前にクエリが実行されてもかまいません。ただ知りたいだけです!

回答:


105

このブログは更新された回答を提供します。

ブログの投稿から引用すると、これは推奨され、私にとってはうまくいきました。

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

答えをくれたNicolas Cadouに感謝します。ここで検索を行う人に役立つことを願っています。


2
辞書として値を取得する簡単な方法はありますか?
ダミアン

5
@Damienを指定するとc = q.statement.compile(...)、取得できますc.params
Hannele

1
投稿はmysqlでタグ付けされているため、この回答のpostgresqlの詳細は実際には関係ありません。
Hannele 2018

3
OPを正しく理解している場合、彼は最後のクエリを求めています。方言(ここではpostgres)を指定して印刷すると、まだ与えてくれリテラル値の代わりにプレースホルダを。@マットの答えは仕事をします。プレースホルダーを使用してSQLを取得することは、as_scalar()-method ofの方が簡単ですQuery
Patrick B.

1
@PatrickB。同意する。マットの答えは「正しい」答えと考えられるべきです。これを行うだけで同じ結果が得られstr(q)ます。
アンドレ・C.アンデルセン

89

ドキュメントが使用するliteral_bindsクエリ印刷するqパラメータを含むを:

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

上記のアプローチには、intやstringなどの基本的なタイプでのみサポートされているという警告があります。さらに、事前設定された値のないbindparam()を直接使用すると、そのいずれかを文字列化できません。

ドキュメントもこの警告を発行します:

Webフォームや他のユーザー入力アプリケーションなど、信頼できない入力から受け取った文字列コンテンツでは、この手法を使用しないでください。Python値を直接SQL文字列値に強制変換するSQLAlchemyの機能は、信頼できない入力に対して安全ではなく、渡されるデータのタイプを検証しません。リレーショナルデータベースに対して非DDL SQLステートメントをプログラムで呼び出す場合は、常にバインドされたパラメーターを使用します。


ありがとうございました!これは非常に役に立ち、パンダの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を使用しているので、私の方言は「定位置」であり、辞書ではなくparamsリストが必要です。現在、その..で動作するようにあなたの例を取得しようと
CCE

adaptこのような使い方はしないでください。毎回、少なくともその戻り値でprepare()を呼び出し、引数として接続を提供するので、適切な引用を行うことができます。
Alex Gaynor

@アレックス:psycopgで適切な引用を行う正しい方法は何でしょうか?(戻り値でprepare()を呼び出す以外に、これは最適ではないようです)
albertov

申し訳ありませんが、obj.prepare(connection)を呼び出すかぎり、問題はありませんが、問題ありません。これは、libpqが引用用に提供する「良い」APIが接続を必要とするためです(Unicode文字列のエンコーディングなどを提供します)。
Alex Gaynor、2011年

1
ありがとう。prepare戻り値を呼び出してみましたが、そのメソッドがないようです:AttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'。私はpsycopg2 2.2.1 BTWを使用しています
albertov

18

MySQLdbバックエンドの場合、albertovの素晴らしい答えを少し修正しました(ありがとうございました!)。私はそれらがマージされたかどうかをチェックするためにマージできると確信していますcomp.positionalTrue、それはこの質問の範囲を少し超えています。

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

15

ことは、sqlalchemyがデータとクエリを決して混合しないことです。クエリとデータは、基になるデータベースドライバーに個別に渡されます。データの補間はデータベースで行われます。

Sqlalchemyは、これまで見てきたようにクエリをstr(myquery)データベースに渡し、値は別のタプルに入れられます。

(albertovが以下に提案するように)クエリを使用してデータを補間するいくつかのアプローチを使用できますが、それはsqlalchemyが実行しているものとは異なります。


なぜ同じではないのですか?DB-APIがトランザクションを実行しており、クエリの並べ替えなどを行っていることを理解していますが、これによりクエリが変更される可能性はありますか?
cce

1
@cce:あなたは最後のクエリを見つけようとしています。SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC IS最後のクエリが。それら%sはsqlalchemyによってデータベースに送信されます
-sqlalchemy

@cce:一部のdbapiモジュールはそれも実行しません-多くの場合、データベース自体によって実行されます
nosklo

1
なるほど、私はあなたが言っているか見て、おかげで-にさらに掘りsqlalchemy.dialects.mysql.mysqldbdo_executemany()MySQLdbはカーソルに別々のステートメント&パラメータを渡します。いいね!
2011年

10

psycopg2を使用するpostgresqlバックエンドの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"}}'}

10

まず、主にデバッグの目的でこれを行っていると仮定します。SQLAlchemyFluent APIの外部でステートメントを変更することはお勧めしません。

残念ながら、クエリパラメータを含めてコンパイルされたステートメントを表示する簡単な方法はないようです。SQLAlchemyは、実際にはパラメーターをステートメントに入れません。パラメーターは、辞書としてデータベースエンジンに渡されます。これにより、データベース固有のライブラリが特殊文字のエスケープなどを処理して、SQLインジェクションを回避できます。

しかし、これは2ステップのプロセスでかなり簡単に行うことができます。ステートメントを取得するには、すでに示したようにしてクエリを印刷します。

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

query.statementを使用すると、パラメータ名を確認できます。(:id_1下と%s上の注意-この非常に単純な例では実際には問題ではありませんが、より複雑なステートメントでは重要になる可能性があります。)

>>> print(query.statement)
>>> print(query.statement.compile()) # reasonably 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()。params
Ben

興味深いことに、私がこの回答を書いてからSQLAlchemyが少し変わった可能性があります。
Hannele

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

デフォルトでは、ステートメントはSQLiteやOracleなどのSQLデータベースと互換性のある基本的な「名前付き」実装を使用してコンパイルされます。PostgreSQLなどの方言を指定する必要がある場合は、

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

または、方言をSQLiteとして明示的に指定する場合は、paramstyleを「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年前に投稿されたAndyBarrの回答よりどのように優れていますか/異なっていますか?
Piotr Dobrogost 2017

AndyBarrの回答には、DBSessionでクエリステートメントを生成する例が含まれていますが、この回答には、宣言型APIとselectメソッドを使用した例が含まれています。特定の方言でクエリステートメントをコンパイルすることに関しては、答えは同じです。SQLAlchemyを使用して生のクエリを生成し、Twisterのadbapiで実行します。この使用例では、セッションなしでクエリをコンパイルし、クエリ文字列とパラメータを抽出する方法を知っていると役立ちます。
エリック

3

ConnectionEventsファミリーのイベントを使用できます。after_cursor_executeまたはbefore_cursor_execute

@zzzeekによるsqlalchemy UsageRecipesには、次の例があります。

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を取得しますクエリ自体を調べるだけでPostgresバックエンド:

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

いくつかの種類のフィルターを設定している場合、ステートメントはパラメーターが何であるかを表示しません。
Hannele 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.