SQLAlchemy: gerçek sorguyu yazdır


165

Gerçekten bind parametreleri yerine değerleri de dahil olmak üzere benim uygulama için geçerli SQL yazdırmak istiyorum, ama SQLAlchemy (tasarım, oldukça eminim) bunu nasıl açık değildir.

Herkes bu sorunu genel olarak çözdü mü?


1
Yapmadım, ancak SQLAlchemy'nin sqlalchemy.enginegünlüğüne dokunarak muhtemelen daha az kırılgan bir çözüm oluşturabilirsiniz . Sorguları ve bağlama parametrelerini günlüğe kaydeder, yalnızca bağlama yer tutucularını kolayca oluşturulmuş bir SQL sorgu dizesindeki değerlerle değiştirmeniz gerekir.
Simon

@Simon: Logger kullanarak iki sorun var: 1) sadece bir ifade yürütürken yazdırır 2) Hala bir dize değiştirme yapmak zorunda kalacaktım, bu durumda, tam olarak bind-template dizesini tam olarak bilemezdim ve bir şekilde sorgu metninden ayrıştırmak ve çözümü daha kırılgan yapmak zorunda kalırdım .
bukzor

Yeni URL'nin @ zzzeek SSS için docs.sqlalchemy.org/en/latest/faq/… olduğu anlaşılıyor .
Jim DeLaHunt

Yanıtlar:


168

Vakaların büyük çoğunluğunda, bir SQLAlchemy deyiminin veya sorgunun "dizgisi" şu kadar basittir:

print str(statement)

Bu hem ORM Queryhem de herhangi bir select()ifadeye veya başka bir ifadeye uygulanır.

Not : sqlalchemy belgelerinde aşağıdaki ayrıntılı cevap verilmektedir .

İfadeyi belirli bir lehçeye veya motora derlenmiş olarak almak için, ifadenin kendisi zaten birine bağlı değilse bunu compile () öğesine iletebilirsiniz :

print statement.compile(someengine)

veya motorsuz:

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

Bir ORM Querynesnesi verildiğinde , compile()yönteme ulaşmak için önce yalnızca .statement erişimcisine erişmemiz gerekir :

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

bağlı parametrelerin son dizgiye "inline" olacağı özgün şartla ilgili olarak, burada zorluk, SQLAlchemy'nin normal olarak bununla görevlendirilmemesidir, çünkü bu, Python DBAPI tarafından uygun şekilde ele alınır, bağlı parametreleri atlamaktan bahsetmemek muhtemelen modern web uygulamalarında en çok kullanılan güvenlik açıklarıdır. SQLAlchemy, DDL yayma gibi belirli durumlarda bu dizilimi yapma yeteneğine sahiptir. Bu işlevselliğe erişmek için şu adresteki 'literal_binds' bayrağını kullanabilirsiniz 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})

Yukarıdaki yaklaşım sadece ints ve stringler gibi temel tipler için desteklendiği uyarılarına sahiptir ve ayrıca bindparam önceden ayarlanmış bir değeri olmayan bir doğrudan kullanılırsa, bunu da dize edemez.

Desteklenmeyen türler için satır içi hazır oluşturmayı desteklemek TypeDecoratoriçin, bir TypeDecorator.process_literal_paramyöntem içeren hedef türüne bir a uygulayın :

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

gibi çıktı üreten:

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

2
Bu, dizelerin etrafına tırnak işareti koymaz ve bazı bağlı parametreleri çözmez.
bukzor

1
cevabın ikinci yarısı en son bilgilerle güncellendi.
zzzeek

2
@zzzeek Neden güzel baskı sorguları sqlalchemy'ye varsayılan olarak dahil edilmiyor? Gibi query.prettyprint(). Büyük sorgular ile hata ayıklama ağrısını son derece kolaylaştırır.
jmagnusson

2
@jmagnusson çünkü güzellik bakanın gözündedir :) @compilesGüzel baskı sistemleri uygulamak için herhangi bir sayıda üçüncü taraf paketi için geniş kancalar (örn. cursor_execute olayı, Python kayıt filtreleri , vb.) vardır.
zzzeek

1
@buzkor re: 1.0 giderilen oldu limiti bitbucket.org/zzzeek/sqlalchemy/issue/3034/...
zzzeek

66

Bu, python 2 ve 3'te çalışır ve öncekinden biraz daha temizdir, ancak SA> = 1.0 gerektirir.

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

Demo:

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

