SQLAlchemy relationships to same table - python

I have declared following models:
from sqlalchemy import (
Column,
Table,
Integer,
Date,
String,
ForeignKey,
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, backref
engine = create_engine('sqlite:///data.sqlite')
DBSession = sessionmaker(bind=engine)
Base = declarative_base()
lineup = Table('lineups', Base.metadata,
Column('match_id', Integer, ForeignKey('data.id')),
Column('player_id', Integer, ForeignKey('players.id')))
class Match(Base):
__tablename__ = 'data'
id = Column(Integer, primary_key=True)
date = Column(Date)
tournament = Column(String)
team1 = Column(String)
team2 = Column(String)
team1_lineup = relationship('Player', secondary=lineup)
team2_lineup = relationship('Player', secondary=lineup)
best_of = Column(Integer)
maps = relationship('Map')
score = Column(String)
class Map(Base):
__tablename__ = 'maps'
id = Column(Integer, primary_key=True)
match = Column(Integer, ForeignKey('data.id'))
name = Column(String)
score = Column(String)
class Player(Base):
__tablename__ = 'players'
id = Column(Integer, primary_key=True)
nickname = Column(String)
team = Column(String)
And I'm creating new Match object this way:
match = Match(...) # all kwargs except team1_lineup and team2_lineup
p1 = Player(id=1, nickname='p1', team='team')
p2 = Player(id=2, nickname='p2', team='team')
p3 = Player(id=3, nickname='p2', team='team')
match.team1_lineup.append(p1)
match.team2_lineup.append(p2)
match.team2_lineup.append(p3)
After commiting new object, I'm querying it.
>>> from hltv.models import Match, DBSession
>>> s = DBSession()
>>> m = s.query(Match).first()
>>> m.team1_lineup
[<hltv.models.Player object at 0x7f1a93009d10>, <hltv.models.Player object at 0x7f1a93009d90>, <hltv.models.Player object at 0x7f1a93009e10>]
>>> m.team2_lineup
[<hltv.models.Player object at 0x7f1a93009d10>, <hltv.models.Player object at 0x7f1a93009d90>, <hltv.models.Player object at 0x7f1a93009e10>]
The problem is m.team1_lineup and m.team2_lineup are the same. How can I solve this?
Additionally, how do I assign ID to every lineup (lineups with same players should have same ID)?

I've managed to solve my problem. I had to add IDs to lineup (which I've renamed to Team) and specify how to join tables by providing two different IDs for those teams. Another way way solution posted by André.
Here is the code:
from sqlalchemy import (
Column,
Table,
Integer,
Date,
String,
ForeignKey,
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref
engine = create_engine('sqlite:///data.sqlite')
DBSession = sessionmaker(bind=engine)
Base = declarative_base()
class Match(Base):
__tablename__ = 'data'
id = Column(Integer, primary_key=True)
date = Column(Date)
tournament = Column(String)
best_of = Column(Integer)
score = Column(String)
maps = relationship('Map')
team1_id = Column(ForeignKey('team.id'))
team2_id = Column(ForeignKey('team.id'))
team1 = relationship('Team', primaryjoin='Match.team1_id == Team.id')
team2 = relationship('Team', primaryjoin='Match.team2_id == Team.id')
class Map(Base):
__tablename__ = 'map'
id = Column(Integer, primary_key=True)
match = Column(Integer, ForeignKey('data.id'))
name = Column(String)
score = Column(String)
lineup = Table('lineup',
Base.metadata,
Column('player_id', Integer, ForeignKey('player.id')),
Column('team_id', Integer, ForeignKey('team.id')))
class Player(Base):
__tablename__ = 'player'
id = Column(Integer, primary_key=True)
nickname = Column(String)
class Team(Base):
__tablename__ = 'team'
id = Column(Integer, primary_key=True)
name = Column(String)
players = relationship('Player', secondary=lineup, backref='teams')

I have some suggestions in relationships:
Considering that you have for each match 2(two) teams and 2(two) differents line up, when you use that same match id for two different lineups happens this query mistake to return the lineups;
I am not good in sports, but I think that you can do a better normalization on models. For example: Create a new model Team
My suggestion is change some models for Many to many, like that:
from sqlalchemy import (
Column,
Table,
Integer,
Date,
String,
ForeignKey,
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, backref
engine = create_engine('sqlite:///data.sqlite')
DBSession = sessionmaker(bind=engine)
Base = declarative_base()
lineup = Table('lineups', Base.metadata,
Column('team_id', Integer, ForeignKey('teams.id')),
Column('player_id', Integer, ForeignKey('players.id')))
class Team(Base):
__tablename__ = 'teams'
id = Column(Integer, primary_key=True,autoincrement= True)
name = Column(String)
team_lineup = relationship('Player',secondary=lineup)
match_team = Table('match_teams', Base.metadata,
Column('match_id', Integer, ForeignKey('matches.id')),
Column('team_id', Integer, ForeignKey('teams.id')))
class Match(Base):
__tablename__ = 'matches'
id = Column(Integer, primary_key=True,autoincrement= True)
date = Column(Date)
tournament = Column(String)
team = relationship('Team',secondary=match_team)
best_of = Column(Integer)
maps = relationship('Map')
score = Column(String)
class Map(Base):
__tablename__ = 'maps'
id = Column(Integer, primary_key=True,autoincrement= True)
match = Column(Integer, ForeignKey('matches.id'))
name = Column(String)
score = Column(String)
class Player(Base):
__tablename__ = 'players'
id = Column(Integer, primary_key=True,autoincrement= True)
nickname = Column(String)
Creating objets...
s = DBSession()
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
t1 = Team(name="t1")
t2 = Team(name="t2")
p1 = Player()
p2 = Player()
p3 = Player()
s.add(p1)
s.add(p2)
s.add(p3)
t1.team_lineup.append(p1)
t2.team_lineup.append(p2)
t2.team_lineup.append(p3)
s.add(t1)
s.add(t2)
m = Match()
m.tournament="xpto"
m.team.append(t1)
m.team.append(t2)
s.add(m)
s.commit()
And after that, you can see that the lineups is like your expectation:
>>> m = s.query(Match).first()
>>> for t in m.team:
... print t.team_lineup
...
[<__main__.Player object at 0x10b57c890>]
[<__main__.Player object at 0x10b57c910>, <__main__.Player object at 0x10b57c990>]
In this case you can have "n" teams for each match. I don't know if is possible in the sport that you are managing.

Related

sqlalchemy.exc.InvalidRequestError: When initializing mapper mapped class Event->event, expression 'Tag' failed to locate a name ('Tag')

I am trying to use ORM with SQLAlchemy in Python. My current solution fails and throws an exception right in the moment the ORM is first used. I receive the following exception:
sqlalchemy.exc.InvalidRequestError: When initializing mapper mapped class Event->event, expression 'Tag' failed to locate a name ('Tag'). If this is a class name, consider adding this relationship() to the <class 'backend.source.database.event.Event'> class after both dependent classes have been defined.
My classes are defined like in the offical SQLAlchemy-Documentation (https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#many-to-many), which is why I am kinda confused about that error.
association = Table('event_to_tag', declarative_base().metadata,
Column('event_id', Integer, ForeignKey('event.id'), primary_key=True),
Column('tag_id', Integer, ForeignKey('tag.id'), primary_key=True))
class Event(declarative_base()):
__tablename__ = "event"
id = Column(Integer, primary_key=True)
title = Column(String(255))
location = Column(String(255))
organizer_id = Column(Integer, ForeignKey(Organizer.id))
start = Column(DateTime)
end = Column(DateTime)
lang = Column(String(255))
costs = Column(DECIMAL)
registration = Column(TINYINT)
url = Column(String(255))
description = Column(Text)
tags = relationship("Tag", secondary=association, back_populates="events")
class Tag(declarative_base()):
__tablename__ = "tag"
id = Column(Integer, primary_key=True)
name = Column(String(255))
events = relationship("Event", secondary=association, back_populates="tags")
Thank you, greetings
I think you need to define a Base = declarative_base(), need to use in your models and associations.
from sqlalchemy import Column, Integer, ForeignKey, String, DECIMAL, Text, DateTime, Table, create_engine
from sqlalchemy.dialects.mssql import TINYINT
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import declarative_base, relationship, scoped_session, sessionmaker
Base = declarative_base()
association = Table('event_to_tag',
Base.metadata,
Column('event_id', Integer, ForeignKey('events.id'), primary_key=True),
Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True))
class Event(Base):
__tablename__ = "events"
id = Column(Integer, primary_key=True)
title = Column(String(255))
location = Column(String(255))
# organizer_id = Column(Integer, ForeignKey(Organizer.id))
start = Column(DateTime)
end = Column(DateTime)
lang = Column(String(255))
costs = Column(DECIMAL)
registration = Column(UUID)
url = Column(String(255))
description = Column(Text)
tags = relationship("Tag", secondary=association, back_populates="events")
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True)
name = Column(String(255))
events = relationship("Event", secondary=association, back_populates="tags")
class CreateEngine:
def __init__(self):
self.connection_string = "postgresql+psycopg2://<user_name>:<password>#127.0.0.1/<table_name>"
self.engine = create_engine(self.connection_string)
def create_table(self):
return Base.metadata.create_all(self.engine)
def create_session(self):
session_factory = sessionmaker(bind=self.engine)
Session = scoped_session(session_factory)
with Session() as session:
pass
if __name__ == "__main__":
CreateEngine().create_table()

How to set a M2M hybrid count property in SQLAlchemy?

I have two tables bound by a M2M relationship. Books and Writers, writers can have many books and books can have many writers.
I want to have a count property on both books and writers so I could sort them by, for example, the writer who wrote the most books.
# many to many association table
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('Writer',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'base'
id = Column(Integer, primary_key=True)
name = Column(String)
writers = relationship(Writer,secondary=book_writer_association_table,back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship(Book,secondery=book_writer_association_table,back_populates="writers")
#hybrid_property
def book_count(self):
return len(self.books)
#book_count.expression
def book_count(cls):
#what goes here?
I tried various approaches like detailed here:
class Foo(Base):
__tablename__ = 'foo'
id = Column(Integer, primary_key=True)
bar_id = Column(Integer, ForeignKey('bar.id'))
bar = relationship('Bar')
class Bar(Base):
__tablename__ = 'bar'
id = Column(Integer, primary_key=True)
#hybrid_property
def foo_count(self):
return object_session(self).query(Foo).filter(Foo.bar==self).count()
#foo_count.expression
def foo_count(cls):
return select([func.count(Foo.id)]).where(Foo.bar_id == cls.id).label('foo_count')
However, in this example, there are only two tables and I'm unsure how to achieve a more complicated join here. Another user suggested using column_property but I run into exactly the same problem there. I'm unsure how to further add tables to the join.
You can customize idea from here to M2M case. For this you should mention association_table in hybrid_property instead of Book table. So, you eliminate join with Book table and simplify your case to One-to-Many relation.
I came up with this solution.
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String, select, func, create_engine, Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, object_session, sessionmaker, Session
# Declare models
#as_declarative()
class Base:
pass
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('writer_id',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
name = Column(String)
writers = relationship("Writer", secondary=book_writer_association_table, back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship("Book", secondary=book_writer_association_table, back_populates="writers")
#hybrid_property
def book_count(self):
return object_session(self).query(book_writer_association_table).filter(book_writer_association_table.c.writer_id == self.id).count()
#book_count.expression
def book_count(cls):
return select([func.count(book_writer_association_table.c.book_id)]).where(book_writer_association_table.c.writer_id == cls.id).label('book_count')
# Load DB schema
engine = create_engine('sqlite:///sqlite3.db')
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(autocommit=True, bind=engine)
db: Session = SessionLocal()
# Creating test instances
b1 = Book(name="Book 1")
b2 = Book(name="Book 2")
db.add(b1)
db.add(b2)
w1 = Writer(name="Writer 1")
w2 = Writer(name="Writer 2")
db.add(w1)
db.add(w2)
b1.writers.append(w1)
b1.writers.append(w2)
b2.writers.append(w1)
query = db.query(Writer, Writer.book_count)
print(str(query)) # checking query
print()
writers: List[Writer] = query.all() # testing query
for writer, book_count in writers:
print(f"{writer.name}: {book_count}")
Result:
> Writer 1: 2
> Writer 2: 1
I'm unsure how to further add tables to the join.
SQL from here db.query(Writer, Writer.book_count) looks clean, without any joins. So, I think you shouldn't have any problems with subsequent joins.
> SELECT writer.id AS writer_id, writer.name AS writer_name, (SELECT count(book_writer_association.book_id) AS count_1
> FROM book_writer_association
> WHERE book_writer_association.writer_id = writer.id) AS book_count
> FROM writer
Edit: If you need join Book table to provide additional filtering you can do it like this. Here I filtered book with price less than 150
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String, select, func, create_engine, Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, object_session, sessionmaker, Session
# Declare models
#as_declarative()
class Base:
pass
book_writer_association_table = Table('book_writer_association',Base.metadata,
Column('book_id',ForeignKey('book.id'), primary_key=True),
Column('writer_id',ForeignKey('writer.id'), primary_key=True)
)
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
name = Column(String)
price = Column(Integer)
writers = relationship("Writer", secondary=book_writer_association_table, back_populates="books")
class Writer(Base):
__tablename__ = 'writer'
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship("Book", secondary=book_writer_association_table, back_populates="writers")
#hybrid_property
def book_count(self):
return (
object_session(self)
.query(book_writer_association_table)
.join(Book, Book.id == book_writer_association_table.c.book_id)
.filter(book_writer_association_table.c.writer_id == self.id)
.filter(Book.price > 150)
.count()
)
#book_count.expression
def book_count(cls):
# return select([func.count(book_writer_association_table.c.book_id)]).where(book_writer_association_table.c.writer_id == cls.id).label('book_count')
#
return (
select([func.count(book_writer_association_table.c.book_id)])
.join(Book, Book.id == book_writer_association_table.c.book_id)
.where(book_writer_association_table.c.writer_id == cls.id)
.filter(Book.price > 150)
.label('book_count')
)
# Load DB schema
engine = create_engine('sqlite:///sqlite3.db')
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(autocommit=True, bind=engine)
db: Session = SessionLocal()
# Creating test instances
b1 = Book(name="Book 1", price=100)
b2 = Book(name="Book 2", price=200)
db.add(b1)
db.add(b2)
w1 = Writer(name="Writer 1")
w2 = Writer(name="Writer 2")
db.add(w1)
db.add(w2)
b1.writers.append(w1)
b1.writers.append(w2)
b2.writers.append(w1)
query = db.query(Writer, Writer.book_count)
print(str(query)) # checking query
print()
writers: List[Writer] = query.all() # testing query
for writer, book_count in writers:
print(f"{writer.name}: {book_count}")
query:
SELECT writer.id AS writer_id,
writer.name AS writer_name,
(SELECT count(book_writer_association.book_id) AS count_1
FROM book_writer_association
JOIN book ON book.id = book_writer_association.book_id
WHERE book_writer_association.writer_id = writer.id
AND book.price > ?) AS book_count
FROM writer

Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression

Please could someone help me with this error? I have actually been really struggling to find solid, simple examples for SQLAlchemy. Whilst there are plenty of Model examples of there is not much examples of how to use these Models.
The Error:
sqlalchemy.exc.NoForeignKeysError:
Could not determine join condition between parent/child tables on relationship Species.sc_genus
- there are no foreign keys linking these tables.
Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
The Code
from sqlalchemy import Integer, Column, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relation
Base = declarative_base()
class Genus(Base):
__tablename__ = 'genus'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_sub_family = "sc_sub_family"
def __repr__(self):
return "<Genus(common_name='%s')>" % (self.scientific_name)
# Species is a child of Genus
class Species(Base):
__tablename__ = 'species'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_genus = relation("Genus", backref="species")
def __repr__(self):
return "<Species(common_name='%s')>" % (self.scientific_name)
def addSpecies(session):
species = Species()
species.common_name = "House Cat"
species.scientific_name = "Felis catus"
genus = Genus()
genus.scientific_name = "Felis"
session.add(genus)
species.sc_genus = genus
session.add(species)
session.commit()
if __name__ == "__main__":
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
## A bunch of stuff to make the connection to the database work.
engine = create_engine('sqlite:///foos.db', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
addSpecies(session)
I needed to specify a foreign key for the relationship.
class Genus(Base):
__tablename__ = 'genus'
id = Column(Integer, primary_key=True)
scientific_name = Column(String)
# sc_sub_family = "sc_sub_family"
def __repr__(self):
return "<Genus(common_name='%s')>" % (self.scientific_name)
# Species is a child of Genus
class Species(Base):
__tablename__ = 'species'
id = Column(Integer, primary_key=True)
common_name = Column(String)
scientific_name = Column(String)
sc_genus = relation("Genus", backref="species")
sc_genus_id = Column(Integer, ForeignKey('genus.id'))
def __repr__(self):
return "<Species(common_name='%s')>" % (self.scientific_name)

sqlalchemy: Inserting into joined tables

I try to add a record to a joined-inheritance table.
For some reason I cannot fathom, the dependent table is not INSERTed into.
## Inheritance test case.
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import Session, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Employee(Base):
__tablename__ = 'personnel'
__mapper_args__ = { 'polymorphic_on': 'etyp' }
id = Column(Integer, primary_key=True)
name = Column(String(50))
etyp = Column(String(10))
class Engineer(Employee):
__mapper_args__ = { 'polymorphic_identity':'eng' }
__tablename__ = "engineers"
id = Column(Integer, ForeignKey(Employee.id), index=True)
eng_data = Column(String(50))
engine = create_engine('sqlite:///', echo=True)
Base.metadata.create_all(engine)
session = Session(bind=engine)
e1 = Engineer(name="wally", eng_data="lazy guy")
session.add(e1)
session.commit()
# note that the Engineer table is not INSERTed into
e1 = session.query(Employee).filter_by(name="wally").one()
# the next line triggers an exception because it tries to lookup
# the Engineer row, which is missing
print e1.eng_data
This is Python 2.7, sqlalchemy 0.9.4 (Debian testing).
Making engineers.id a primary key will solve the problem:
class Engineer(Employee):
# ...
id = Column(Integer, ForeignKey(Employee.id), primary_key=True)

SQLAlchemy: mapping multi tables and relationship

There are some models:
from sqlalchemy import create_engine, Table, Column, Integer, \
String, MetaData, join, ForeignKey, select
from sqlalchemy.orm import sessionmaker, column_property, relationship
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///memory')
s = sessionmaker(bind = engine)()
Base = declarative_base()
metadata = MetaData()
class BaseItem(Base):
__tablename__ = 'base_item'
id = Column(Integer, primary_key=True)
name = Column(Integer)
class Stats(Base):
__tablename__ = 'stats'
id = Column(Integer, primary_key=True)
base_item_id = Column(ForeignKey('base_item.id'))
key = Column(String(60))
value = Column(Integer)
class ItemProxy(Base):
__tablename__ = 'item_proxy'
id = Column(Integer, primary_key=True)
base_item_id = Column(ForeignKey('base_item.id'))
some = Column(Integer)
bonus_item_id = Column(ForeignKey('base_item.id'))
class Item(Base):
__table__ = BaseItem.__table__.join(ItemProxy.__table__, ItemProxy.__table__.c.base_item_id == BaseItem.__table__.c.id)
id = ItemProxy.__table__.c.id
base_id = column_property(BaseItem.__table__.c.id, ItemProxy.__table__.c.base_item_id)
stat = relationship('Stats')
bonus_stat = relationship('Stats', ???)
The model ItemProxy bonus_item_id field will not be filled at all times.
What you need to specify a relationship that would have made it the correct query?

Categories

Resources