SQLAlchemy: Multiple ForeignKeyConstraint referencing the same destination Column - python

I'm trying to properly define the relations between a few tables that all use composite keys. The issue I am facing is that one Column used in those keys is common to all the tables.
Consider the following tables and relations, written using sqlalchemy:
class Match(Base):
__tablename__ = 'match'
id = Column(String, primary_key=True, nullable=False)
players = relationship('PlayerMatch', back_populates='match')
region = Column(String, primary_key=True, nullable=False)
class Player(Base):
__tablename__ = 'player'
id = Column(String, primary_key=True, nullable=False)
matches = relationship('PlayerMatch', back_populates='player')
region = Column(String, primary_key=True, nullable=False)
class PlayerMatch(Base):
__tablename__ = 'player_match'
match = relationship('Match', back_populates='players')
match_id = Column(String, primary_key=True, nullable=False)
player = relationship('Player', back_populates='matches')
player_id = Column(String, primary_key=True, nullable=False)
region = Column(String, primary_key=True, nullable=False)
__table_args__ = (
ForeignKeyConstraint(['region', 'player_id'], ['player.region', 'player.id']),
ForeignKeyConstraint(['region', 'match_id'], ['match.region', 'match.id']),
)
This is obviously conflictual for sqlachemy since it doesn't know how to fill the region attribute of PlayerMatch. It throws the following warning:
SAWarning: relationship 'PlayerMatch.match' will copy column match.region to column player_match.region, which conflicts with relationship(s): 'PlayerMatch.match' (copies player.region to player_match.region). Consider applying viewonly=True to read-only relationships, or provide a primaryjoin condition marking writable columns with the foreign() annotation.
Note that region is guaranteed to be the same for both the match and player involved in this relation. As is, it still works but gives me the previous warning. I've spent some time trying to define join conditions of the various relationships but I can't get it to work properly. I'd be fine with a mechanism to simply indicate which region (coming from Match or from Player) is to actually be persisted in PlayerMatch.
What is the correct way to achieve this?
Thanks for your help.

The solution is to specify the primaryjoin attribute of the PlayerMatch.player relationship in order for it to use both Player.id and Player.region, but to prevent from persisting the region coming from Player, as follows:
player = relationship(
'Player',
back_populates='matches',
primaryjoin="and_(Player.id == foreign(PlayerMatch.player_id), "
"Player.region == PlayerMatch.region)"
)

Related

How to select filtering by associated entries collection in sqlalchemy

I have many-to-many relationships for users and roles and I want to select user which have specific roles using realtions.
For example I want to get user having:
roles = ["role_1", "role_2", "role_3"]
so I tried
query.filter(Users.roles.contains(roles))
(where roles - List[Roles])
but I got
sqlalchemy.exc.ArgumentError: Mapped instance expected for relationship comparison to object. Classes, queries and other SQL elements are not accepted in this context; for comparison with a subquery, use Users.roles.has(**criteria).
then I tried
query.filter(Users.roles.has(Roles.name.in_(roles)))
where roles already List[str]
And I got
sqlalchemy.exc.InvalidRequestError: 'has()' not implemented for collections. Use any().
but any() selects entry that has any associated role when I need entry that has all required roles. So how to select it right way using relationships instead of joins and etc.?
class Users(sa.Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
login = Column(String(50), unique=False)
roles = relationship('Roles', secondary='user_roles_map',
cascade='all, delete')
class Roles(sa.Model):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(40), unique=True)
class UserRolesMap(sa.Model):
__tablename__ = 'user_roles_map'
id_seq = Sequence(__tablename__ + "_id_seq")
id = Column(Integer(), id_seq, server_default=id_seq.next_value(),
unique=True, nullable=False)
user_id = Column(
Integer, ForeignKey('users.id'),
primary_key=True)
role_id = Column(
Integer, ForeignKey('roles.id'),
primary_key=True)
I didn't find what I was looking for, so for now I just wrote it with joins:
query = db_session.query(Users) \
.filter_by(**parameters)
.join(UserRolesMap, UserRolesMap.user_id == Users.id)\
.filter(UserRolesMap.role_id.in_(roles_ids))\
.group_by(Users)\
.having(func.count(UserRolesMap.role_id) >= len(roles_ids))
where roles_ids was collected from Roles table before. And if you need user with only required roles you can replace ">=" with "==".

