SQLAlchemy: in truy vấn thực tế


165

Tôi thực sự muốn có thể in ra SQL hợp lệ cho ứng dụng của mình, bao gồm các giá trị, thay vì các tham số liên kết, nhưng không rõ ràng làm thế nào để làm điều này trong SQLAlchemy (theo thiết kế, tôi khá chắc chắn).

Có ai đã giải quyết vấn đề này một cách chung chung chưa?


1
Tôi chưa, nhưng có lẽ bạn có thể xây dựng một giải pháp ít dễ vỡ hơn bằng cách nhấn vào sqlalchemy.enginenhật ký của SQLAlchemy . Nó ghi nhật ký các truy vấn và các tham số liên kết, bạn chỉ phải thay thế các trình giữ chỗ liên kết bằng các giá trị trên chuỗi truy vấn SQL được xây dựng sẵn.
Simon

@Simon: có hai vấn đề khi sử dụng bộ ghi: 1) nó chỉ in khi một câu lệnh đang thực thi 2) Tôi vẫn phải thực hiện thay thế chuỗi, ngoại trừ trong trường hợp đó, tôi sẽ không biết chính xác chuỗi liên kết mẫu và tôi phải bằng cách nào đó phân tích nó ra khỏi văn bản truy vấn, làm cho giải pháp trở nên mong manh hơn .
bukzor

URL mới dường như là docs.sqlalchemy.org/en/latest/faq/ khuyên cho Câu hỏi thường gặp của @ zzzeek.
Jim DeLaHunt

Câu trả lời:


166

Trong phần lớn các trường hợp, "chuỗi hóa" của câu lệnh hoặc truy vấn SQLAlchemy đơn giản như:

print str(statement)

Điều này áp dụng cả cho ORM Querycũng như bất kỳ select()hoặc bất kỳ tuyên bố nào khác.

Lưu ý : câu trả lời chi tiết sau đây đang được duy trì trên tài liệu sqlalchemy .

Để có được câu lệnh như được biên dịch cho một phương ngữ hoặc công cụ cụ thể, nếu bản thân câu lệnh chưa bị ràng buộc với một câu lệnh, bạn có thể chuyển câu lệnh này để biên dịch () :

print statement.compile(someengine)

hoặc không có động cơ:

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

Khi được cung cấp một Queryđối tượng ORM , để có được compile()phương thức này, trước tiên chúng ta chỉ cần truy cập vào trình truy cập .statement :

statement = query.statement
print statement.compile(someengine)

liên quan đến quy định ban đầu rằng các tham số bị ràng buộc phải được "nội tuyến" vào chuỗi cuối cùng, thách thức ở đây là SQLAlchemy thường không được giao nhiệm vụ này, vì điều này được Python DBAPI xử lý một cách thích hợp, chưa kể đến việc bỏ qua các tham số bị ràng buộc là có lẽ là lỗ hổng bảo mật được khai thác rộng rãi nhất trong các ứng dụng web hiện đại. SQLAlchemy có khả năng hạn chế để thực hiện chuỗi này trong một số trường hợp nhất định như phát ra DDL. Để truy cập chức năng này, người ta có thể sử dụng cờ 'lítal_binds', được chuyển đến 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})

Cách tiếp cận ở trên có một số lưu ý rằng nó chỉ được hỗ trợ cho các loại cơ bản, chẳng hạn như int và chuỗi, và hơn nữa nếu bindparam không có giá trị đặt trước được sử dụng trực tiếp, thì nó cũng không thể xâu chuỗi được.

Để hỗ trợ kết xuất theo nghĩa đen nội tuyến cho các loại không được hỗ trợ, hãy triển khai loại TypeDecoratorcho mục tiêu bao gồm một TypeDecorator.process_literal_paramphương thức:

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

sản xuất như:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)

2
Điều này không đặt dấu ngoặc kép quanh chuỗi và không giải quyết một số thông số bị ràng buộc.
bukzor

1
nửa sau của câu trả lời đã được cập nhật với thông tin mới nhất.
zzzeek

2
@zzzeek Tại sao mặc định các truy vấn in đẹp được bao gồm trong sqlalchemy? Thích query.prettyprint(). Nó giảm bớt nỗi đau gỡ lỗi với các truy vấn lớn vô cùng.
jmagnusson

