バインドパラメーターではなく、値を含め、アプリケーションで有効なSQLを出力できるようにしたいのですが、SQLAlchemyでこれを行う方法は明確ではありません(設計上、私はかなり確信しています)。
誰かがこの問題を一般的な方法で解決しましたか?
バインドパラメーターではなく、値を含め、アプリケーションで有効なSQLを出力できるようにしたいのですが、SQLAlchemyでこれを行う方法は明確ではありません(設計上、私はかなり確信しています)。
誰かがこの問題を一般的な方法で解決しましたか?
回答:
ほとんどの場合、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)
query.prettyprint()
。大きなクエリでデバッグの負担を大幅に軽減します。
@compiles
プリティプリンティングシステムを実装するためのサードパーティのパッケージには、十分なフック(cursor_executeイベント、Pythonロギングフィルターなど)があります。
これは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
必要なことはデバッグ時にのみ意味がある場合、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
同じ振る舞いを得るために。
flask-sqlalchemy
これのユーザーにとっては受け入れられた答えであるべきです。
この目的のために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ステートメントをプログラムで呼び出す場合は、常にバインドされたパラメーターを使用します。
したがって、@ 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
。
datatime.now()
python 3 + sqlalchemy 1.0を使用する場合を除いて、すべての値が機能します。@zzzeekのアドバイスに従って、カスタムTypeDecoratorを作成して、そのTypeDecoratorも機能するようにする必要があります。
このコードは、@ 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)
return "%s" % value
代わりにreturn repr(value)
フロート、int型、長いセクションではPythonのようにlong型を出力したため22L
だけではなく22
"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")
mysqlで
上記の解決策は、重要なクエリでは「うまく機能しない」ことを指摘しておきます。私が遭遇した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つのレベルでテストされています。
from file import render_query; print(render_query(query))
sqlalchemy.engine
ログを利用することで、脆弱性の少ないソリューションを構築できます。クエリとバインドパラメータをログに記録します。バインドプレースホルダを、簡単に作成されたSQLクエリ文字列の値に置き換えるだけで済みます。