sqlalchemy ORM insert many to many objects in one transaction / change order of operations

Question:

I have this code snippet for M2M relation among players and matches through table elo.

SQLAlchemy==2.0.1

import os
from datetime import datetime
from uuid import UUID

from pytz import utc
from sqlalchemy import Column, create_engine, DateTime, ForeignKey, Integer
from sqlalchemy.orm import registry, relationship, Mapped, Session
from sqlalchemy import UUID as saUUID


mapper_registry = registry()


@mapper_registry.mapped
class Elo:
    __tablename__ = "elos"

    player: UUID = Column(saUUID, ForeignKey("players.id"), primary_key=True)
    match: UUID = Column(saUUID, ForeignKey("matches.id"), primary_key=True)
    elo: int = Column(Integer)


@mapper_registry.mapped
class Player:
    __tablename__ = "players"

    id: Mapped[UUID] = Column(saUUID, primary_key=True)

    matches: Mapped[list["Match"]] = relationship(secondary="elos", back_populates="players")


@mapper_registry.mapped
class Match:
    __tablename__ = "matches"

    id: Mapped[UUID] = Column(saUUID, primary_key=True)
    date: Mapped[datetime] = Column(DateTime, default=datetime.now(tz=utc))

    players: Mapped[list[Player]] = relationship(secondary="elos", back_populates="matches")

I want to INSERT 3 objects at a time: Player, Match and Elo, but sqlalchemy tries to insert in Elo first so it fails, because there is no Player or Match inside my DB. I’ve tried to use just .add instead of .add_all and it gave same result. Probably it wants to do operations in alphabetical order?

sqlalchemy.exc.IntegrityError: (psycopg2.errors.ForeignKeyViolation) insert or update on table "elos" violates foreign key constraint "elos_player_fkey"

So how do I change order of operations in SQLAclhemy or what should I do in my case?
Thanks in advance

if __name__ == '__main__':
    con_string = "postgresql://user:[email protected]:5432/db"
    engine = create_engine(con_string, echo=True)
    with Session(engine) as session:
        player = Player(id=UUID(bytes=os.urandom(16)))
        match = Match(id=UUID(bytes=os.urandom(16)))
        elo = Elo(player=player.id, match=match.id, elo=1999)
        session.add_all([player, match, elo])
        session.commit()
Asked By: Ayudesee

||

Answers:

If you want to have an extra column on the many-to-many table, you can use the association object pattern. This entails adding relationships to the Elo model, and altering the relationship on the Player and Match models:

@mapper_registry.mapped
class Elo:
    __tablename__ = 'elos'

    player_id: UUID = Column(saUUID, ForeignKey('players.id'), primary_key=True)
    match_id: UUID = Column(saUUID, ForeignKey('matches.id'), primary_key=True)
    elo: int = Column(Integer)
    player: Mapped['Player'] = relationship(back_populates='matches')
    match: Mapped['Match'] = relationship(back_populates='players')


@mapper_registry.mapped
class Player:
    __tablename__ = 'players'

    id: Mapped[UUID] = Column(saUUID, primary_key=True)

    matches: Mapped[list[Elo]] = relationship(
        back_populates='player'
    )


@mapper_registry.mapped
class Match:
    __tablename__ = 'matches'

    id: Mapped[UUID] = Column(saUUID, primary_key=True)
    date: Mapped[datetime] = Column(DateTime, default=datetime.now(tz=utc))

    players: Mapped[list[Elo]] = relationship(
        back_populates='match'
    )

Creating models looks like this:

with Session(engine) as s, s.begin():
    player = Player(
        id=UUID(bytes=os.urandom(16)),
    )
    elo = Elo(elo=42)
    elo.match = Match(id=UUID(bytes=os.urandom(16)))
    player.matches.append(elo)
    s.add(player)

Producing this log output:

2023-02-04 17:15:56,463 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-02-04 17:15:56,463 INFO sqlalchemy.engine.Engine INSERT INTO matches (id, date) VALUES (%(id)s::UUID, %(date)s)
2023-02-04 17:15:56,463 INFO sqlalchemy.engine.Engine [generated in 0.00008s] {'id': UUID('a811bf0a-6ff9-77eb-6ffa-c3ce2351acd6'), 'date': datetime.datetime(2023, 2, 4, 17, 15, 56, 419086, tzinfo=<UTC>)}
2023-02-04 17:15:56,464 INFO sqlalchemy.engine.Engine INSERT INTO players (id) VALUES (%(id)s::UUID)
2023-02-04 17:15:56,464 INFO sqlalchemy.engine.Engine [generated in 0.00006s] {'id': UUID('3c0cbbe4-1c58-e6df-aff4-8f1fc0c73fcc')}
2023-02-04 17:15:56,465 INFO sqlalchemy.engine.Engine INSERT INTO elos (player_id, match_id, elo) VALUES (%(player_id)s::UUID, %(match_id)s::UUID, %(elo)s)
2023-02-04 17:15:56,465 INFO sqlalchemy.engine.Engine [generated in 0.00007s] {'player_id': UUID('3c0cbbe4-1c58-e6df-aff4-8f1fc0c73fcc'), 'match_id': UUID('a811bf0a-6ff9-77eb-6ffa-c3ce2351acd6'), 'elo': 42}
2023-02-04 17:15:56,465 INFO sqlalchemy.engine.Engine COMMIT
Answered By: snakecharmerb
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.