Why am I getting AmbiguousForeignKeysError?

后端 未结 2 359
青春惊慌失措
青春惊慌失措 2021-01-27 04:54

I\'ve run into an issue after following the SqlAlchemy guide here.

Given the following simplified module:

class _Base():
    id_ = Column(Integer, primary_         


        
相关标签:
2条回答
  • 2021-01-27 05:45

    The two-way relationship and multiple join paths prevent SQLAlchemy from automatically determining the joins, and the relationships in both tables emit very similar error messages makes it difficult to understand where the problems lie (and whether a given change makes any progress in solving them). I found the simplest approach was to comment out the relationship in Player until ResultMatch was working properly.

    The changes to MatchResult are the same as those specified in the multiple join paths docs referenced in the question. To get the relationship in Player to work I specified the primary join condition so that SQLAlchemy could determine how to join to MatchResult.

    class Player(Base):
        __tablename__ = 'player'
    
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.String(100))
    
        matches = orm.relationship('MatchResult',
           primaryjoin="or_(Player.id == MatchResult.p1_id, Player.id == MatchResult.p2_id)")
    
    
    class MatchResult(Base):
        __tablename__ = 'match_result'
    
        id = sa.Column(sa.Integer, primary_key=True)
        p1_id = sa.Column(sa.Integer, sa.ForeignKey('player.id'))
        p2_id = sa.Column(sa.Integer, sa.ForeignKey('player.id'))
    
        p1 = orm.relationship("Player", foreign_keys=[p1_id])
        p2 = orm.relationship("Player", foreign_keys=[p2_id])
    

    Once these changes have been made, basic querying can be done without any explicit aliasing or joins.

    ms = session.query(MatchResult)
    for r in ms:
        print(r.p1_id, r.p1.name, r.p2_id, r.p2.name)
    
    p1 = session.query(Player).filter(Player.name == 'bar').one()
    for m in p1.matches:
        print(m.p1.name, m.p2.name)
    

    The above code, for clarity and usefulness to other readers, does not include the inheritance, mixin and session management code that is specific to the OP's application. Thiis version includes all of these.

    import sqlalchemy as sa
    from sqlalchemy.ext.declarative import declarative_base, declared_attr
    from sqlalchemy import orm
    
    
    class _Base():
        id_ = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    
    
    Base = declarative_base(cls=_Base)
    
    
    class BlgMixin():
    
        @declared_attr
        def __table_args__(cls):
            return {'schema': "belgarath_backup", "extend_existing": True}
    
    
    class DataAccessLayer():
    
        def __init__(self):
            conn_string = "mysql+mysqlconnector://root:root@localhost/"
            self.engine = sa.create_engine(conn_string)
    
        def create_session(self):
            Base.metadata.create_all(self.engine)
            Session = orm.sessionmaker()
            Session.configure(bind=self.engine)
            self.session = Session()
    
    
    class Player(Base, BlgMixin):
        __tablename__ = 'player'
    
        name = sa.Column(sa.String(100))
    
        match = orm.relationship('MatchResult',
                                 primaryjoin="or_(Player.id_ == MatchResult.p1_id, Player.id_ == MatchResult.p2_id)")
    
    
    class MatchResult(Base, BlgMixin):
        __tablename__ = 'match_result'
    
        p1_id = sa.Column(sa.Integer, sa.ForeignKey(f'{BlgMixin.__table_args__.get("schema")}.player.id_'))
        p2_id = sa.Column(sa.Integer, sa.ForeignKey(f'{BlgMixin.__table_args__.get("schema")}.player.id_'))
    
        p1 = orm.relationship("Player", foreign_keys=[p1_id])
        p2 = orm.relationship("Player", foreign_keys=[p2_id])
    
    
    dal = DataAccessLayer()
    Base.metadata.drop_all(bind=dal.engine)
    Base.metadata.create_all(bind=dal.engine)
    
    
    names = ['foo', 'bar', 'baz', 'zoo']
    dal.create_session()
    ps = [Player(name=n) for n in names]
    dal.session.add_all(ps)
    dal.session.flush()
    p1, p2, p3, p4 = ps
    m1 = MatchResult(p1_id=p1.id_, p2_id=p2.id_)
    m2 = MatchResult(p1_id=p2.id_, p2_id=p1.id_)
    m3 = MatchResult(p1_id=p3.id_, p2_id=p1.id_)
    m4 = MatchResult(p1_id=p1.id_, p2_id=p4.id_)
    dal.session.add_all([m1, m2, m3, m4])
    dal.session.commit()
    
    
    ms = dal.session.query(MatchResult)
    for r in ms:
        print(r.p1_id, r.p1.name, r.p2_id, r.p2.name)
    print()
    
    p1 = dal.session.query(Player).filter(Player.name == 'bar').one()
    for m in p1.match:
        print(m.p1.name, m.p2.name)
    dal.session.close()
    
    
    0 讨论(0)
  • 2021-01-27 05:50

    The issue is with the definition of this relationship match = relationship("MatchResult") for the Player class. If you completely remove this line, and use the below definitions for the relationships, all the queries you mentioned should work as expected:

    class Player(Base, BlgMixin):
        __tablename__ = "player"
    
        name_ = Column(String(100))
    
    
    class MatchResult(Base, BlgMixin):
        __tablename__ = "match_result"
    
        p1_id = Column(ForeignKey(Player.id_))
        p2_id = Column(ForeignKey(Player.id_))
    
        p1 = relationship(Player, foreign_keys=p1_id)
        p2 = relationship(Player, foreign_keys=p2_id)
    

    In fact, the desired select query can also be constructed, but you need to specify the relationships explicitly on JOINs:

    player_1 = aliased(Player)
    player_2 = aliased(Player)
    q = (
        dal.session
        .query(
            MatchResult.p1_id,
            player_1.name_,
            MatchResult.p2_id,
            player_2.name_,
        )
        .join(player_1, MatchResult.p1)  # explicitly specify which relationship/FK to join on
        .join(player_2, MatchResult.p2)  # explicitly specify which relationship/FK to join on
    )
    

    I would, however, make few more changes to the model to make it even more user-friednly:

    1. add backref to the relationship so that it can be navigated back from the Player
    2. add a property to show all the matches of one player for both sides

    Model definitions:

    class Player(Base, BlgMixin):
        __tablename__ = "player"
    
        name_ = Column(String(100))
    
        @property
        def all_matches(self):
            return self.matches_home + self.matches_away
    
    
    class MatchResult(Base, BlgMixin):
        __tablename__ = "match_result"
    
        p1_id = Column(ForeignKey(Player.id_))
        p2_id = Column(ForeignKey(Player.id_))
        
        p1 = relationship(Player, foreign_keys=p1_id, backref="matches_home")
        p2 = relationship(Player, foreign_keys=p2_id, backref="matches_away")
    

    This will allow navigating the relationships as per below example:

    p1 = session.query(Player).get(1)
    print(p1)
    for match in p1.all_matches:
        print(" ", match)
    
    0 讨论(0)
提交回复
热议问题