在删除所有“业务”逻辑后,我的表设置如下所示,
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
定义的关系,它可以正确连接,并基于此正确过滤-但我不知道如何。