如何配置三个表之间的关系?

How to configure relationships between three tables?

我目前有以下三个table:

class Match(Base):
    
    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    tournament_id = Column(Integer, ForeignKey("myschema.tournament.id_"))


class Tournament(Base):

    id_ = Column(Integer, primary_key=True)
    latitude = Column(Float)
    longitude = Column(Float)

    match = relationship("Match", backref="tournament")


class Weather(Base):

    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    latitude = Column(Float)
    longitude = Column(Float)
    conditions = Column(String(50))

我正在寻找一个查询来获取 Match table 中每场比赛的天气状况。像这样:

qry = session.query(Match.id_, Weather.conditions)
qry = qry.select_from(Match)
qry = qry.join(Tournament, Weather)
qry = qry.all()

Weather 中的唯一键是 date_timelatitudelongitude 的组合,其中:

我可以接受外键上的简单关系,例如MatchTournament 的那个,但我在试图找出更复杂的东西时绝望地迷路了。

我希望以上内容是不言自明的 - 如果需要数据,请告诉我,我会添加一些。

我正在使用 SQLAlchemy v1.3。


更新:

我一直在尝试根据指南创建关系 here:

class Match(Base):

    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    tournament_id = Column(Integer, ForeignKey("myschema.tournament.id_"))

    weather = relationship(
        "Weather", primaryjoin="Match.date_time == Weather.date_time"
    )

class Tournament(Base):

    id_ = Column(Integer, primary_key=True)
    latitude = Column(Float)
    longitude = Column(Float)

    match = relationship("Match", backref="tournament")
    weather = relationship(
        "Weather",
        primaryjoin="and_(Tournament.latitude == Weather.latitude, " +
        "Tournament.longitude == Weather.longitude)"
    )


class Weather(Base):

    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    latitude = Column(Float)
    longitude = Column(Float)
    conditions = Column(String(50))

但是,当我 运行 来自上面的查询时,我得到错误:

Don't know how to join to <class 'container.Weather'>. Please use the .select_from() method to establish an explicit left side, as well as providing an explicit ON clause if not present already to help resolve the ambiguity.

我哪里错了?

选项-1:普通查询

实际上,它与编写普通 SQL 查询非常相似:

qry = (
    session.query(Match.id_, Weather.conditions)
    .select_from(Match)
    .join(Tournament)
    .join(
        Weather,
        and_(
            Match.date_time == Weather.date_time,
            Tournament.latitude == Weather.latitude,
            Tournament.longitude == Weather.longitude,
        ),
    )
).all()

选项-2:计算属性 在澄清您想建立“关系”之后,我实际上认为使用 column_property 而不是从 Match 到 Weather 的 relationship 看起来更有效:

class Match(Base):
    __tablename__ = "match"
    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    tournament_id = Column(Integer, ForeignKey("tournament.id_"))

    # NOTE: Weather and Tournament should be defined earlier to use the expressions below. Otherwise, a stringified definition could be used instead
    weather_conditions = column_property(
        select([Weather.conditions.label("match_weather_conditions")])
        .where(tournament_id == Tournament.id_)
        .where(date_time == Weather.date_time)
        .where(Weather.latitude == Tournament.latitude)
        .where(Weather.longitude == Tournament.longitude)
        .as_scalar()
        .label("weather_conditions")
    )

每当您将 Match 作为 属性 查询时,这将查询 weather_conditions


最终:选项 3:实际 relationship

如下所示定义关系(无需更改模型定义的任何其他部分):

class Match(Base):
    __tablename__ = "match"
    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    tournament_id = Column(Integer, ForeignKey("tournament.id_"))

    weather = relationship(
        Weather,
        secondary=Tournament.__table__,
        primaryjoin=tournament_id == Tournament.id_,
        secondaryjoin=and_(
            Weather.latitude == Tournament.latitude,
            Weather.longitude == Tournament.longitude,
            date_time == Weather.date_time,
        ),
        viewonly=True,
        uselist=False,
    )

您在问题中提出的 Configuring how Relationship Joins link 包含类似解决方案的示例。

现在,要执行查询,您还需要指定 join 条件才能使用您想要的查询:

q = session.query(Match.id_, Weather.conditions).join(Weather, Match.weather)

可以理解,您想利用 ORM 的关系映射,而不是在 class 之外编写显式 SQL(或映射查询)。为此,您需要明确主连接和辅助连接。

这是一个使用显式主连接和辅助连接的解决方案:
注意:前面的回答既充分又正确,我的解决方案是编写代码的不同风格,但ORM的底层行为将是几乎相等。

from sqlalchemy.ext.associationproxy import association_proxy


class Match(Base):

    id_ = Column(Integer, primary_key=True)
    date_time = Column(DateTime, index=True)
    tournament_id = Column(Integer, ForeignKey("myschema.tournament.id_"))

    # Relationship for weather
    weather = relationship("Weather",
        secondary='Tournament',
        primaryjoin='Tournament.id == Match.tournament_id',
        secondaryjoin="""
            and_(
                Tournament.latitude == Weather.latitude,
                Tournament.longitude == Weather.longitude,
                Match.date_time == Weather.date_time
            )
        """,
        uselist=False,
        doc="""Join `Match` and `Weather` on `date_time` while using `Tournament`
        as an association table to match `[latitude, longitude]`.

        This will allow querying of the weather of a given match but will
        not allow updating of weather through a `Match` object.
        """
    )

    weather_conditions = association_proxy('conditions', 'weather',
        doc='Access the weather conditions without unpacking weather relationship')


class Tournament(Base):

    id_ = Column(Integer, primary_key=True)
    latitude = Column(Float)
    longitude = Column(Float)

    match = relationship("Match", backref="tournament",
        doc='Join on `Tournament.id == Match.tournament_id`')

    weather = relationship("Weather",
        secondary='Match',
        primaryjoin='Tournament.id == Match.tournament_id',
        secondaryjoin="""
            and_(
                Tournament.latitude == Weather.latitude,
                Tournament.longitude == Weather.longitude,
                Match.date_time == Weather.date_time
            )
        """,
        uselist=False,
        doc="""Join `Tournament` and `Weather` on `[latitude, longitude]` while
        using `Match` as an association table to match `date_time`.

        This will allow querying of the weather of a given tournament but will
        not allow updating of weather through a `Tournament` object.
        """
    )

    weather_conditions = association_proxy('conditions', 'weather',
        doc='Access the weather conditions without unpacking weather relationship')