2
@jmagnusson vì vẻ đẹp nằm trong mắt của kẻ si tình :) Có nhiều móc nối (ví dụ: sự kiện con trỏ, bộ lọc ghi nhật ký Python @compiles, v.v.) cho bất kỳ số gói bên thứ ba nào để thực hiện các hệ thống in ấn đẹp.
zzzeek

1
@buzkor re: giới hạn đã được sửa trong 1.0 bitbucket.org/zzzeek/sqlalchemy/su/3034/
Kẻ

66

Điều này hoạt động trong python 2 và 3 và sạch hơn một chút so với trước đây, nhưng yêu cầu 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

Bản giới thiệu:

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

Cung cấp đầu ra này: (được thử nghiệm trong python 2.7 và 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
Điều này thật tuyệt vời ... Sẽ phải thêm nó vào một số lib gỡ lỗi để chúng ta có thể dễ dàng truy cập nó. Cảm ơn đã làm những bước chân trên cái này. Tôi ngạc nhiên rằng nó phải quá phức tạp.
Corey O.

5
Tôi khá chắc chắn rằng điều này là cố ý, bởi vì những người mới bị cám dỗ với con trỏ.execute () chuỗi đó. Nguyên tắc đồng ý người lớn thường được sử dụng trong trăn.
bukzor

Rất hữu ích. Cảm ơn!
clime

Thực sự rất tốt đẹp. Tôi đã lấy tự do và kết hợp nó vào stackoverflow.com/a/42066590/2127439 , bao gồm SQLAlchemy v0.7.9 - v1.1.15, bao gồm các câu lệnh INSERT và UPDATE (PY2 / PY3).
wolfmanx

rất đẹp. nhưng nó đang chuyển đổi như dưới đây. 1) truy vấn (Bảng) .filter (Table.Column1.is_ (Sai) đến WHERE Cột1 IS 0. 2) truy vấn (Bảng) .filter (Table.Column1.is_ (True) cho WHERE Cột1 IS 1. 3) truy vấn ( Bảng) .filter (Table.Column1 == func.any ([1,2,3])) thành WHERE Cột1 = any ('[1,2,3]') ở trên các chuyển đổi không chính xác trong cú pháp.
Sekhar C

51

Cho rằng những gì bạn muốn chỉ có ý nghĩa khi gỡ lỗi, bạn có thể bắt đầu SQLAlchemy với echo=True, để ghi nhật ký tất cả các truy vấn SQL. Ví dụ:

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

Điều này cũng có thể được sửa đổi cho chỉ một yêu cầu:

echo=False- nếu True, Công cụ sẽ ghi nhật ký tất cả các câu lệnh cũng như một repr()danh sách tham số của chúng vào bộ ghi công cụ, mặc định sys.stdout. Các echothuộc tính của Enginethể được sửa đổi bất cứ lúc nào để biến khai thác gỗ và tắt. Nếu được đặt thành chuỗi "debug", các hàng kết quả cũng sẽ được in thành đầu ra tiêu chuẩn. Cờ này cuối cùng kiểm soát một logger Python; xem Cấu hình ghi nhật ký để biết thông tin về cách định cấu hình ghi nhật ký trực tiếp.

Nguồn: Cấu hình động cơ SQLAlchemy

Nếu được sử dụng với Flask, bạn chỉ cần đặt

app.config["SQLALCHEMY_ECHO"] = True

để có được hành vi tương tự.


6
Câu trả lời này xứng đáng được cao hơn .. và đối với người dùng flask-sqlalchemynày nên là câu trả lời được chấp nhận.
jso

25

Chúng ta có thể sử dụng phương pháp biên dịch cho mục đích này. Từ các tài liệu :

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

Kết quả:

SELECT * FROM users WHERE users.name BETWEEN 'm' AND 'z'

Cảnh báo từ các tài liệu:

Không bao giờ sử dụng kỹ thuật này với nội dung chuỗi nhận được từ đầu vào không tin cậy, chẳng hạn như từ các biểu mẫu web hoặc các ứng dụng đầu vào của người dùng khác. Các phương tiện của SQLAlchemy để ép buộc các giá trị Python thành các giá trị chuỗi SQL trực tiếp không an toàn đối với đầu vào không tin cậy và không xác thực loại dữ liệu được truyền. Luôn sử dụng các tham số ràng buộc khi lập trình gọi các câu lệnh SQL không DDL theo chương trình đối với cơ sở dữ liệu quan hệ.


