SQLAlchemy : 실제 쿼리 인쇄


165

바인드 매개 변수가 아닌 값을 포함하여 응용 프로그램에 유효한 SQL을 실제로 인쇄하고 싶지만 SQLAlchemy 에서이 작업을 수행하는 방법은 확실하지 않습니다 (설계 상 상당히 확실합니다).

누구 든지이 문제를 일반적인 방식으로 해결 했습니까?


1
나는하지 않았지만 SQLAlchemy의 sqlalchemy.engine로그를 활용하여 덜 취약한 솔루션을 구축 할 수 있습니다. 쿼리 및 바인드 매개 변수를 기록하므로 바인드 자리 표시자를 쉽게 구성된 SQL 쿼리 문자열의 값으로 바꾸면됩니다.
사이먼

@Simon : 로거 사용에는 두 가지 문제가 있습니다 .1) 명령문이 실행될 때만 인쇄됩니다 .2) 문자열 대체를 수행해야합니다.이 경우를 제외하고는 바인드 템플릿 문자열을 정확히 알 수 없습니다. , 어떻게 든 쿼리 텍스트에서 구문 분석하여 솔루션을 보다 취약 하게 만들어야합니다 .
bukzor

@zzzeek의 FAQ에 대한 새 URL은 docs.sqlalchemy.org/en/latest/faq/… 인 것으로 보입니다 .
Jim DeLaHunt

답변:


168

대부분의 경우 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가 일반적으로이 작업을 수행하지 않는 것입니다. 아마도 현대 웹 애플리케이션에서 가장 널리 이용되는 보안 허점 일 것입니다. 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
이것은 문자열을 따옴표로 묶지 않으며 일부 바운드 매개 변수를 해결하지 않습니다.
bukzor

1
답변의 후반부는 최신 정보로 업데이트되었습니다.
zzzeek

2
@zzzeek 왜 기본적으로 sqlalchemy에 예쁜 인쇄 쿼리가 포함되어 있지 않습니까? 처럼 query.prettyprint(). 큰 쿼리로 엄청난 디버깅 문제를 해결합니다.
jmagnusson

2
@jmagnusson 아름다움이 보는 사람의 눈에 들어 있기 때문에 :) @compiles예쁜 인쇄 시스템을 구현하기 위해 많은 타사 패키지에 대한 충분한 후크 (예 : cursor_execute 이벤트, Python 로깅 필터 등)가 있습니다.
zzzeek


66

이것은 파이썬 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 () 시키려고 유혹하기 때문에 이것이 의도적으로 어렵다는 것을 확신합니다. 동의하는 성인의 원칙은 파이썬에서 일반적으로 사용됩니다.
bukzor

굉장히 유용하다. 감사!
clime

정말 좋습니다. 나는 자유를 취하여 이것을 INSERT 및 UPDATE 문 (PY2 / PY3)을 포함하여 SQLAlchemy v0.7.9-v1.1.15를 다루는 stackoverflow.com/a/42066590/2127439에 통합했습니다 .
wolfmanx

아주 좋아요 그러나 아래와 같이 변환하고 있습니다. 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])) to WHERE Column1 = any ( '[1,2,3]') 위의 변환은 구문에서 올바르지 않습니다.
Sekhar C

52

원하는 경우 디버깅 할 때만 의미가 echo=True있으므로 SQLAlchemy를 사용 하여 모든 SQL 쿼리를 기록 할 수 있습니다. 예를 들면 다음과 같습니다.

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

단일 요청에 대해서만 수정할 수도 있습니다.

echo=False–이면 True엔진은 모든 명령문과 repr()매개 변수 목록을 엔진 로거에 기본값으로 기록합니다 sys.stdout. 의 echo속성은 Engine언제든지 로깅을 켜거나 끄도록 수정할 수 있습니다. string으로 설정하면 "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'

문서에서 경고 :

웹 양식이나 다른 사용자 입력 응용 프로그램과 같은 신뢰할 수없는 입력에서 수신 한 문자열 내용에는이 기술을 사용하지 마십시오. 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을 사용할 때의 값을 제외한 모든 값이 작동합니다 . 해당 기능을 사용하려면 사용자 정의 TypeDecorator를 만드는 방법에 대한 @zzzeek의 조언을 따라야합니다.
jmagnusson

그것은 너무 구체적입니다. 날짜 시간은 파이썬과 sqlalchemy의 조합에서 작동하지 않습니다. 또한 py27에서 ASCII가 아닌 유니 코드는 폭발을 일으 킵니다.
bukzor

내가 볼 수있는 한, TypeDecorator 라우트는 테이블 정의를 변경해야하는데 이는 단순히 쿼리를보기위한 합리적인 요구 사항이 아닙니다. 나는 당신과 zzzeek의 것에 좀 더 가깝게 대답을 편집했지만 테이블 정의와 올바르게 직교하는 사용자 정의 방언을 사용했습니다.
bukzor 2016 년

11

이 코드는 @bukzor의 훌륭한 기존 답변 을 기반으로 합니다. 방금 datetime.datetimeOracle 에 유형에 대한 사용자 정의 렌더링을 추가했습니다.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에, 긴 섹션 파이썬으로 걷고 출력 때문에 22L대신의22
OrganicPanda

이 레시피 (원본뿐만 아니라)는 bindparam 문자열 값을 ASCII로 표현할 수없는 경우 UnicodeDecodeError를 발생시킵니다. 나는 이것을 고치는 요지 를 게시했다 .
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))
Alfonso Pérez

저에게 도움이 된이 전체 페이지의 유일한 예입니다! 감사 !
fougerejo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.