【问题标题】:SQLAlchemy filter on list attribute列表属性上的 SQLAlchemy 过滤器
【发布时间】:2019-09-29 17:07:44
【问题描述】:

我使用 Flask-SQLAlchemy 定义了以下模型:

"""models.py"""

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

skill_candidate = db.Table(
    'SkillCandidate',
    db.Column('skill_id', db.String, db.ForeignKey('skill.id')),
    db.Column('candidate_id', db.Integer, db.ForeignKey('candidate.id')))

class Candidate(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    skills = db.relationship("Skill", secondary=skill_candidate)

class Skill(db.Model):
    id = db.Column(db.String, primary_key=True)
    name = db.Column(db.String, nullable=False, unique=True)

我想要达到的目标如下: 我想返回所有拥有列表输入中提供的技能的候选人(甚至理想情况下,是一个 Skill_id 列表)

我尝试了以下方法:

def get_skilled_candidates(skill_ids):
    return Candidate.query.join(skill_candidate).\
       filter(and_(*[skill_candidate.c.skill_id == skill_id for skill_id in skill_ids])).\
            all()

目的是过滤每项技能的所有候选人,并用 and_ 语句组成

如果我使用包含 1 个项目的列表(它返回所有拥有该技能的候选人),效果很好,但如果我在输入列表中添加更多技能(即使我有符合标准的候选人)

【问题讨论】:

  • 您能否在查询中显示constraint_item_candidateconstraint_item_candidate.c 的确切含义?
  • 我的错误,这是一个错字。 constraint_item_candidate 实际上是skill_candidate,即 Skill 和 Candidate 的关联表。 Skill_candidate.c 是访问db.Table 实例的列字段的方式
  • 你需要关系除法/“for all”,意思是“不存在技能 id 不存在于 Skill_candidate”。一些例子:stackoverflow.com/questions/49438529/…stackoverflow.com/questions/42673699/…
  • 非常棒的精确定位,但我不能完全理解那个双重否定......我会在处理它后立即尝试写一个表达式
  • 我以为我有它,我尝试先发送原始查询:SQL select * from SkillCandidate where not (exists (select * from SkillCandidate where SkillCandidate.skill_id not in (1, 2))) 但它导致返回一个空结果(1 和 2 是所需技能的 id)

标签: python sqlalchemy flask-sqlalchemy


【解决方案1】:

如 cmets 中所述,您需要的是 FORALL 操作 (universal quantifier) 或 relational division

FORALL x ( p(x) )

可以表示为

NOT ( EXISTS x ( NOT ( p(x) ) ) )

如果您不了解FORALL 及其关系,这有点笨拙且难以推理。鉴于您的模型,它可能看起来像:

def get_skilled_candidates(skill_ids):
    # Form a temporary derived table using unions
    skills = db.union_all(*[
        db.select([db.literal(sid).label('skill_id')])
        for sid in skill_ids]).alias()

    return Candidate.query.\
        filter(
            ~db.exists().select_from(skills).where(
                ~db.exists().
                    where(db.and_(skill_candidate.c.skill_id == skills.c.skill_id,
                                  skill_candidate.c.candidate_id == Candidate.id)).
                    correlate_except(skill_candidate))).\
        all()

当然还有其他方式来表达相同的查询,例如:

def get_skilled_candidates(skill_ids):
    return Candidate.query.\
        join(skill_candidate).\
        filter(skill_candidate.c.skill_id.in_(skill_ids)).\
        group_by(Candidate.id).\
        having(db.func.count(skill_candidate.c.skill_id.distinct()) ==
               len(set(skill_ids))).\
        all()

本质上是通过计数检查所有技能 ID 是否匹配。

如果使用 Postgresql,你也可以这样做:

from sqlalchemy.dialects.postgresql import array_agg

def get_skilled_candidates(skill_ids):
    # The double filtering may seem redundant, but the WHERE ... IN allows
    # the query to use indexes, while the HAVING ... @> does the final filtering.
    return Candidate.query.\
        join(skill_candidate).\
        filter(skill_candidate.c.skill_id.in_(skill_ids)).\
        group_by(Candidate.id).\
        having(array_agg(skill_candidate.c.skill_id).contains(skill_ids)).\
        all()

这在某种程度上与其他答案中的部分 Python 解决方案相当。

此外,可以使用聚合 EVERY

def get_skilled_candidates(skill_ids):
    # Form a temporary derived table using unions
    skills = db.union_all(*[
        db.select([db.literal(sid).label('skill_id')])
        for sid in skill_ids]).alias()

    # Perform a CROSS JOIN between candidate and skills
    return Candidate.query.\
        join(skills, db.true()).\
        group_by(Candidate.id).\
        having(db.func.every(
            db.exists().
                where(db.and_(skill_candidate.c.skill_id == skills.c.skill_id,
                              skill_candidate.c.candidate_id == Candidate.id)).
                correlate_except(skill_candidate))).\
        all()

【讨论】:

  • 优秀。这就是我所缺少的:初始条件 ~db.exists().select_from(skills) 我也在考虑计数条件部分。我将尝试测试所有这些解决方案的性能
【解决方案2】:

您可以查询具有列表中任何技能的所有候选人,然后使用列表理解过滤结果。这可能不如@IljaEverilä 提到的关系划分方法那么高效,但它确实简化了查询方面。

skill_ids = ['id_1', 'id_2']
candidates = session.query(Candidate).\
    filter(Candidate.skills.any(Skill.id.in_(skill_ids)).\
    all()

candidates = [
    c for c in candidates
    if set(s.id for s in c.skills).issuperset(skill_ids)
]

【讨论】:

  • 我可以这样做,但我担心让 python 处理过滤而不是 sql 引擎对性能的影响
  • @AugBar - 是的,我刚刚意识到我错过了您问题的一个重要部分,您希望获得一份只有那些拥有所有的候选人的名单列表中的技能,对吗?
  • 他们必须至少具备列表中指定的所有技能
  • @AugBar - 编辑以产生所需的结果,但我猜想问题 cmets 中提到的关系划分方法会更高效。
猜你喜欢
  • 2017-03-24
  • 2023-04-02
  • 2012-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-17
  • 1970-01-01
相关资源
最近更新 更多