动态添加过滤器到SQLAlchemy TextClause

时间:2017-04-20 15:42:25

标签: python sqlalchemy flask-sqlalchemy

假设我有一个SQLAlchemy表,它看起来像:

class Country:
    name = VARCHAR
    population = INTEGER
    continent = VARCHAR
    num_states = INTEGER

我的申请允许查看所有国家/地区的姓名和人口。所以我有TextClause看起来像

"select name, population from Country"

我在我的应用程序中允许原始查询,因此我没有选择将其更改为可选择。

在运行时,我想允许我的用户选择一个字段名称并输入一个我想允许过滤的字段值。例如:用户可以说我只想查看大陆是亚洲的国家的名称和人口。所以我动态地想要添加过滤器

.where(Country.c.continent == 'Asia')

但我无法将.where添加到TextClause

同样,我的用户可以选择查看num_states大于10的国家/地区的名称和人口。所以我动态地想要添加过滤器

.where(Country.c.num_states > 10)

但我再次无法将.where添加到TextClause

我有什么方法可以解决这个问题?

子查询可以以任何方式帮助吗?

3 个答案:

答案 0 :(得分:1)

如果不解析查询,这在一般意义上是不可行的。在关系代数术语中,用户将投影和选择操作应用于表,并且您希望对其应用选择操作。由于用户可以应用任意投影(例如用户耗材SELECT id FROM table),因此无法保证始终在顶部应用过滤器,因此您必须在用户之前应用过滤器确实。这意味着您需要将其重写为SELECT id FROM (some subquery),这需要解析用户的查询。

但是,我们可以通过让数据库引擎为您进行解析,根据您使用的数据库进行排序。这样做的方法是使用CTE,基本上用CTE对表名进行阴影处理。

使用您的示例,它看起来如下所示。用户提供查询

SELECT name, population FROM country;

您使用CTE隐藏country

WITH country AS (
  SELECT * FROM country
  WHERE continent = 'Asia'
) SELECT name, population FROM country;

不幸的是,由于SQLAlchemy的CTE支持的工作方式,很难让它为TextClause生成CTE。解决方案是使用自定义编译扩展,基本上自己生成字符串,如下所示:

class WrappedQuery(Executable, ClauseElement):
    def __init__(self, name, outer, inner):
        self.name = name
        self.outer = outer
        self.inner = inner

@compiles(WrappedQuery)
def compile_wrapped_query(element, compiler, **kwargs):
    return "WITH {} AS ({}) {}".format(
        element.name,
        compiler.process(element.outer),
        compiler.process(element.inner))

c = Country.__table__
cte = select(["*"]).select_from(c).where(c.c.continent == "Asia")
query = WrappedQuery("country", cte, text("SELECT name, population FROM country"))
session.execute(query)

从我的测试来看,这仅适用于PostgreSQL。 SQLite和SQL Server都将其视为递归而不是阴影,MySQL不支持CTE。

答案 1 :(得分:0)

请根据条件添加过滤器。 filter用于添加sqlalchemy中的条件。

Country.query.filter(Country.num_states > 10).all()

你也可以这样做:

query = Country.query.filter(Country.continent == 'Asia')
if user_input == 'states':
    query = query.filter(Country.num_states > 10)
query = query.all()

答案 2 :(得分:0)

我在文档中找不到任何好的东西。我最终只是采用字符串处理....但至少它有效!

from sqlalchemy.sql import text

query = """select name, population from Country"""

if continent is not None:
    additional_clause = """WHERE continent = {continent};"""
    query = query + additional_clause
    text_clause = text(
        query.format(
            continent=continent,
        ),
    )
else:
     text_clause = text(query)

with sql_connection() as conn:
    results = conn.execute(text_clause)

您也可以使用更多子句链接此逻辑,但您必须为第一个WHERE子句创建一个布尔标志,然后对后续的子句使用AND。