django query to get data

I am into a very confusing situation where I have one to many relation and I want to query data like I want all parent table data but want to get only data from child tables which fulfill condition of site_id = 100.
class Policy(Base):
"""table containing details for Policies"""
__tablename__ = "UmbrellaPolicy"
id = Column(Integer, primary_key=True)
policy_id = Column(Integer, nullable=False, index=True)
user_defined_name = Column(String(255), nullable=True)
and child is like this
class Site(Base):
__tablename__ = "Site"
id = Column(Integer, primary_key=True)
policy_id = Column(Integer, ForeignKey("Policy.id"))
site_id = Column(String(32), nullable=False, index=True)
policy = relationship("Policy", backref="sites")
You should be able to filter join relations like this
parents = Policy.objects.filter(site__site_id=100)
You can find more info about the Django query API here but its generally of the form where you reference the relation with classname__columnname there are many other ways to filter/query that you can reference in the docs

Many to many relationship in sqlAlchemy with two foreign keys to the same table

I have these class models:
class Application(Base):
__tablename__ = 'applications'
identifier = Column(Integer, primary_key=True)
name = Column(String)
description = Column(String)
level1 = relationship("Teams", secondary='assoc_apps_teams', back_populates='support_1')
level2 = relationship("Teams", secondary='assoc_apps_teams', back_populates='support_2')
class Teams(Base):
__tablename__ = 'teams'
identifier = Column(Integer, primary_key=True)
name = Column(String)
description = Column(String)
support_1 = relationship("Application", secondary='assoc_apps_teams', back_populates='level1')
support_2 = relationship("Application", secondary='assoc_apps_teams', back_populates='level2')
class AssocAppsTeams(Base, DictSerializable):
__tablename__ = 'assoc_apps_teams'
identifier = Column(Integer, primary_key=True)
apps_id = Column(Integer, ForeignKey("applications.identifier"), nullable=False)
support1_id = Column(Integer, ForeignKey("teams.identifier"), nullable=False)
support2_id = Column(Integer, ForeignKey("teams.identifier"), nullable=False)
if __name__ == "__main__":
app = model.Application("app", "desc")
session.add(app)
session.commit()
session.close()
Consedering that an application has 2 support levels, each level can have one or more than one team ( each support is a team).
When I run my script I get this error:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Applications.support1 - there are multiple foreign key paths linking the tables via secondary table 'assoc_apps_levels'. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference from the secondary table to each of the parent and child tables.
In this case, I have a relationship many to many: an application is managed by two levels of support that are teams, and each support can manage more than one application. How to map this relationship correctly??

SqlAlchemy can't determine join condition

I have 2 tables defined:
class TCableSet(Base):
__tablename__ = 'tCableSet'
ixCableSet = Column(Integer, primary_key=True)
decCableSetOne = Column(Numeric(8, 2))
decCableSetTwo = Column(Numeric(8, 2))
decCableSetThree = Column(Numeric(8, 2))
class TStepVoltage(Base):
__tablename__ = 'tStepVoltage'
ixStepVoltage = Column(Integer, primary_key=True)
ixSubReport = Column(Integer, ForeignKey('tSubReport.ixSubReport'), nullable=False)
iVoltage = Column(Integer)
ixPhaseA = Column(Integer, ForeignKey('tCableSet.ixCableSet'), nullable=False)
ixPhaseB = Column(Integer, ForeignKey('tCableSet.ixCableSet'), nullable=False)
ixPhaseC = Column(Integer, ForeignKey('tCableSet.ixCableSet'), nullable=False)
sub_report = relationship('TSubReport',
backref=backref('step_voltage'))
I understand why I am getting this error but can't figure out a proper way (yet).
When the table gets saved, I store the values in the tCableSet table and then use the id as a foreign key in my tStepVoltage table. The problem I have is when I go to retrieve the data, I want to be able to get the values(tCableSet row) along with the rest of my tStepVoltage table via a relationship, however I'm not sure how to go about this since I don't have a field in my tCableSet that can directly be linked via relationship to my tStepVoltage. I basically just needed the tCableSet for normalization
Since you have more than one foreign key that points to the same table, you have to tell SQLAlchemy which foreign key to use.
For example:
sub_report_a = relationship('TSubReport',
backref=backref('step_voltage'),
foreign_keys=[ixPhaseA])