Bu çıktıyı verir: (python 2.7 ve 3.4'te test edilmiştir)

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
Bu harika ... Kolayca erişebilmemiz için bazı hata ayıklama kütüphanelerine eklemeniz gerekecek. Bu konuda ayak işi yaptığınız için teşekkürler. Çok karmaşık olması beni şaşırttı.
Corey O.

5
Yeni başlayanlar cursor.execute () bu dize için cazip çünkü bu kasıtlı olarak zor eminim. Ancak yetişkinlere rıza gösterme ilkesi python'da yaygın olarak kullanılmaktadır.
bukzor

Çok kullanışlı. Teşekkürler!
clime

Gerçekten çok iyi. Özgürlüğü aldım ve INSERT ve UPDATE ifadeleri (PY2 / PY3) dahil SQLAlchemy v0.7.9 - v1.1.15'i kapsayan stackoverflow.com/a/42066590/2127439 içine dahil ettim.
wolfmanx

çok hoş. ancak aşağıdaki gibi dönüştürülüyor. 1) sorgu (Tablo) .filter (Tablo.Column1.is_ (Yanlış) WHERE Sütun1 IS 0'a. 2) sorgu (Tablo) .filter (Tablo. Tablo) .filter (Table.Column1 == işlev (herhangi bir [1,2,3])) WHERE Sütun1 = yukarıdaki dönüşümlerin herhangi biri ('[1,2,3]') sözdiziminde yanlış.
Sekhar C

52

İstediğiniz şeyin yalnızca hata ayıklama sırasında mantıklı olduğu düşünüldüğünde, SQLAlchemy'yi echo=Truetüm SQL sorgularını günlüğe kaydetmek için başlatabilirsiniz . Örneğin:

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

Bu, yalnızca tek bir istek için de değiştirilebilir:

echo=False- eğer True, Motor tüm ifadeleri repr()ve parametre listelerinden birini varsayılan olarak motor günlüğüne kaydeder sys.stdout. echoÖznitelik Engineaçılıp günlüğünü açmak için herhangi bir zamanda değiştirilebilir. Dizeye ayarlanırsa "debug", sonuç satırları standart çıktıya da yazdırılır. Bu bayrak sonuçta bir Python kaydedicisini kontrol eder; Günlüğü doğrudan yapılandırma hakkında bilgi için bkz. Günlüğü Yapılandırma .

Kaynak: SQLAlchemy Engine Yapılandırması

Flask ile kullanılırsa,

app.config["SQLALCHEMY_ECHO"] = True

aynı davranışı elde etmek için.


6
Bu cevap çok daha yüksek olmayı hak ediyor .. ve bunun kullanıcıları flask-sqlalchemyiçin kabul edilen cevap olmalı.
jso

25

Bu amaçla derleme yöntemini kullanabiliriz . Gönderen docs :

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

Sonuç:

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

Dokümanlardan uyarı:

Bu tekniği asla web formları veya diğer kullanıcı girişi uygulamaları gibi güvenilir olmayan girdiden alınan dize içeriğiyle kullanmayın. SQLAlchemy'nin Python değerlerini doğrudan SQL dize değerlerine zorlama olanakları, güvenilmeyen girdilere karşı güvenli değildir ve iletilen veri türünü doğrulamaz. İlişkisel bir veritabanına DDL olmayan SQL deyimlerini programlı olarak çağırırken her zaman bağlı parametreleri kullanın.


13

Yani @ zzzeek adlı kullanıcının @ bukzor koduna yaptığı yorumlar üzerine kolayca "yazdırılabilir" bir sorgu almak için geldim:

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)

Şahsen ben sqlparseSQL reindent için kullanılan girintili olmayan kod okuma zor bir zaman var . İle kurulabilir pip install sqlparse.


@bukzor datatime.now()Python 3 + sqlalchemy 1.0 kullanılırken dışındaki tüm değerler çalışır . @ Zzzeek'in de çalışabilmesi için özel bir TypeDecorator oluşturma konusundaki tavsiyelerine uymanız gerekir.
jmagnusson

Bu biraz fazla spesifik. Tarih saatinde hiçbir piton ve sqlalchemy kombinasyonu çalışmaz. Ayrıca, py27'de, ascii olmayan unicode bir patlamaya neden olur.
bukzor

Görebildiğim kadarıyla, TypeDecorator yolu tablo tanımlarımı değiştirmemi gerektiriyor, bu da sadece sorgularımı görmek için makul bir gereklilik değil. Cevabınızı sizinkine ve zzzeek'inkine biraz daha yakın olacak şekilde düzenledim, ancak tablo tanımlarına düzgün şekilde dik olan özel bir lehçenin rotasını izledim.
bukzor

11

Bu kod @bukzor'dan gelen mevcut parlak cevaba dayanmaktadır. datetime.datetimeOracle'ın türüne özel render ekledimTO_DATE() .

Kodu veritabanınıza uyacak şekilde güncellemekten çekinmeyin:

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 halkının neden bu kadar basit bir operasyonun bu kadar zor olmasının makul olduğuna inandığını anlamıyorum .
bukzor

Teşekkür ederim! render_literal_value benim için iyi çalıştı. Benim tek değişiklik oldu: return "%s" % valueyerine return repr(value)şamandıra, int, uzun bölüm Python olarak uzun ürünler çıkışı çünkü 22Lyerine sadece22
OrganicPanda

Herhangi bir bindparam dize değeri ascii'de temsil edilemiyorsa, bu tarif (orijinalin yanı sıra) UnicodeDecodeError öğesini yükseltir. Bunu düzelten bir öz yayın gönderdim .
gsakkis

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

1
@bukzor - Yukarıdaki "makul" olup olmadığını sormak hatırlamıyorum bu yüzden gerçekten "inanıyorum" olduğunu söyleyemem - FWIW, değil! :) lütfen cevabımı gör.
zzzeek

8

Yukarıda verilen çözümlerin önemsiz sorgularla "işe yaramadığını" belirtmek isterim. Karşılaştığım bir sorun, sorunlara neden olan pgsql ARRAY gibi daha karmaşık türlerdi. Benim için sadece pgsql ARRAY'larla bile çalışan bir çözüm buldum:

ödünç alındı: https://gist.github.com/gsakkis/4572159

Bağlantılı kod, SQLAlchemy'nin daha eski bir sürümünü temel alıyor gibi görünüyor. _Mapper_zero_or_none özelliğinin mevcut olmadığını belirten bir hata alırsınız. İşte daha yeni bir sürümle çalışacak güncellenmiş bir sürüm, sadece _mapper_zero_or_none yerine bind ile değiştirin. Ayrıca, bunun pgsql dizileri için desteği vardır:

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

İç içe dizilerin iki düzeyinde test edilmiştir.


Lütfen nasıl kullanılacağına dair bir örnek gösterin? Thank you
slashdottir

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

Bu sayfanın benim için çalışan tek örneği bu! Teşekkürler !
9:43
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.