ฉันจะรับแบบสอบถาม SQL ดิบที่คอมไพล์แล้วจากนิพจน์ SQLAlchemy ได้อย่างไร


106

ฉันมีวัตถุแบบสอบถาม SQLAlchemy และต้องการรับข้อความของคำสั่ง 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ที่ฉันต้องการข้อมูล

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

ฉันเริ่มได้รับข้อความว่า SQLAlchemy ไม่ต้องการให้ฉันรู้แบบสอบถามพื้นฐานเนื่องจากมันทำลายลักษณะทั่วไปของอินเทอร์เฟซของนิพจน์ API กับ DB-API ที่แตกต่างกันทั้งหมด ฉันไม่สนใจว่าการสืบค้นจะถูกดำเนินการก่อนที่ฉันจะพบว่ามันคืออะไร ฉันแค่อยากจะรู้!

คำตอบ:


108

บล็อกนี้ให้คำตอบที่อัปเดต

การอ้างอิงจากโพสต์บล็อกนี่เป็นคำแนะนำและใช้ได้ผลสำหรับฉัน

>>> 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 สำหรับคำตอบ! ฉันหวังว่ามันจะช่วยคนอื่น ๆ ที่มาค้นหาที่นี่


2
มีวิธีง่ายๆในการรับค่าเป็นพจนานุกรมหรือไม่?
Damien

6
@Damien ให้c = q.statement.compile(...)คุณจะได้รับc.params
Hannele

1
โพสต์ถูกแท็กด้วย mysql ดังนั้นรายละเอียด postgresql ในคำตอบนี้จึงไม่เกี่ยวข้องจริงๆ
Hannele

4
ถ้าฉันเข้าใจ OP อย่างถูกต้องเขาต้องการคำถามสุดท้าย พิมพ์ด้วยการระบุภาษา (ที่นี่ postgres) ยังคงให้ฉันตัวยึดตำแหน่งแทนค่าที่แท้จริง @ คำตอบของ Matt ได้ผล การรับ SQL ด้วยตัวยึดตำแหน่งสามารถทำได้ง่ายขึ้นด้วยas_scalar()-method ของQuery.
Patrick B.

1
@PatrickB. ฉันเห็นด้วย. คำตอบของ Matt ควรถือเป็นคำตอบที่ "ถูกต้อง" str(q)ฉันจะได้รับผลเช่นเดียวกับการนี้โดยเพียงแค่การทำ
André C. Andersen

97

เอกสารที่ใช้literal_bindsในการพิมพ์แบบสอบถามqรวมทั้งพารามิเตอร์:

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

วิธีการข้างต้นมีข้อแม้ว่ารองรับเฉพาะประเภทพื้นฐานเช่น ints และ strings และยิ่งไปกว่านั้นถ้าใช้ bindparam () ที่ไม่มีค่าที่ตั้งไว้ล่วงหน้าโดยตรงก็จะไม่สามารถสตริงได้เช่นกัน

เอกสารประกอบยังออกคำเตือนนี้:

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


ขอบคุณ! สิ่งนี้มีประโยชน์มากทำให้ฉันใช้ฟังก์ชัน read_sql ของแพนด้าได้อย่างไม่ลำบาก!
Justin Palmer

24

สิ่งนี้ควรใช้ได้กับ 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)

2
ขอบคุณสำหรับสิ่งนี้! น่าเศร้าที่ฉันใช้ MySQL ดังนั้นภาษาถิ่นของฉันจึงเป็น "ตำแหน่ง" และจำเป็นต้องมีรายการพารามิเตอร์แทนพจนานุกรม ขณะนี้กำลังพยายามทำให้ตัวอย่างของคุณใช้งานได้ ..
cce

กรุณาอย่าใช้adaptในลักษณะนี้ ในการโทรขั้นต่ำเตรียม () กับค่าที่ส่งคืนจากค่านี้ในแต่ละครั้งโดยให้การเชื่อมต่อเป็นอาร์กิวเมนต์ดังนั้นจึงสามารถทำการอ้างอิงที่เหมาะสมได้
Alex Gaynor

@ อเล็กซ์: อะไรคือวิธีที่ถูกต้องในการอ้างถึงอย่างเหมาะสมกับ Psycopg? (นอกเหนือจากการโทรเตรียม () ในค่าส่งคืนซึ่งดูเหมือนว่าคุณจะบอกเป็นนัยว่าไม่เหมาะสม)
albertov

