簡體   English   中英

如何從 SQLAlchemy 表達式中獲取原始的、已編譯的 SQL 查詢?

[英]How do I get a raw, compiled SQL query from a SQLAlchemy expression?

我有一個 SQLAlchemy 查詢 object 並希望獲取已編譯的 SQL 語句的文本,其所有參數都已綁定(例如,沒有%s或其他等待語句編譯器或 MySQLdb 方言引擎綁定的變量等)。

在查詢上調用str()會顯示如下內容:

SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC

我試過查看 query._params 但它是一個空字典。 我使用sqlalchemy.ext.compiler.compiles裝飾器的這個例子編寫了我自己的編譯器,但即使那里的語句仍然有%s我想要數據的地方。

我不太清楚何時將我的參數混入以創建查詢; 在檢查查詢 object 時,它們始終是一個空字典(盡管查詢執行得很好,並且當您打開 echo 日志記錄時引擎將其打印出來)。

我開始收到 SQLAlchemy 不想讓我知道底層查詢的消息,因為它打破了所有不同 DB-API 的表達式 API 接口的一般性質。 我不介意在我發現它是什么之前是否執行了查詢; 我只是想知道!

博客提供了更新的答案。

引用博客文章,這是建議並為我工作。

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

其中 q 定義為:

>>> q = DBSession.query(model.Name).distinct(model.Name.value) \
             .order_by(model.Name.value)

或者只是任何一種session.query()

感謝 Nicolas Cadou 的回答! 我希望它可以幫助到這里搜索的其他人。

文檔使用literal_binds打印包含參數的查詢q

print(q.statement.compile(compile_kwargs={"literal_binds": True}))

上面的方法有一個警告,它只支持基本類型,例如整數和字符串,而且如果直接使用沒有預設值的 bindparam() ,它也無法將其字符串化。

該文檔還發出此警告:

切勿將此技術用於從不受信任的輸入(例如來自 Web 表單或其他用戶輸入應用程序)接收的字符串內容。 SQLAlchemy 將 Python 值強制轉換為直接 SQL 字符串值的工具對於不受信任的輸入是不安全的,並且不會驗證傳遞的數據類型。 在對關系數據庫以編程方式調用非 DDL SQL 語句時,始終使用綁定參數。

這應該適用於 Sqlalchemy >= 0.6

from sqlalchemy.sql import compiler

from psycopg2.extensions import adapt as sqlescape
# or use the appropiate escape function from your db driver

def compile_query(query):
    dialect = query.session.bind.dialect
    statement = query.statement
    comp = compiler.SQLCompiler(dialect, statement)
    comp.compile()
    enc = dialect.encoding
    params = {}
    for k,v in comp.params.iteritems():
        if isinstance(v, unicode):
            v = v.encode(enc)
        params[k] = sqlescape(v)
    return (comp.string.encode(enc) % params).decode(enc)

問題是,sqlalchemy 永遠不會將數據與您的查詢混合在一起。 查詢和數據分別傳遞給您的底層數據庫驅動程序 - 數據的插值發生在您的數據庫中。

Sqlalchemy 將您在str(myquery)看到的查詢傳遞給數據庫,並且這些值將放在一個單獨的元組中。

您可以使用某種方法,自己用查詢插入數據(如下面的 albertov 建議),但這與 sqlalchemy 正在執行的不同。

對於 MySQLdb 后端,我稍微修改了 albertov 的精彩答案(非常感謝!)。 我確信它們可以合並以檢查comp.positionalTrue但這稍微超出了這個問題的范圍。

def compile_query(query):
    from sqlalchemy.sql import compiler
    from MySQLdb.converters import conversions, escape

    dialect = query.session.bind.dialect
    statement = query.statement
    comp = compiler.SQLCompiler(dialect, statement)
    comp.compile()
    enc = dialect.encoding
    params = []
    for k in comp.positiontup:
        v = comp.params[k]
        if isinstance(v, unicode):
            v = v.encode(enc)
        params.append( escape(v, conversions) )
    return (comp.string.encode(enc) % tuple(params)).decode(enc)

