SQLAlchemy: พิมพ์แบบสอบถามจริง


165

ฉันต้องการพิมพ์ SQL ที่ถูกต้องสำหรับแอปพลิเคชันของฉันรวมถึงค่ามากกว่าผูกพารามิเตอร์ แต่ไม่ชัดเจนว่าจะทำอย่างไรใน SQLAlchemy (โดยการออกแบบฉันค่อนข้างแน่ใจ)

มีใครแก้ไขปัญหานี้ในลักษณะทั่วไป?


1
ฉันยังไม่ได้ แต่คุณอาจสร้างโซลูชันที่มีความเปราะบางน้อยลงได้โดยแตะที่sqlalchemy.engineบันทึกของ SQLAlchemy มันบันทึกแบบสอบถามและพารามิเตอร์ผูกคุณจะต้องแทนที่ตัวยึดตำแหน่งผูกกับค่าในสตริงแบบสอบถาม SQL สร้างขึ้นอย่างง่ายดาย
Simon

@Simon: มีสองปัญหากับการใช้ logger: 1) มันจะพิมพ์เฉพาะเมื่อคำสั่งดำเนินการ 2) ฉันยังคงต้องทำการแทนที่สตริงยกเว้นในกรณีนั้นฉันไม่รู้จักสตริง bind-template อย่างแน่นอน และฉันจะต้องแยกมันออกมาจากข้อความค้นหาทำให้การแก้ไขมีความเปราะบางมากขึ้น
bukzor

URL ใหม่ดูเหมือนจะเป็นdocs.sqlalchemy.org/en/latest/faq/…สำหรับคำถามที่พบบ่อยของ @ zzzeek
Jim DeLaHunt

คำตอบ:


168

ในกรณีส่วนใหญ่คำว่า "การทำให้เป็นสตริง" ของคำสั่งหรือแบบสอบถาม SQLAlchemy นั้นง่ายเหมือน:

print str(statement)

สิ่งนี้ใช้ได้ทั้งกับ ORM Queryเช่นเดียวกับใด ๆselect()คำสั่งหรืออื่น ๆ

หมายเหตุ : คำตอบโดยละเอียดต่อไปนี้ได้รับการปรับปรุงในเอกสาร sqlalchemyเอกสาร

หากต้องการให้คำสั่งคอมไพล์ไปยังภาษาหรือเอ็นจินเฉพาะหากคำสั่งนั้นไม่ได้ถูกผูกไว้กับคำสั่งที่คุณสามารถส่งให้คอมไพล์ () :

print statement.compile(someengine)

หรือไม่มีเครื่องยนต์:

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

เมื่อได้รับQueryวัตถุORM เพื่อให้ได้มาซึ่งcompile()วิธีการที่เราต้องการเพียงแค่เข้าถึง. statement accessor ก่อน:

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

ด้วยความเคารพต่อข้อตกลงดั้งเดิมที่พารามิเตอร์ที่ผูกไว้จะต้อง "inlined" ในสตริงสุดท้ายความท้าทายที่นี่คือ SQLAlchemy โดยปกติจะไม่ได้มอบหมายให้กับสิ่งนี้เนื่องจากการจัดการอย่างเหมาะสมโดย Python DBAPI ไม่ต้องพูดถึงการข้ามพารามิเตอร์ที่ถูกผูกไว้ อาจเป็นช่องโหว่ด้านความปลอดภัยที่ใช้กันอย่างแพร่หลายมากที่สุดในเว็บแอปพลิเคชันที่ทันสมัย SQLAlchemy มีความสามารถที่ จำกัด ในการทำ stringification นี้ในบางสถานการณ์เช่นการเปล่ง 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})

วิธีการข้างต้นมีข้อแม้ที่สนับสนุนเฉพาะสำหรับประเภทพื้นฐานเช่น ints และสตริงและยิ่งถ้า 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 เพราะความสวยงามอยู่ในสายตาของผู้ดู :) มีตะขอมากมาย (เช่นเหตุการณ์ cursor_execute ตัวกรองการบันทึก Python @compilesและอื่น ๆ ) สำหรับแพ็คเกจของบุคคลที่สามจำนวนมากเพื่อนำไปใช้กับระบบการพิมพ์ที่สวยงาม
zzzeek

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
น่ากลัว ... จะต้องเพิ่มสิ่งนี้ลงใน libs ดีบักบางส่วนเพื่อให้เราสามารถเข้าถึงได้อย่างง่ายดาย ขอบคุณที่ทำ footwork ในอันนี้ ฉันประหลาดใจที่มันต้องซับซ้อนมาก
Corey O.

5
ฉันค่อนข้างแน่ใจว่านี่เป็นเรื่องยากโดยเจตนาเพราะมือใหม่ถูกล่อลวงให้เคอร์เซอร์ดำเนินการ () สตริงนั้น หลักการของการยินยอมผู้ใหญ่มักใช้ในงูหลามแม้ว่า
bukzor

มีประโยชน์มาก. ขอบคุณ!
clime

ดีมากจริงๆ ฉันใช้เสรีภาพและรวมสิ่งนี้ไว้ในstackoverflow.com/a/42066590/2127439ซึ่งครอบคลุม SQLAlchemy v0.7.9 - v1.1.15 รวมถึงคำสั่ง INSERT และ UPDATE (PY2 / PY3)
wolfmanx

