关系Secondaryjoin中使用的表上的SQLAlchemy筛选器查询

时间:2018-07-23 12:43:28

标签: python orm sqlalchemy

在删除所有“业务”逻辑后,我的表设置如下所示,

Source

Sport 1----* Competition


Sport 1----* SourceSport

Competition 1----* SourceCompetition


Source 1----* SourceSport

Source 1----* SourceCompetition


import logging
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship, foreign, joinedload
from sqlalchemy.sql.elements import and_
from typing import List, cast

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
Model = db.Model


class Source(Model):
    id = Column(Integer, primary_key=True)

    key = Column(String(10), unique=True)


class Sport(Model):
    id = Column(Integer, primary_key=True)

    name = Column(String(50), unique=True)


class Competition(Model):
    id = Column(Integer, primary_key=True)

    key = Column(String(6), unique=True)

    sport_id = Column(Integer, ForeignKey(Sport.id))
    sport = relationship(Sport, foreign_keys=(sport_id,))


class _Sourcer(Model):
    __abstract__ = True

    @declared_attr
    def source_id(self):
        return Column(Integer, ForeignKey(Source.id),
                      primary_key=True)

    @declared_attr
    def source(self):
        return relationship(Source,
                            foreign_keys=(self.source_id,))


class SourceSport(_Sourcer):
    # source_competitions: List['SourceCompetition']
    #
    sport_id = Column(Integer, ForeignKey(Sport.id),
                      primary_key=True)
    sport = relationship(Sport,
                         foreign_keys=(sport_id,))

    source_sport_id = Column(String(20))

    competitions = relationship(
        Competition,
        primaryjoin=sport_id == foreign(Competition.sport_id),
        uselist=True)

    source_competitions: List['SourceCompetition'] = relationship(
        lambda: SourceCompetition,
        primaryjoin=lambda: and_(
            SourceSport.sport_id == Competition.sport_id,
        ),
        secondary=Competition.__table__,
        secondaryjoin=lambda: and_(
            SourceSport.source_id == SourceCompetition.source_id,
            Competition.id == SourceCompetition.competition_id,
        ),
        innerjoin=True,
        uselist=True,
        back_populates='source_sport')


class SourceCompetition(_Sourcer):
    competition_id = Column(Integer, ForeignKey(Competition.id),
                            primary_key=True)
    competition = relationship(Competition,
                               foreign_keys=(competition_id,))

    source_competition_id = Column(String(20))

    sport = relationship(
            Sport,
            primaryjoin=competition_id == Competition.id,
            secondary=Competition.__table__,
            secondaryjoin=Competition.sport_id == Sport.id,
            uselist=False
        )

    source_sport: SourceSport = relationship(
        SourceSport,
        primaryjoin=and_(
            competition_id == Competition.id,
        ),
        secondary=Competition.__table__,
        secondaryjoin=lambda: and_(
            SourceSport.source_id == SourceCompetition.source_id,
            Competition.sport_id == SourceSport.sport_id,
        ),
        innerjoin=True,
        uselist=False,
        back_populates=SourceSport.source_competitions.key)


def to_basic(v, exclude):
    return (
        [as_dict(i, exclude) for i in v] if isinstance(v, list) else
        as_dict(v, exclude) if hasattr(v, '__dict__') else
        v)


def as_dict(v, exclude):
    # noinspection PyProtectedMember
    return {k: to_basic(v, exclude)
            for k, v in v.__dict__.items()
            if (not k.startswith('_')) and (k not in exclude)}


def drop_all():
    db.drop_all()


def create_all():
    db.create_all()

    sport = Sport(name='American Football')
    comp = Competition(key='NFL', sport=sport)

    source = Source(key='main')
    ssport = SourceSport(sport=sport, source=source,
                         source_sport_id='s1')
    scomp = SourceCompetition(
        competition=comp, source=source, source_competition_id='c1')

    db.session.add(ssport)
    db.session.add(scomp)

    sport = Sport(name='Baseball')
    comp = Competition(key='MLB', sport=sport)

    ssport = SourceSport(sport=sport, source=source,
                         source_sport_id='s2')
    scomp = SourceCompetition(
        competition=comp, source=source, source_competition_id='c2')

    db.session.add(ssport)
    db.session.add(scomp)
    db.session.commit()


def db_reset():
    drop_all()
    create_all()

如果我对此执行了联接查询,没有进行过滤,

logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

for ssport in cast(
        List[SourceSport],
        SourceSport.query
        .options(
            joinedload(SourceSport.source_competitions),
        )
        ):

    # noinspection PyTypeChecker
    logging.warning(to_basic(ssport, ('source_sport',)))

一切都很好

INFO:sqlalchemy.engine.base.Engine:SELECT source_sport.sport_id AS source_sport_sport_id, source_sport.source_sport_id AS source_sport_source_sport_id, source_sport.source_id AS source_sport_source_id, source_competition_1.competition_id AS source_competition_1_competition_id, source_competition_1.source_competition_id AS source_competition_1_source_competition_id, source_competition_1.source_id AS source_competition_1_source_id 
FROM source_sport JOIN competition AS competition_1 ON source_sport.sport_id = competition_1.sport_id JOIN source_competition AS source_competition_1 ON source_sport.source_id = source_competition_1.source_id AND competition_1.id = source_competition_1.competition_id