首先讓我先說我假設您這樣做主要是為了調試目的——我不建議嘗試修改 SQLAlchemy fluent API 之外的語句。

不幸的是,似乎沒有一種簡單的方法來顯示包含查詢參數的編譯語句。 SQLAlchemy 實際上並沒有將參數放入語句中——它們作為字典傳遞到數據庫引擎中 這允許特定於數據庫的庫處理諸如轉義特殊字符以避免 SQL 注入之類的事情。

但是,您可以相當輕松地分兩步完成此操作。 要獲取語句,您可以按照已經顯示的方式進行操作,只需打印查詢:

>>> print(query)
SELECT field_1, field_2 FROM table WHERE id=%s;

您可以使用 query.statement 更近一步,以查看參數名稱。 注意:id_1下面與%s以上 - 在這個非常簡單的例子中並不是真正的問題,但可能是更復雜語句中的關鍵。

>>> print(query.statement)
>>> print(query.statement.compile()) # seems to be equivalent, you can also
                                     # pass in a dialect if you want
SELECT field_1, field_2 FROM table WHERE id=:id_1;

然后,您可以通過獲取編譯語句的params屬性來獲取參數的實際值:

>>> print(query.statement.compile().params)
{u'id_1': 1} 

這至少適用於 MySQL 后端; 我希望它對於 PostgreSQL 也足夠通用,而無需使用psycopg2

對於使用 psycopg2 的 postgresql 后端,您可以監聽do_execute事件,然后使用游標、語句和類型強制參數以及Cursor.mogrify()來內聯參數。 您可以返回 True 以防止實際執行查詢。

import sqlalchemy

class QueryDebugger(object):
    def __init__(self, engine, query):
        with engine.connect() as connection:
            try:
                sqlalchemy.event.listen(engine, "do_execute", self.receive_do_execute)
                connection.execute(query)
            finally:
                sqlalchemy.event.remove(engine, "do_execute", self.receive_do_execute)

    def receive_do_execute(self, cursor, statement, parameters, context):
        self.statement = statement
        self.parameters = parameters
        self.query = cursor.mogrify(statement, parameters)
        # Don't actually execute
        return True

示例用法:

>>> engine = sqlalchemy.create_engine("postgresql://postgres@localhost/test")
>>> metadata = sqlalchemy.MetaData()
>>> users = sqlalchemy.Table('users', metadata, sqlalchemy.Column("_id", sqlalchemy.String, primary_key=True), sqlalchemy.Column("document", sqlalchemy.dialects.postgresql.JSONB))
>>> s = sqlalchemy.select([users.c.document.label("foobar")]).where(users.c.document.contains({"profile": {"iid": "something"}}))
>>> q = QueryDebugger(engine, s)
>>> q.query
'SELECT users.document AS foobar \nFROM users \nWHERE users.document @> \'{"profile": {"iid": "something"}}\''
>>> q.statement
'SELECT users.document AS foobar \nFROM users \nWHERE users.document @> %(document_1)s'
>>> q.parameters
{'document_1': '{"profile": {"iid": "something"}}'}

以下解決方案使用 SQLAlchemy 表達式語言並與 SQLAlchemy 1.1 配合使用。 該解決方案沒有將參數與查詢混合(按照原作者的要求),而是提供了一種使用 SQLAlchemy 模型為不同的 SQL 方言生成 SQL 查詢字符串和參數字典的方法。 該示例基於教程http://docs.sqlalchemy.org/en/rel_1_0/core/tutorial.html

鑒於班級,

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class foo(Base):
    __tablename__ = 'foo'
    id = Column(Integer(), primary_key=True)
    name = Column(String(80), unique=True)
    value = Column(Integer())

我們可以使用select函數生成查詢語句。

from sqlalchemy.sql import select    
statement = select([foo.name, foo.value]).where(foo.value > 0)

接下來,我們可以將語句編譯成查詢對象。

query = statement.compile()

默認情況下,該語句是使用與 SQLite 和 Oracle 等 SQL 數據庫兼容的基本“命名”實現編譯的。 如果需要指定像PostgreSQL這樣的方言,可以這樣做

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

