简体   繁体   English

在Flask SQL-Alchemy中通过ForeignKey中的多个实例进行过滤

[英]Filtering by Multiple Instances within ForeignKey in Flask SQL-Alchemy

Let's say I have a model Toolbox, foreign key associated with a variety of Tools: 假设我有一个模型工具箱,与各种工具相关的外键:

class Toolbox(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    tools = db.relationship('Tools')

class Tool(db.Model):
    id = db.Column(db.Integer)
    name = db.Column(db.String(64), index=True)
    quantity = db.Column(db.Integer)
    toolbox_id = db.Column(db.Integer, db.ForeignKey('toolbox.id'))
    toolbox = db.relationship(Toolbox)

and a dictionary of tools and their quantities: 以及工具及其数量的字典:

tool_dict = {'screwdriver' : 3, 'wrench' : 1 }

How can I make query in SQLAlchemy that would return a list of Toolboxes that contain at least 3 screwdrivers and 1 wrench? 如何在SQLAlchemy中进行查询,该查询将返回至少包含3个螺丝刀和1个扳手的工具箱列表?

I got so far as: 我到目前为止:

t = Toolbox.query.join(Tool).filter(Tool.name.in_(tool_dict.keys()))

however this returns all toolboxes containing screwdrivers and wrenches regardless of quantity. 但是,这将返回所有包含螺丝刀和扳手的工具箱,无论数量如何。

One way to approach this would be to first select from tool relation a union of sets of (toolbox_id, name) tuples, where quantity is satisfied. 解决此问题的一种方法是,首先从tool关系中选择满足quantity(toolbox_id, name)元组集合的并集。 Then divide that found set with the names of the required tools, and you're left with the toolbox_id s you want. 然后找到的集除以所需工具的名称,然后剩下所需的toolbox_id

Another, in this case a lot simpler, way would be to select the intersection of toolbox ids from tools that satisfy the quantity, and then select the toolboxes that match the ids. 在这种情况下更简单的另一种方法是从满足数量的工具中选择工具箱ID的交集 ,然后选择与ID匹配的工具箱。

Initial data 初始数据

In [51]: db.session.add(Toolbox(tools=[
    ...:     Tool(name='hammer', quantity=1),
    ...:     Tool(name='screwdriver', quantity=3),
    ...:     Tool(name='wrench', quantity=1)]))

In [52]: db.session.add(Toolbox(tools=[
    ...:     Tool(name='hammer', quantity=1),
    ...:     Tool(name='screwdriver', quantity=2),
    ...:     Tool(name='wrench', quantity=1)]))

In [53]: db.session.add(Toolbox(tools=[
    ...:     Tool(name='hammer', quantity=1),
    ...:     Tool(name='screwdriver', quantity=3)]))

In [54]: db.session.add(Toolbox(tools=[
    ...:     Tool(name='hammer', quantity=1),
    ...:     Tool(name='wrench', quantity=1)]))

In [55]: db.session.add(Toolbox(tools=[
    ...:     Tool(name='hammer', quantity=1)]))

In [56]: db.session.commit()

Intersect 相交

Form the intersection: 形成交集:

In [9]: tools = db.intersect(*(db.session.query(Tool.toolbox_id).
   ...:                        filter(Tool.name == name,
   ...:                               Tool.quantity >= quantity)
   ...:                        for name, quantity in tool_dict.items())).alias()

Then select the Toolbox es where id is in the intersection: 然后选择Toolbox es,其中id在交点处:

In [10]: db.session.query(Toolbox).filter(Toolbox.id.in_(tools)).all()
Out[10]: [<__main__.Toolbox at 0x7f7ca781c048>]

In [11]: _[0].id
Out[11]: 1

Division

Form a CTE from the selection of tools, and an alias of that CTE, as we have to use it in an inner NOT EXISTS later: 通过选择工具和该CTE的别名来形成CTE,因为稍后我们必须在内部NOT EXISTS中使用它:

In [41]: tools = db.union(*(db.session.query(Tool.toolbox_id, Tool.name).
    ...:                    filter(Tool.name == name,
    ...:                           Tool.quantity >= quantity)
    ...:                    for name, quantity in tool_dict.items())).cte()

In [42]: tools_alias = tools.alias()

Form a relation of required tools: 形成所需工具的关系:

In [38]: required_tools = db.union(
    ...:     *(db.select([db.literal(name).label('tool_name')])
    ...:       for name in tool_dict.keys())).alias()

This could be made a bit simpler in some DBs, for example in Postgresql you could do: 在某些数据库中,这可能会变得简单一些,例如在Postgresql中,您可以执行以下操作:

from sqlalchemy.dialects.postgresql import array
required_tools = func.unnest(array(list(tool_dict.keys()))).alias()

Perform division: 执行除法:

In [63]: db.session.query(tools.c.tool_toolbox_id.distinct()).\
    ...:     filter(~db.session.query().select_from(required_tools).
    ...:            filter(~db.session.query().select_from(tools_alias).
    ...:                   filter(tools_alias.c.tool_toolbox_id == tools.c.tool_toolbox_id,
    ...:                          tools_alias.c.tool_name == required_tools.c.tool_name).
    ...:                   exists().correlate_except(tools_alias)).
    ...:            exists()).all()
Out[63]: [(1)]

The doubly nested negation is an eyesore, but it answers the query "find those toolboxes (ids), for which no such required tool exists that is not in the toolbox". 双重嵌套的否定是令人讨厌的,但是它回答了查询“查找那些工具箱(id),对于这些工具箱,不存在不在工具箱中的所需工具”。

To fetch the desired Toolbox es directly we can tweak the query a bit to select from toolbox relation at top level and the formed union at the innermost level only: 要直接获取所需的Toolbox ,我们可以对查询进行一些调整,以从顶级的toolbox关系中选择,而仅从最内层的层次中选择形成的并集:

In [16]: tools = db.union(*(db.session.query(Tool.toolbox_id, Tool.name).
    ...:                    filter(Tool.name == name,
    ...:                           Tool.quantity >= quantity)
    ...:                    for name, quantity in tool_dict.items())).alias()

In [17]: db.session.query(Toolbox).\
    ...:     filter(~db.session.query().select_from(required_tools).
    ...:            filter(~db.session.query().select_from(tools).
    ...:                   filter(tools.c.tool_toolbox_id == Toolbox.id,
    ...:                          tools.c.tool_name == required_tools.c.tool_name).
    ...:                   exists().correlate_except(tools)).
    ...:            exists()).all()
Out[17]: [<__main__.Toolbox at 0x7f302f589898>]

In [18]: _[0].id
Out[18]: 1

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM