Чтобы определить отношения между игрой и двумя командами, которые в ней играют, вам нужно дать только внешние ключи таблицы games
; команда может играть в нескольких играх, отношения один-ко-многим; удалить столбец games_id
. Исключение, которое вы получили, немного напоминает красную сельдь, но ему не удается правильно настроить аргумент foreign_keys='games.id'
в отношении, которому не нужен этот внешний ключ.
Конфигурация отношений в классе Team
немного сложна, поскольку атрибут Team.games
должен относиться к любому внешнему ключу. Это описано в документации по Обработка нескольких путей соединения ; Вы были почти там, но здесь не нужен параметр uselist
:
class Game(Base):
__tablename__ = 'games'
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
team1_id = Column(Integer, ForeignKey('teams.id'))
team2_id = Column(Integer, ForeignKey('teams.id'))
team1 = relationship("Team", foreign_keys=team1_id)
team2 = relationship("Team", foreign_keys=team2_id)
Обратите внимание, что здесь опущены ссылки back_populates
, поскольку два отношения, обновляющие одно отношение на другом сайте, приводят к тому, что один или другой из двух внешних ключей обновляется с другим значением, что приводит к игре между одной командой по обе стороны!
Атрибут обратной связи, Team.games
, требует пользовательского primaryjoin
, поскольку вы ищете игры, в которых team1_id
или team2_id
- это внешний ключ, указывающий назад. Используйте аннотацию foreign()
, чтобы помочь SQLAlchemy определить, когда обновлять связь (она будет отслеживать изменения внешнего ключа), и использовать lambda
, чтобы отложить разрешение столбцов:
class Team(Base):
__tablename__ = 'teams'
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
# game_id = Column(Integer, ForeignKey('games.c.id'), nullable=False)
games = relationship(
"Game",
primaryjoin=lambda: or_(
Team.id == foreign(Game.team1_id),
Team.id == foreign(Game.team2_id)
),
viewonly=True,
)
users = relationship("User", secondary='teams_users', back_populates="teams")
Вы также можете сделать primaryjoin
строкой, содержащей выражение, которое теперь выполняется в lambda
, поэтому 'or_(Team.id == foreign(Game.team1_id), Team.id == foreign(Game.team2_id))'
.
Опять же, нет back_populates
, этот тип отношений не может автоматически обновлять отношения между загруженными объектами. Если вам нужно увидеть эти отношения перед фиксацией, вам нужно выполнить сброс сеанса. Я также добавил viewonly=True
, потому что вы не можете отобразить мутации в список Team.games
на обновления в базе данных (что означает, что добавление новой игры в список означает, что эта команда является командой 1 или командой 2?).
Возможно, вы захотите добавить пользовательскую таблицу ограничений, чтобы гарантировать, что игры никогда не будут проходить между одной и той же командой с обеих сторон:
class Game(Base):
# ...
__table_args__ = (
CheckConstraint(team1_id != team2_id, name='different_teams'),
)
Быстрая демонстрация отношений:
from itertools import combinations
engine = create_engine('sqlite:///:memory:', echo=False)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine)()
teams = [Team() for _ in range(3)]
session.add_all(teams)
user = User(id=42, teams=teams)
session.add(user)
games = [Game(team1=t1, team2=t2) for t1, t2 in combinations(teams, 2)]
session.add_all(games)
session.commit()
for team in user.teams:
print('Team:', team.id, 'games:', [g.id for g in team.games])
for game in session.query(Game):
print(f'Game {game.id}: team {game.team1.id} vs {game.team2.id}')
который выводит:
Team: 2 games: [1, 3]
Team: 1 games: [1, 2]
Team: 3 games: [2, 3]
Game 1: team 1 vs 2
Game 2: team 1 vs 3
Game 3: team 2 vs 3