ดีมาก. แต่มันแปลงเป็นด้านล่าง 1) การสืบค้น (ตาราง) .filter (Table.Column1.is_ (เท็จ) ถึง WHERE Column1 คือ 0 2) แบบสอบถาม (ตาราง) .filter (Table.Column1.is_ (True) ถึง WHERE Column1 IS 1 3) ( ตาราง) .filter (Table.Column1 == func.any ([1,2,3])) ไปที่ WHERE Column1 = ใด ๆ ('[1,2,3]') ข้างบน Conversion ไม่ถูกต้องในไวยากรณ์
Sekhar C

52

ระบุว่าสิ่งที่คุณต้องการจะสมเหตุสมผลเมื่อทำการดีบั๊กคุณสามารถเริ่ม SQLAlchemy ด้วยecho=Trueเพื่อบันทึกการสืบค้น SQL ทั้งหมด ตัวอย่างเช่น:

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

สิ่งนี้สามารถแก้ไขได้สำหรับการร้องขอเพียงครั้งเดียว:

echo=False- ถ้าTrueเครื่องยนต์จะเข้าสู่ระบบงบทั้งหมดเช่นเดียวกับรายการพารามิเตอร์ของพวกเขาไปตัดไม้เครื่องมือที่เริ่มต้นที่repr() แอตทริบิวต์สามารถปรับเปลี่ยนได้ตลอดเวลาเพื่อเลี้ยวเข้าสู่ระบบในและนอก หากตั้งค่าเป็นสตริงแถวผลลัพธ์จะถูกพิมพ์ไปยังเอาต์พุตมาตรฐานเช่นกัน การตั้งค่าสถานะนี้ในที่สุดควบคุมตัวบันทึก Python ดูที่การกำหนดค่าการบันทึกสำหรับข้อมูลเกี่ยวกับวิธีกำหนดค่าการบันทึกโดยตรงsys.stdoutechoEngine"debug"

ที่มา: การกำหนดค่าเครื่องมือ SQLAlchemy

หากใช้กับ Flask คุณสามารถตั้งค่าได้ง่ายๆ

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'

คำเตือนจากเอกสาร:

อย่าใช้เทคนิคนี้กับเนื้อหาสตริงที่ได้รับจากอินพุตที่ไม่น่าเชื่อถือเช่นจากเว็บฟอร์มหรือแอปพลิเคชันที่ผู้ใช้ป้อนเข้าอื่น ๆ สิ่งอำนวยความสะดวกของ SQLAlchemy เพื่อบังคับค่า Python ให้เป็นค่าสตริง SQL โดยตรงจะไม่ปลอดภัยต่ออินพุตที่ไม่น่าเชื่อถือและไม่ตรวจสอบประเภทของข้อมูลที่จะส่งผ่าน ใช้พารามิเตอร์ที่ถูกผูกไว้เสมอเมื่อเรียกใช้คำสั่งที่ไม่ใช่ DDL SQL โดยทางโปรแกรมกับฐานข้อมูลเชิงสัมพันธ์


13

ดังนั้นการสร้างความคิดเห็นของ @ zzzeek เกี่ยวกับรหัสของ @ bukzor ฉันจึงได้สิ่งนี้เพื่อให้ได้คำค้นหา "สวย ๆ ที่พิมพ์ได้":

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 ที่กำหนดเองเพื่อให้ทำงานได้เช่นกัน
jmagnusson

นั่นเป็นสิ่งที่เฉพาะเจาะจงเกินไป datetime ไม่สามารถใช้งานร่วมกับ python และ sqlalchemy ได้ นอกจากนี้ใน py27 non-ascii unicode ทำให้เกิดการระเบิด
bukzor

เท่าที่ฉันเห็นเส้นทาง TypeDecorator กำหนดให้ฉันต้องเปลี่ยนคำจำกัดความของตารางซึ่งไม่ใช่ข้อกำหนดที่สมเหตุสมผลในการดูข้อความค้นหาของฉัน ฉันแก้ไขคำตอบของฉันให้ใกล้เคียงกับคุณและ zzzeek มากขึ้นเล็กน้อย แต่ฉันใช้เส้นทางของภาษาถิ่นที่กำหนดเองซึ่งตั้งฉากกับคำจำกัดความของตารางอย่างถูกต้อง
bukzor

11

รหัสนี้ขึ้นอยู่กับคำตอบที่ยอดเยี่ยมที่มีอยู่จาก @bukzor ฉันเพิ่งเพิ่มการแสดงผลที่กำหนดเองสำหรับdatetime.datetimeประเภทลงใน OracleTO_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, long เพราะ Python แสดงผลเป็น longs 22Lแทนแทนที่จะเป็นเพียง22
OrganicPanda

สูตรนี้ (เช่นเดียวกับต้นฉบับ) เพิ่ม UnicodeDecodeError หากค่าสตริง bindparam ใด ๆ ไม่สามารถแทนได้ใน ascii ฉันโพสต์ส่วนสำคัญที่แก้ไขปัญหานี้
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 ARRAYs:

ยืมมาจาก: 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.