13

Vì vậy, dựa trên nhận xét của @ zzzeek về mã của @ bukzor, tôi đã nghĩ ra điều này để dễ dàng có được một truy vấn "có thể in được":

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)

Cá nhân tôi có một thời gian khó đọc mã không được thụt lề nên tôi đã sử dụng sqlparseđể giới thiệu lại SQL. Nó có thể được cài đặt với pip install sqlparse.


@bukzor Tất cả các giá trị hoạt động ngoại trừ giá trị datatime.now()khi sử dụng python 3 + sqlalchemy 1.0. Bạn sẽ phải làm theo lời khuyên của @ zzzeek về việc tạo TypeDecorator tùy chỉnh để cái đó hoạt động tốt.
jmagnusson

Điều đó hơi quá cụ thể. Thời gian không hoạt động trong bất kỳ sự kết hợp nào giữa python và sqlalchemy. Ngoài ra, trong py27, unicode không ascii gây ra vụ nổ.
bukzor

Theo như tôi có thể thấy, tuyến TypeDecorator yêu cầu tôi thay đổi các định nghĩa bảng của mình, đây không phải là một yêu cầu hợp lý để chỉ cần xem các truy vấn của tôi. Tôi đã chỉnh sửa câu trả lời của mình để gần hơn một chút với câu trả lời của bạn và của zzzeek, ​​nhưng tôi đã chọn lộ trình của một phương ngữ tùy chỉnh, trực giao chính xác với các định nghĩa bảng.
bukzor

11

Mã này dựa trên câu trả lời tuyệt vời hiện có từ @bukzor. Tôi vừa thêm kết xuất tùy chỉnh cho datetime.datetimekiểu vào Oracle TO_DATE().

Vui lòng cập nhật mã cho phù hợp với cơ sở dữ liệu của bạn:

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
Tôi không thấy lý do tại sao dân gian SA tin rằng một hoạt động đơn giản như vậy lại khó khăn đến vậy .
bukzor

Cảm ơn bạn! render_literal_value hoạt động tốt với tôi. Thay đổi duy nhất của tôi là: return "%s" % valuethay vì return repr(value)trong phần float, int, long vì Python đã xuất ra từ lâu 22Lthay vì chỉ22
OrganicPanda

Công thức này (cũng như bản gốc) làm tăng UnicodeDecodeError nếu bất kỳ giá trị chuỗi bindparam nào không thể biểu diễn trong ascii. Tôi đã đăng một ý chính sửa lỗi này.
gsakkis

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

1
@bukzor - Tôi không nhớ là đã được hỏi nếu điều trên là "hợp lý" vì vậy bạn không thể thực sự nói rằng tôi "tin" nó - FWIW, không phải vậy! :) xin vui lòng xem câu trả lời của tôi.
zzzeek

8

Tôi muốn chỉ ra rằng các giải pháp được đưa ra ở trên không "chỉ hoạt động" với các truy vấn không tầm thường. Một vấn đề tôi gặp phải là các loại phức tạp hơn, chẳng hạn như các ARRAY của pssql gây ra sự cố. Tôi đã tìm thấy một giải pháp mà đối với tôi, chỉ hoạt động ngay cả với ARRAY ARSSAY:

mượn từ: https://gist.github.com/gsakkis/4572159

Mã được liên kết dường như dựa trên phiên bản cũ hơn của SQLAlchemy. Bạn sẽ gặp lỗi khi nói rằng thuộc tính _mapper_zero_or_none không tồn tại. Đây là phiên bản cập nhật sẽ hoạt động với phiên bản mới hơn, bạn chỉ cần thay thế _mapper_zero_or_none bằng liên kết. Ngoài ra, điều này có hỗ trợ cho các mảng pssql:

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

Đã thử nghiệm với hai cấp độ của các mảng lồng nhau.


Hãy cho một ví dụ về cách sử dụng nó? Cảm ơn bạn
slashdottir

from file import render_query; print(render_query(query))
Alfonso Pérez

Đó là ví dụ duy nhất của toàn bộ trang này làm việc cho tôi! Cảm ơn !
fougerejo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.