Объект 'Table' не имеет атрибута 'id' в отношении SQLAlchemy - PullRequest
1 голос
/ 07 марта 2019

У меня установлена ​​связь между тремя отдельными классами с использованием SQLAlchemy, с таблицей связей для отношения многие ко многим.Минимальный пример:

from sqlalchemy import *    
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import configure_mappers, relationship

Base = declarative_base()

teams_users = Table(
    'teams_users', Base.metadata,
    Column('team_id', ForeignKey('teams.id')),
    Column('user_id', ForeignKey('users.id'))
)

class User(Base):
    __tablename__ = 'users'
    # No autoincrement, since we're using externally-generated UIDs
    id = Column(Integer, primary_key=True, autoincrement=False)
    teams = relationship('Team', secondary=teams_users, back_populates="users")    

class Team(Base):
    __tablename__ = 'teams'
    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
    game_id = Column(Integer, ForeignKey('games.id'), nullable=False)
    games = relationship("Game", foreign_keys='games.id')
    users = relationship("User", secondary='teams_users', back_populates="teams")

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", back_populates="games", foreign_keys=team1_id, uselist=False)
    tean2 = relationship("Team", back_populates="games", foreign_keys=team2_id, uselist=False)

# done declaring, trigger the error
configure_mappers()

Попытка запросить любое из этих отношений возвращает ошибку 'Table' object has no attribute 'id':

Traceback (most recent call last):
  File "...", line 35, in <module>
    configure_mappers()
  File "/.../sqlalchemy/orm/mapper.py", line 3033, in configure_mappers
    mapper._post_configure_properties()
  File "/.../sqlalchemy/orm/mapper.py", line 1832, in _post_configure_properties
    prop.init()
  File "/.../sqlalchemy/orm/interfaces.py", line 183, in init
    self.do_init()
  File "/.../sqlalchemy/orm/relationships.py", line 1655, in do_init
    self._process_dependent_arguments()
  File "/.../sqlalchemy/orm/relationships.py", line 1680, in _process_dependent_arguments
    setattr(self, attr, attr_value())
  File "/.../sqlalchemy/ext/declarative/clsregistry.py", line 281, in __call__
    x = eval(self.arg, globals(), self._dict)
  File "<string>", line 1, in <module>
AttributeError: 'Table' object has no attribute 'id'

Моя цель при ее построении состоит в том, чтобы я мог легко проверить, какие командыкаждый пользователь когда-либо был частью.

Кроме того, Game имеет два внешних ключа в Team, потому что сценарий использования для этого проекта поддерживает команды произвольного размера, но только две команды.Это позволяет мне получить результат "team1 выиграно" и сразу же получить ссылку на победивших пользователей для отслеживания статистики и исторической справки.

Что я здесь не так делаю?

1 Ответ

3 голосов
/ 07 марта 2019

Чтобы определить отношения между игрой и двумя командами, которые в ней играют, вам нужно дать только внешние ключи таблицы 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...