Фильтр SQLAlchemy для атрибута списка - PullRequest
2 голосов
/ 24 сентября 2019

У меня есть следующая модель, определенная с помощью 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_

Itхорошо работает, если я использую список из 1 элемента (он возвращает всех кандидатов, обладающих навыком), но не работает, если я добавляю больше навыков в список ввода (даже если у меня есть кандидаты в базу, которые соответствуют критериям)

Ответы [ 2 ]

3 голосов
/ 26 сентября 2019

Вы можете запросить всех кандидатов с любым из навыков в вашем списке, а затем отфильтровать результат с пониманием списка.Это может быть не так эффективно, как подход реляционного разделения, упомянутый в @ 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)
]
2 голосов
/ 28 сентября 2019

Как отмечается в комментариях, вам потребуется операция FORALL ( универсальный квантификатор ) или реляционное деление .

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

, который по сути проверяет счетчиком, что все идентификаторы уменийбыли сопоставлены.

При использовании 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()
...