SQLAlchemy:実際のクエリを出力します


165

バインドパラメーターではなく、値を含め、アプリケーションで有効なSQLを出力できるようにしたいのですが、SQLAlchemyでこれを行う方法は明確ではありません(設計上、私はかなり確信しています)。

誰かがこの問題を一般的な方法で解決しましたか?


1
まだしていませんが、SQLAlchemyのsqlalchemy.engineログを利用することで、脆弱性の少ないソリューションを構築できます。クエリとバインドパラメータをログに記録します。バインドプレースホルダを、簡単に作成されたSQLクエリ文字列の値に置き換えるだけで済みます。
Simon

@Simon:ロガーの使用には2つの問題があります:1)ステートメントの実行時にのみ出力されます2)文字列の置換を実行する必要がありますが、その場合を除いて、バインドテンプレート文字列が正確にわかりません、そして私は何とかそれをクエリテキストから解析して、ソリューションをより脆弱にする必要があります。
ブクソール

@zzzeekのFAQ では、新しいURLはdocs.sqlalchemy.org/en/latest/faq/…のようです。
Jim DeLaHunt 2015年

回答:


167

ほとんどの場合、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が通常これで処理されないことです。これは、バインドされたパラメーターのバイパスは言うまでもなく、Python DBAPIによって適切に処理されるためです。おそらく、最新のWebアプリケーションで最も広く利用されているセキュリティホールです。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やstringなどの基本的なタイプでのみサポートされているという警告があります。さらに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
これは文字列を引用符で囲んだり、バインドされたパラメータを解決したりしません。
ブクソール2014

1
回答の後半は最新情報で更新されています。
zzzeek 14

2
@zzzeekきれいに印刷するクエリがデフォルトでsqlalchemyに含まれていないのはなぜですか?のようにquery.prettyprint()。大きなクエリでデバッグの負担を大幅に軽減します。
jmagnusson 2014

2
@jmagnusson美しさは見る人の目にあるからです:) @compilesプリティプリンティングシステムを実装するためのサードパーティのパッケージには、十分なフック(cursor_executeイベント、Pythonロギングフィルターなど)があります。
zzzeek 2014

1
@buzkor再: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
これはすごいです...簡単にアクセスできるように、これをいくつかのデバッグライブラリに追加する必要があります。これについてフットワークをしてくれてありがとう。それがとても複雑である必要があることを私は驚いています。
Corey O.

5
初心者はその文字列をcursor.execute()に誘惑するので、これは意図的に難しいと確信しています。大人に同意するという原則は、Pythonでは一般的に使用されています。
bukzor 2012年

非常に便利。ありがとう!
クライム

とてもいいですね。私は自由を取り、これをINSERTおよびUPDATEステートメント(PY2 / PY3)を含むSQLAlchemy v0.7.9-v1.1.15をカバーするstackoverflow.com/a/42066590/2127439に組み込みました。
wolfmanx 2017年

非常に素晴らしい。しかし、それは以下のように変換していますか?1)query(Table).filter(Table.Column1.is_(False)からWHERE Column1 IS 0へ。2)query(Table).filter(Table.Column1.is_(True)からWHERE Column1 IS 1へ。3)query( Table).filter(Table.Column1 == func.any([1,2,3]))からWHERE Column1 = any( '[1,2,3]')への変換では、構文が正しくありません。
セカールC

51

必要なことはデバッグ時にのみ意味がある場合、SQLAlchemyをecho=Trueで起動して、すべてのSQLクエリをログに記録できます。例えば:

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

これは、単一の要求に対しても変更できます。

echo=False–の場合True、エンジンはすべてのステートメントrepr()とそのパラメータリストをエンジンロガーに記録しsys.stdoutます。デフォルトではです。のecho属性はEngineいつでも変更して、ロギングのオンとオフを切り替えることができます。文字列"debug"に設定すると、結果行も標準出力に出力されます。このフラグは最終的にPythonロガーを制御します。ロギングを直接構成する方法については、ロギングの構成を参照してください。

ソース:SQLAlchemyエンジン構成

Flaskと併用すると、簡単に設定できます

app.config["SQLALCHEMY_ECHO"] = True

同じ振る舞いを得るために。


6
この答えはもっと高いに値します...そしてflask-sqlalchemyこれのユーザーにとっては受け入れられた答えであるべきです。
jso 2018年

25

この目的のためにcompileメソッドを使用できます。ドキュメントから:

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'

ドキュメントからの警告:

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


13

したがって、@ bukzorのコードに関する@zzzeekのコメントを基に、「かなり印刷可能な」クエリを簡単に取得するためにこれを思い付きました。

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を作成して、そのTypeDecoratorも機能するようにする必要があります。
jmagnusson

それは少し具体的すぎる。datetimeは、pythonとsqlalchemyのどの組み合わせでも機能しません。また、py27では、ASCII以外のUnicodeが爆発を引き起こします。
bukzor 2015年

私の知る限り、TypeDecoratorルートではテーブル定義を変更する必要がありますが、これは単にクエリを表示するだけの妥当な要件ではありません。私はあなたとzzzeekに少し近づくように私の回答を編集しましたが、テーブルの定義に適切に直交するカスタムの方言のルートを採用しました。
ブクソール2015年

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
SAの人々が、そのような単純な操作が非常に難しいことが合理的であると信じている理由はわかりません。
bukzor

ありがとうございました!render_literal_valueはうまく機能しました。私の唯一の変更点は以下のとおりであった:return "%s" % value代わりにreturn repr(value)フロート、int型、長いセクションではPythonのようにlong型を出力したため22Lだけではなく22
OrganicPanda

このレシピ(およびオリジナル)は、bindparam文字列値がASCIIで表現できない場合、UnicodeDecodeErrorを発生させます。これを修正した要点を投稿しました。
gsakkis 2013年

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

1
@bukzor-上記が「妥当」であるかどうか尋ねられたことを覚えていないので、私がそれを「信じている」と実際に述べることはできません-FWIW、そうではありません!:)私の答えを見てください。
zzzeek 2014年

8

上記の解決策は、重要なクエリでは「うまく機能しない」ことを指摘しておきます。私が遭遇した1つの問題は、問題を引き起こすpgsql ARRAYなどのより複雑なタイプでした。私はpgsql ARRAYでも機能する解決策を見つけました:

借用元: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 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)

ネストされた配列の2つのレベルでテストされています。


使い方の例を教えてください。ありがとう
slashdottir 2015年

from file import render_query; print(render_query(query))
AlfonsoPérez19年

それが私のために働いたこのページ全体の唯一の例です!よろしくお願いします!
fougerejo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.