Python/SqlAlchemy 3-way circular dependency

I am having an issue with creating a set of relationships among 3 of my tables. When I run the code to create the tables I get a circular dependency error.
I tried fiddling around with use_alter and post_update based on responses to similar posts, but I wasn't able to solve the issue.
Basically a map has a set of locations, a character has a set of maps, but a character is also located on one of those map locations.
In addition, a map can have parent/child relationships with other maps.
class Character(Base):
__tablename__ = 'character'
ID = Column(Integer, primary_key=True)
name = Column(Unicode(255), nullable=False)
classID = Column(Integer, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
profileID = Column(Integer, ForeignKey('profile.ID'), nullable=False)
locationID = Column(Integer, ForeignKey('location.ID'))
location = relationship("Location")
maps = relationship("Map", backref="owner", cascade="save-update, merge, delete, delete-orphan")
class Map(Base):
__tablename__ = 'map'
ID = Column(Integer, primary_key=True)
name = Column(Unicode(255))
maptypeID = Column(Integer, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
parentID = Column(Integer, ForeignKey('map.ID'))
ownerID = Column(Integer, ForeignKey('character.ID'))
children = relationship("Map", backref=backref("parent", remote_side="Map.ID"))
locations = relationship("Location", backref='map', cascade="save-update, merge, delete, delete-orphan")
class Location(Base):
__tablename__ = 'location'
ID = Column(Integer, primary_key=True)
x = Column(Integer, nullable=False)
y = Column(Integer, nullable=False)
locationtypeID = Column(Integer, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
mapID = Column(Integer, ForeignKey('map.ID'), nullable=False)
How can I solve this issue?
Edit (Solved):
After some more playing around with use_alter, I was able to solve the problem by changing the mapID definition in the Location class from:
mapID = Column(Integer, ForeignKey('map.ID'), nullable=False)
To:
mapID = Column(Integer, ForeignKey('map.ID', use_alter=True, name="fk_location_map"), nullable=False)
In response to the recommendation of breaking the cyclic dependency, I'd rather have the correct relationships and data integrity represented in the schema. I'd much rather worry about fixing ORM issues or changing ORMs rather than fudging the schema to fit the ORM's expectations.
In this particular case, I can't think of a more concise and elegant way to represent all of the information(in the most fundamental sense of the word) that I need the schema to represent for the application.
Side note: Having worked with other languages/frameworks/ORMs, this kind of snafu is often automatically accounted for by the ORM. For example, in .NET E/F, I believe the FK constraints are typically added and activated after all the table creation statements.
You can probably get alembic to help you with a cyclic dependency in your schema, but I'm not going to help you with that. Instead, I'm going to strongly urge you to break the cyclic dependency.
If i'm understanding your schema, you have the rooms or buildings in your model represented by Location. the players/monsters in your schema are Characters, which at any time must be in exactly one location. Additionally, there are some Map items floating around, which are always about some specific location, and are always in the inventory of exactly one character.
So here's how I'd spell that
class Location:
id = Column(Integer, primary_key=True)
class Character:
id = Column(Integer, primary_key=True)
location_id = Column(ForeignKey(Location.id))
location = relationship(Location, backref="characters")
class Map:
id = Column(Integer, primary_key=True)
location_id = Column(ForeignKey(Location.id))
owner_id = Column(ForeignKey(Character.id))
location = relationship(Location, backref="maps")
owner = relationship(Character, backref="inventory")

Categories