WARNING:root:{'source_id': 1, 'sport_id': 1, 'source_sport_id': 's1', 'source_competitions': [{'source_competition_id': 'c1', 'source_id': 1, 'competition_id': 1}]}
WARNING:root:{'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2', 'source_competitions': [{'source_competition_id': 'c2', 'source_id': 1, 'competition_id': 2}]}

但是,如果我在Competition上使用过滤器来运行它,

for ssport in cast(
        List[SourceSport],
        SourceSport.query
        .options(
            joinedload(SourceSport.source_competitions),
        )
        .filter(Competition.key == 'MLB')
        ):

    # noinspection PyTypeChecker
    logging.warning(to_basic(ssport, ('source_sport',)))

未正确应用过滤器,我们得到了所有结果。

输出

INFO:sqlalchemy.engine.base.Engine:SELECT source_sport.sport_id AS source_sport_sport_id, source_sport.source_sport_id AS source_sport_source_sport_id, source_sport.source_id AS source_sport_source_id, source_competition_1.competition_id AS source_competition_1_competition_id, source_competition_1.source_competition_id AS source_competition_1_source_competition_id, source_competition_1.source_id AS source_competition_1_source_id 
FROM competition, source_sport JOIN competition AS competition_1 ON source_sport.sport_id = competition_1.sport_id JOIN source_competition AS source_competition_1 ON source_sport.source_id = source_competition_1.source_id AND competition_1.id = source_competition_1.competition_id 
WHERE competition."key" = ?
INFO:sqlalchemy.engine.base.Engine:('MLB',)

WARNING:root:{'source_id': 1, 'sport_id': 1, 'source_sport_id': 's1', 'source_competitions': [{'source_competition_id': 'c1', 'source_id': 1, 'competition_id': 1}]}
WARNING:root:{'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2', 'source_competitions': [{'source_competition_id': 'c2', 'source_id': 1, 'competition_id': 2}]}

从该日志中,您可以看到sqlalchemy正在经历SourceSport的关系,并使用辅助表Competition,但将其别名为competition_1,但是那么由于有了过滤器,它没有引用此已联接的表,而是在competition列表中将FROM包括为competition

如果我预先加载该联接,

for ssport in cast(
        List[SourceSport],
        SourceSport.query
        .join(Competition, SourceSport.competitions)
        .options(
            joinedload(SourceSport.source_competitions),
        )
        .filter(Competition.key == 'MLB')
        ):

    # noinspection PyTypeChecker
    logging.warning(to_basic(ssport, ('source_sport',)))

INFO:sqlalchemy.engine.base.Engine:SELECT source_sport.sport_id AS source_sport_sport_id, source_sport.source_sport_id AS source_sport_source_sport_id, source_sport.source_id AS source_sport_source_id, source_competition_1.competition_id AS source_competition_1_competition_id, source_competition_1.source_competition_id AS source_competition_1_source_competition_id, source_competition_1.source_id AS source_competition_1_source_id 
FROM source_sport JOIN competition ON competition.sport_id = source_sport.sport_id JOIN competition AS competition_1 ON source_sport.sport_id = competition_1.sport_id JOIN source_competition AS source_competition_1 ON source_sport.source_id = source_competition_1.source_id AND competition_1.id = source_competition_1.competition_id 
WHERE competition."key" = ?
INFO:sqlalchemy.engine.base.Engine:('MLB',)
WARNING:root:{'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2', 'source_competitions': [{'source_competition_id': 'c2', 'source_id': 1, 'competition_id': 2}]}

现在它可以正确过滤,但是查询很奇怪/不正确,两次连接了competition表。


在查询中“手动”完成所有操作,

for ssport in cast(
        List[SourceSport],
        SourceSport.query
        .join(Source)
        .join(Sport)
        .join(Competition)
        .join(SourceCompetition,
              ((SourceCompetition.source_id == Source.id) &
               (SourceCompetition.competition_id == Competition.id)))
        .filter(Competition.key == 'MLB')
        ):

    # noinspection PyTypeChecker
    logging.warning(to_basic(ssport, ('source_sport',)))

完全按照我的预期工作,

INFO:sqlalchemy.engine.base.Engine:SELECT source_sport.sport_id AS source_sport_sport_id, source_sport.source_sport_id AS source_sport_source_sport_id, source_sport.source_id AS source_sport_source_id 
FROM source_sport JOIN source ON source.id = source_sport.source_id JOIN sport ON sport.id = source_sport.sport_id JOIN competition ON sport.id = competition.sport_id JOIN source_competition ON source_competition.source_id = source.id AND source_competition.competition_id = competition.id 
WHERE competition."key" = ?
INFO:sqlalchemy.engine.base.Engine:('MLB',)
WARNING:root:{'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2'}

我想做的是让关系工作与上一个^^^完全一样,所以我可以做任何一个

SourceSport.query
    .options(joinedload(SourceSport.source_competitions))
    .filter(Competition.key == 'MLB')

SourceSport.query
    .join(SourceCompetition, SourceSport.source_competitions)
    .filter(Competition.key == 'MLB')

根据source_competitions定义的关系,它可以正确连接,并基于此正确过滤-但我不知道如何。

0 个答案:

没有答案