ขออภัยฉันคิดว่าวลีของฉันไม่ดีตราบใดที่คุณโทรหา obj.prepare (การเชื่อมต่อ) คุณก็น่าจะโอเค เนื่องจาก API "ดี" ที่ libpq มีให้สำหรับการอ้างอิงจำเป็นต้องมีการเชื่อมต่อ (และมีสิ่งต่างๆเช่นการเข้ารหัสสำหรับสตริง Unicode)
Alex Gaynor

1
ขอบคุณ. ฉันได้ลองเรียกprepareค่าตอบแทนแล้ว แต่ดูเหมือนว่าจะไม่มีวิธีการAttributeError: 'psycopg2._psycopg.AsIs' object has no attribute 'prepare'ดังกล่าว: ฉันใช้ psycopg2 2.2.1 BTW
albertov

18

สำหรับแบ็กเอนด์ MySQLdb ฉันแก้ไขคำตอบที่ยอดเยี่ยมของ albertov (ขอบคุณมาก!) เล็กน้อย ฉันแน่ใจว่าสามารถรวมเข้าด้วยกันเพื่อตรวจสอบว่าcomp.positionalเป็นTrueแต่เกินขอบเขตของคำถามนี้เล็กน้อย

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)

สุดยอด! ฉันแค่ต้องการรายการพารามิเตอร์ที่ถูกผูกไว้ที่ถูกส่งไปยัง MySQL และแก้ไขด้านบนเพื่อให้return tuple(params)ทำงานได้อย่างมีเสน่ห์! คุณช่วยฉันไว้นับไม่ถ้วนที่ต้องไปตามถนนที่แสนเจ็บปวด
horcle_buzz

17

สิ่งนี้คือ sqlalchemy ไม่เคยผสมข้อมูลกับข้อความค้นหาของคุณ แบบสอบถามและข้อมูลจะถูกส่งแยกกันไปยังโปรแกรมควบคุมฐานข้อมูลพื้นฐานของคุณ - การแก้ไขข้อมูลจะเกิดขึ้นในฐานข้อมูลของคุณ

Sqlalchemy ส่งแบบสอบถามตามที่คุณเห็นในstr(myquery)ฐานข้อมูลและค่าต่างๆจะอยู่ในทูเพิลแยกต่างหาก

คุณสามารถใช้วิธีการบางอย่างที่คุณแก้ไขข้อมูลด้วยแบบสอบถามด้วยตัวคุณเอง (ตามที่ albertov แนะนำด้านล่าง) แต่นั่นไม่ใช่สิ่งเดียวกับที่ sqlalchemy กำลังดำเนินการ


ทำไมมันไม่เหมือนกันล่ะ? ฉันเข้าใจว่า DB-API กำลังทำธุรกรรมอาจจะเรียงลำดับคิวรีใหม่ ฯลฯ แต่จะแก้ไขการสืบค้นของฉันมากกว่านี้ได้หรือไม่
cce

1
@cce: คุณกำลังพยายามค้นหาข้อความค้นหาสุดท้าย SELECT id WHERE date_added <= %s AND date_added >= %s ORDER BY count DESC เป็นคำถามสุดท้าย สิ่งเหล่านี้%sจะถูกส่งไปยังฐานข้อมูลโดย sqlalchemy - sqlalchemy ไม่เคยใส่ข้อมูลจริงแทนที่% s
nosklo

@cce: โมดูล dbapi บางโมดูลไม่ทำเช่นนั้นซึ่งมักจะทำโดยฐานข้อมูลเอง
nosklo

1
AHA ผมเห็นสิ่งที่คุณกำลังจะบอกว่าขอบคุณ - ขุดต่อไปในsqlalchemy.dialects.mysql.mysqldb, do_executemany()ผ่านคำสั่งและพารามิเตอร์แยกเคอร์เซอร์ MySQLdb เย้!
cce

12

ก่อนอื่นให้ฉันเกริ่นก่อนโดยบอกว่าฉันคิดว่าคุณกำลังทำสิ่งนี้เพื่อจุดประสงค์ในการดีบักเป็นหลักฉันไม่แนะนำให้พยายามแก้ไขคำสั่งภายนอก 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โดยไม่จำเป็นต้องใช้