或者,如果您想明確指定方言為 SQLite,您可以將 paramstyle 從 'qmark' 更改為 'named'。

from sqlalchemy.dialects import sqlite
query = statement.compile(dialect=sqlite.dialect(paramstyle="named"))

從查詢對象中,我們可以提取查詢字符串和查詢參數

query_str = str(query)
query_params = query.params

最后執行查詢。

conn.execute( query_str, query_params )

您可以使用ConnectionEvents系列中的事件: after_cursor_executebefore_cursor_execute

@zzzeek的 sqlalchemy UsageRecipes 中,您可以找到以下示例:

Profiling

...
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement,
                        parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())
    logger.debug("Start Query: %s" % statement % parameters)
...

在這里您可以訪問您的聲明

更新:提出了另一種情況,這里以前的解決方案沒有正確生成正確的 SQL 語句。 在深入了解 SQLAlchemy 之后,很明顯您不僅需要針對特定​​方言進行編譯,還需要獲取已編譯的查詢並將其初始化為正確的 DBAPI 連接上下文。 否則,不會執行類型綁定處理器之類的事情,並且不會正確轉換 JSON.NULL 之類的值。

請注意,這使得該解決方案非常適合 Flask + Flask-SQLAlchemy + psycopg2 + PostgreSQL。 您可能需要通過更改方言和引用連接的方式將此解決方案轉換為您的環境。 但是,我非常有信心這會為所有數據類型生成准確的 SQL。

下面的結果是一種簡單的方法,可以通過查詢查詢本身偶爾但可靠地獲取將發送到我的 PostgreSQL 后端的准確的、已編譯的 SQL:

import sqlalchemy.dialects.postgresql.psycopg2

from flask import current_app

def query_to_string(query):
    dialect = sqlalchemy.dialects.postgresql.psycopg2.dialect()
    compiled_query = query.statement.compile(dialect=dialect)
    sqlalchemy_connection = current_app.db.session.connection()
    context = dialect.execution_ctx_cls._init_compiled(
        dialect,
        sqlalchemy_connection,
        sqlalchemy_connection.connection,
        compiled_query,
        None
    )
    mogrified_query = sqlalchemy_connection.connection.cursor().mogrify(
        context.statement,
        context.parameters[0]
    )
    return mogrified_query.decode()

query = [ .... some ORM query .... ]

print(f"compiled SQL = {query_to_string(query)}")

我認為 .statement 可能會起作用: http ://docs.sqlalchemy.org/en/latest/orm/query.html?highlight=query

>>> local_session.query(sqlalchemy_declarative.SomeTable.text).statement
<sqlalchemy.sql.annotation.AnnotatedSelect at 0x6c75a20; AnnotatedSelectobject>
>>> x=local_session.query(sqlalchemy_declarative.SomeTable.text).statement
>>> print(x)
SELECT sometable.text 
FROM sometable

我創建了這個小函數,當我想打印完整的查詢時,我導入了這個小函數,考慮到我正處於測試中,而方言已經綁定:

import re

def print_query(query):
    regex = re.compile(":(?P<name>\w+)")
    params = query.statement.compile().params
    sql = regex.sub("'{\g<name>}'", str(query.statement)).format(**params)
    print(f"\nPrinting SQLAlchemy query:\n\n")
    print(sql)
    return sql

如果 SQLAlchemy 你使用的是 PyMySQL,你可以做一個技巧。

我很着急,浪費了很多時間,所以我改變了驅動程序來打印當前語句的參數。

SQLAlchemy 有意不支持文字值的完全字符串化。

但是 PyMySQL 具有執行此操作的“mogrify”方法,但是,SQLALchemy 在使用 ORM 插入/更新(當它控制游標時)如 db.add 或 commit/flush(用於更新)時沒有調用它的 HOOK。

所以,只是 go 驅動程序正在使用的地方(知道在哪里使用):pip 顯示 pycharm

在文件夾中,找到並編輯文件 cursors.py。

在方法中:

def execute(self, query, args=None):

線下:

query = self.mogrify(query, args)

只需添加:

print(query)

將像魅力一樣工作,調試,解決問題並刪除打印。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM