动态SQL WHERE子句生成

9 投票
3 回答
14389 浏览
提问于 2025-04-17 19:37

我在用Python和SQLite。现在我有一个可以生成我需要的SQL的函数,但感觉不太对劲。

def daily(self, host=None, day=None):
    sql = "SELECT * FROM daily WHERE 1"
    if host:
        sql += " AND host = '%s'" % (host,)
    if day:
        sql += " AND day = '%s'" % (day,)
    return sql

我可能以后还需要添加多个列和条件。

有没有更好的想法呢?

补充:我觉得不对劲的地方在于,我是通过字符串动态构建SQL的。这通常不是最好的做法。因为这样容易受到SQL注入攻击,还需要正确处理字符串。由于有些值是None,所以我不能使用占位符,因为这些值在WHERE条件中是不需要的。

3 个回答

0

psycopg2 有一个非常棒的 sql 模块,可以让你做很多事情,比如:

SQL("SELECT * FROM tbl WHERE {} = {}").format(Identifier(field_name), Literal(field_value))

这在 sqlite 中也可以做到,但你需要把输入转换成数据库使用的编码格式,这样才能避免一些更隐蔽的 SQL 注入攻击,就像 psycopg2 在后台处理的那样。

3

为了完整性,我发现pypika这个库非常方便(如果允许使用库的话):

https://pypika.readthedocs.io/en/latest/index.html

它可以像这样构建SQL查询:

from pypika import Query

q = Query._from('daily').select('*')
if host:
     q = q.where('host' == host)
if day:
     q = q.where('day' == day)
sql = str(q)
16

真的不想用字符串格式化来包含值。把这个工作交给数据库的API,通过SQL参数来处理。

使用参数的好处有:

  • 让数据库有机会准备好语句,并重用查询计划,这样可以提高性能。
  • 省去你处理值时需要正确转义的麻烦(包括避免SQL转义和SQL注入攻击的问题)。

因为SQLLite 支持命名的SQL参数,我建议你返回一个语句和一个包含参数的字典:

def daily(self, host=None, day=None):
    sql = "SELECT * FROM daily"
    where = []
    params = {}
    if host is not None:
        where.append("host = :host")
        params['host'] = host
    if day is not None:
        where.append("day = :day")
        params['day'] = day
    if where:
        sql = '{} WHERE {}'.format(sql, ' AND '.join(where))
    return sql, params

然后把这两个都传给cursor.execute()

cursor.execute(*daily(host, day))

生成SQL的过程会很快变得复杂,你可能想看看SQLAlchemy核心,用它来生成SQL。

对于你的例子,你可以生成:

from sqlalchemy import Table, Column, Integer, String, Date, MetaData

metadata = MetaData()
daily = Table('daily', metadata, 
    Column('id', Integer, primary_key=True),
    Column('host', String),
    Column('day', Date),
)
from sqlalchemy.sql import select

def daily(self, host=None, day=None):
    query = select([daily])
    if host is not None:
        query = query.where(daily.c.host == host)
    if day is not None:
        query = query.where(daily.c.day == day)
    return query

这个query对象可以应用额外的过滤条件,可以排序、分组,可以作为其他查询的子查询,连接在一起,最后发送去执行,这时SQLAlchemy会把它转换成适合你连接的特定数据库的SQL。

撰写回答