จากภายในดีบักเกอร์ PyCharm สิ่งต่อไปนี้ใช้ได้กับฉัน ... qry.compile (). params
Ben

ที่น่าสนใจอาจเป็นเพราะ SQLAlchemy มีการเปลี่ยนแปลงเล็กน้อยตั้งแต่ฉันเขียนคำตอบนี้
Hannele

10

สำหรับแบ็กเอนด์ postgresql โดยใช้ psycopg2 คุณสามารถฟัง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"}}'}

4

โซลูชันต่อไปนี้ใช้ SQLAlchemy Expression Language และทำงานร่วมกับ 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())

เราสามารถสร้างคำสั่งแบบสอบถามโดยใช้ฟังก์ชันเลือก

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

ต่อไปเราสามารถรวบรวมคำสั่งลงในวัตถุแบบสอบถาม

query = statement.compile()

โดยค่าเริ่มต้นคำสั่งจะถูกคอมไพล์โดยใช้การใช้งาน 'ชื่อ' พื้นฐานที่เข้ากันได้กับฐานข้อมูล SQL เช่น SQLite และ Oracle หากคุณต้องการระบุภาษาถิ่นเช่น PostgreSQL คุณสามารถทำได้

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

หรือถ้าคุณต้องการระบุภาษาถิ่นเป็น SQLite อย่างชัดเจนคุณสามารถเปลี่ยนรูปแบบพารามิเตอร์จาก '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 )

คำตอบนี้ดีกว่า / แตกต่างจากคำตอบของ AndyBarr ที่โพสต์เมื่อ 2 ปีก่อนอย่างไร
Piotr Dobrogost

คำตอบของ AndyBarr ประกอบด้วยตัวอย่างของการสร้างคำสั่งแบบสอบถามด้วย DBSession ในขณะที่คำตอบนี้รวมถึงตัวอย่างโดยใช้ API ที่เปิดเผยและเลือกวิธีการ ในส่วนที่เกี่ยวกับการรวบรวมคำสั่งแบบสอบถามด้วยภาษาถิ่นคำตอบจะเหมือนกัน ฉันใช้ SQLAlchemy เพื่อสร้างแบบสอบถามดิบแล้วดำเนินการกับ adbapi ของ Twister สำหรับกรณีการใช้งานนี้การรู้วิธีคอมไพล์คิวรีโดยไม่มีเซสชันและการแยกสตริงคิวรีและพารามิเตอร์มีประโยชน์
eric

3

คุณสามารถใช้เหตุการณ์จากตระกูลConnectionEvents : after_cursor_executeหรือbefore_cursor_execute.

ใน sqlalchemy UsageRecipes by @zzzeek คุณจะพบตัวอย่างนี้:

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

ที่นี่คุณสามารถได้รับการเข้าถึงของคุณคำสั่ง


3

ดังนั้นเมื่อรวบรวมคำตอบที่แตกต่างกันจำนวนมากเข้าด้วยกันฉันจึงได้สิ่งที่ต้องการ: ชุดโค้ดง่ายๆที่จะดร็อปและเป็นครั้งคราว แต่เชื่อถือได้ (เช่นจัดการข้อมูลทุกประเภท) คว้า SQL ที่คอมไพล์แล้วที่ส่งไปยังของฉัน แบ็กเอนด์ Postgres เพียงแค่ซักถามตัวเอง:

from sqlalchemy.dialects import postgresql

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

compiled_query = query.statement.compile(
    dialect=postgresql.dialect(),
    compile_kwargs={"literal_binds": True}
)
mogrified_query = session.connection().connection.cursor().mogrify(
    str(compiled_query),
    compiled_query.params
)

print("compiled SQL = {s}".format(mogrified_query.decode())

0

ฉันคิดว่าคำชี้แจงอาจใช้เคล็ดลับ: 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

คำชี้แจงจะไม่แสดงว่าพารามิเตอร์คืออะไรหากคุณตั้งค่าตัวกรองบางประเภทไว้
Hannele

0

ฉันได้สร้างฟังก์ชั่นเล็ก ๆ น้อย ๆ นี้ที่ฉันนำเข้าเมื่อฉันต้องการพิมพ์ข้อความค้นหาทั้งหมดโดยพิจารณาว่าฉันอยู่ระหว่างการทดสอบเมื่อภาษาถูกผูกไว้แล้ว:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.