Holy crap, thank you so much!
I really can't thank you enough for this. As I said, I've never used the
joined inheritance before, and honestly? Can't believe it's as simple/good
as that.
Only weirdness I'm seeing now is in the init, if I do a db.session.commit()
just after `db.session.add(ssport)`, an odd null Sport gets added. Full
output -
INFO:sqlalchemy.engine.base.Engine:INSERT INTO source ("key") VALUES (?)
INFO:sqlalchemy.engine.base.Engine:('main',)
INFO:sqlalchemy.engine.base.Engine:INSERT INTO sport (name) VALUES (?)
INFO:sqlalchemy.engine.base.Engine:('American Football',)
INFO:sqlalchemy.engine.base.Engine:INSERT INTO sport (name) VALUES (?)
INFO:sqlalchemy.engine.base.Engine:(None,)
INFO:sqlalchemy.engine.base.Engine:INSERT INTO source_sport (sport_id,
source_sport_id, source_id) VALUES (?, ?, ?)
INFO:sqlalchemy.engine.base.Engine:(2, 'sAF', 2)
>From the code,
db.session.add(Source(key='secondary'))
source = Source(key='main')
sport = Sport(name='American Football')
ssport = SourceSport(sport=sport, source=source,
source_sport_id='sAF')
comp = Competition(key='NFL', sport=sport)
scomp = SourceCompetition(
competition=comp, source=source, source_competition_id='sNFL')
db.session.add(ssport)
db.session.commit()
Not sure what's going on here at all?
On Monday, July 23, 2018 at 7:35:40 PM UTC+1, Mike Bayer wrote:
>
> Here's a simple joined inheritance model and you can see the
> relationships are quite simple:
>
>
> from sqlalchemy import *
> from sqlalchemy.orm import *
> from sqlalchemy.ext.declarative import declarative_base
> from sqlalchemy.ext.declarative import declared_attr
>
> from sqlalchemy import Column, Integer, String, ForeignKey
>
>
> Model = declarative_base()
>
>
> class Source(Model):
> __tablename__ = 'source'
>
> id = Column(Integer, primary_key=True)
>
> key = Column(String(10), unique=True)
>
>
> class Sport(Model):
> __tablename__ = 'sport'
>
> id = Column(Integer, primary_key=True)
>
> name = Column(String(50), unique=True)
>
>
> class Competition(Model):
> __tablename__ = 'competition'
>
> 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,))
>
>
> # mixin
> class _Sourcer:
>
> @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, Sport):
> __tablename__ = 'source_sport'
>
> sport_id = Column(Integer, ForeignKey(Sport.id),
> primary_key=True)
> competitions = relationship(
> Competition,
> primaryjoin=sport_id == foreign(Competition.sport_id),
> uselist=True)
>
> source_sport_id = Column(String(20))
>
> source_competitions = relationship(
> "SourceCompetition",
> back_populates="source_sport", innerjoin=True)
>
>
> class SourceCompetition(_Sourcer, Competition):
> __tablename__ = 'source_competition'
>
> competition_id = Column(Integer, ForeignKey(Competition.id),
> primary_key=True)
> source_competition_id = Column(String(20))
>
> sport = relationship(Sport, enable_typechecks=False)
>
> source_sport = relationship(
> "SourceSport", back_populates="source_competitions")
>
>
> session = Session()
>
> q1 = session.query(SourceSport).\
> join(SourceCompetition).filter(Competition.key == 'NBA')
>
> q2 = session.query(SourceSport).\
> join(SourceSport.source_competitions).filter(Competition.key == "NBA")
>
>
> q3 = session.query(SourceSport).join(SourceCompetition).options(
> contains_eager(SourceSport.source_competitions)
> ).filter(Competition.key == "NFL")
>
>
> print("------------------------------")
> print(q1)
> print("------------------------------")
> print(q2)
> print("------------------------------")
> print(q3)
>
> the first two queries come out the same. The third query, wasn't sure
> what you were trying to do with the joinedload() + filter so I turned
> those into contains_eager which seems to be what you mean to do, that
> also gives you the same query except you get more columns in the
> columns clause.
>
> however it means you deal with SourceCompetition and Competition as a
> single entity even though they correspond to two tables.
>
>
> On Mon, Jul 23, 2018 at 2:13 PM, seaders <[email protected] <javascript:>>
> wrote:
> > Hi Mike, thanks for replying. I've attached the full runner py file.
> >
> >> the relationship here names "Competiton" as a "secondary" table which
> > means it will be aliased (here as competition_1) and you cannot filter
> > on it in terms of the Competition entity
> >
> > Getcha, I wasn't aware of that.
> >
> >> If you want to make a
> > relationship from SourceSport to SourceCompetition that also uses
> > "Competition" and you also want to filter on Competition ...
> >
> > Preferably, if possible, I'd like to have all the options available to
> me
> > like with the query. So 1 time, it's filtering on `Competition.key`,
> > another, it's `SourceCompetition.source_competition_id`, basically like
> > you'd do with a manual SQL query. The JOIN between the tables
> > (relationship) should be unchanging, but if I want to do a filter on it,
> the
> > data from the JOIN'd tables would be available to me.
> >
> >> It looks mostly like SourceCompetition as a joined table
> > subclass of Competition would be the most natural
> >
> > I've not yet created a single subclass, so have no experience with them
> (If
> > I had a full handle on them, I might not have ended up here!).
> >
> > ----
> >
> > P.S. I've just realised I've mistakenly posted this in
> sqlalchemy-alembic,
> > instead of sqlalchemy. Should I delete, and repost there, or are we ok
> > here?
> >
> >
> > On Monday, July 23, 2018 at 5:30:53 PM UTC+1, seaders wrote:
> >>
> >> I've these 5 tables,
> >>
> >> Source
> >>
> >> Sport 1----* Competition
> >>
> >> ________________________________
> >>
> >> Sport 1----1 SourceSport
> >>
> >> Competition 1----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, aliased
> >> 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)
> >>
> >>
> >> 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)
> >>
> >>
> >> class SourceSport(_Sourcer):
> >> sport_id = Column(Integer, ForeignKey(Sport.id),
> >> primary_key=True)
> >> sport = relationship(Sport)
> >> source_sport_id = Column(String(20))
> >>
> >>
> >> class SourceCompetition(_Sourcer):
> >> competition_id = Column(Integer, ForeignKey(Competition.id),
> >> primary_key=True)
> >> competition = relationship(Competition)
> >> source_competition_id = Column(String(20))
> >>
> >>
> >> def create_all():
> >> db.create_all()
> >>
> >> source = Source(key='main')
> >>
> >> sport = Sport(name='American Football')
> >> ssport = SourceSport(sport=sport, source=source,
> >> source_sport_id='sAF')
> >>
> >> comp = Competition(key='NFL', sport=sport)
> >> scomp = SourceCompetition(
> >> competition=comp, source=source, source_competition_id='sNFL')
> >>
> >> db.session.add(ssport)
> >> db.session.add(scomp)
> >>
> >> comp = Competition(key='NCAAF', sport=sport)
> >> scomp = SourceCompetition(
> >> competition=comp, source=source,
> source_competition_id='sNCAAF')
> >>
> >> db.session.add(scomp)
> >>
> >> sport = Sport(name='Basketball')
> >> ssport = SourceSport(sport=sport, source=source,
> >> source_sport_id='s2')
> >>
> >> comp = Competition(key='NBA', sport=sport)
> >> scomp = SourceCompetition(
> >> competition=comp, source=source, source_competition_id='sNBA')
> >>
> >> db.session.add(ssport)
> >> db.session.add(scomp)
> >>
> >> comp = Competition(key='NCAAB', sport=sport)
> >> scomp = SourceCompetition(
> >> competition=comp, source=source,
> source_competition_id='sNCAAB')
> >>
> >> db.session.add(scomp)
> >>
> >>
> >> And one of the queries I find myself doing often, is getting all the
> >> SourceCompetion's for SourceSport's.
> >>
> >> The "manual" query of this is fairly straight forward,
> >>
> >> SourceSport.query
> >>
> >> .join(Sport)
> >> .join(Competition)
> >> .join(SourceCompetition,
> >> ((SourceCompetition.source_id == SourceSport.source_id) &
> >> (SourceCompetition.competition_id == Competition.id))
> >> )
> >>
> >>
> >> Every relationship is set up correctly to join easily enough, until
> >> SourceCompetition, and then you've to specify the onclause, but again,
> >> because everything's set up ok, it's easy and straightforward enough.
> >>
> >> I can also easily add filters/options to that,
> >>
> >> ...
> >> .contains_eager(SourceSport.sport)
> >> .filter(Competition.key == 'NFL')
> >>
> >> ...
> >> .filter(Sport.name == 'Basketball')
> >>
> >> Example output of the first one,
> >>
> >> INFO:sqlalchemy.engine.base.Engine:SELECT sport.id AS sport_id,
> sport.name
> >> AS sport_name, 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 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_sport.source_id AND
> >> source_competition.competition_id = competition.id
> >> WHERE competition."key" = ?
> >> INFO:sqlalchemy.engine.base.Engine:('NBA',)
> >>
> >> {'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2', 'sport':
> {'name':
> >> 'Basketball', 'id': 2}}
> >>
> >> Perfect, now all I want to do, is have the exact same "base" join in a
> >> relationship, in SourceSport. I have a version working,
> >>
> >> class SourceSport(_Sourcer):
> >>
> >> ...
> >>
> >> source_competitions: List['SourceCompetition'] = relationship(
> >> lambda: SourceCompetition,
> >> primaryjoin=lambda: and_(
> >> SourceSport.sport_id == foreign(Competition.sport_id),
> >> ),
> >> secondary=Competition.__table__,
> >> secondaryjoin=lambda: and_(
> >> SourceSport.source_id == foreign(SourceCompetition.source_id),
> >> Competition.id == SourceCompetition.competition_id,
> >> ),
> >> innerjoin=True,
> >> uselist=True)
> >>
> >>
> >> Now, this works correctly (although tbh, I think the complexity of it
> >> seems too high for what I'm trying to achieve), so I can do queries
> like,
> >>
> >> SourceSport.query
> >> .options(joinedload(SourceSport.source_competitions))
> >>
> >> ...
> >>
> >> SourceSport.query
> >> .join(SourceCompetition, SourceSport.source_competitions)
> >>
> >>
> >> But the problems come when I try to filter those queries with something
> >> simple like above,
> >>
> >> ...
> >>
> >> .filter(Competition.key == 'NFL')
> >>
> >>
> >> Now, when I look at the logs,
> >>
> >> 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:('NFL',)
> >>
> >> {'source_id': 1, 'sport_id': 1, 'source_sport_id': 'sAF',
> >> 'source_competitions': [{'source_competition_id': 'sNFL', 'source_id':
> 1,
> >> 'competition_id': 1}, {'source_competition_id': 'sNCAAF', 'source_id':
> 1,
> >> 'competition_id': 2}]}
> >>
> >> {'source_id': 1, 'sport_id': 2, 'source_sport_id': 's2', 'sport':
> {'name':
> >> 'Basketball', 'id': 2}, 'source_competitions':
> [{'source_competition_id':
> >> 'sNBA', 'source_id': 1, 'competition_id': 3}, {'source_competition_id':
> >> 'sNCAAB', 'source_id': 1, 'competition_id': 4}]}
> >>
> >> You can see that the JOIN for Competition is using "competition as
> >> competition_1", but the WHERE is just using "competition" from the FROM
> >> table list. This then gives a weird mish-mash of a returned object -
> not at
> >> all correct.
> >>
> >> I think I've tried just about everything, including a standalone join
> and
> >> mapper, but that seemed to be completely wasted work and right back to
> where
> >> I started here, I've read the docs, and stepped through a ton of the
> source,
> >> but still can't figure what to do. I feel like I'm just throwing
> spaghetti
> >> at the wall now, and trying to see will anything stick, and would
> really
> >> appreciate some help to figure this out and get it working.
> >>
> >> Many, many thanks.
> >
> > --
> > You received this message because you are subscribed to the Google
> Groups
> > "sqlalchemy-alembic" group.
> > To unsubscribe from this group and stop receiving emails from it, send
> an
> > email to [email protected] <javascript:>.
> > For more options, visit https://groups.google.com/d/optout.
>
--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
http://www.sqlalchemy.org/
To post example code, please provide an MCVE: Minimal, Complete, and Verifiable
Example. See http://stackoverflow.com/help/mcve for a full description.
---
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.