SQLAlchemy Circular dependency detected - sqlalchemy

I created two models for my application with SQLAlchemy, but while using it i receive this error message:
sqlalchemy.exc.CircularDependencyError: Circular dependency detected. (ProcessState(ManyToOneDP(PlayerModel.clan), <PlayerModel at 0x7f9503070ac8>, delete=False), ProcessState(ManyToOneDP(ClanModel.commander), <ClanModel at 0x7f95030b8da0>, delete=False), ProcessState(OneToManyDP(ClanModel.players), <ClanModel at 0x7f95030b8da0>, delete=False), SaveUpdateState(<PlayerModel at 0x7f9503070ac8>), SaveUpdateState(<ClanModel at 0x7f95030b8da0>), ProcessState(ManyToOneDP(ClanModel.owner), <ClanModel at 0x7f95030b8da0>, delete=False))
How can I fix this error?
class PlayerModel(Base):
__tablename__ = 'player'
chat_id = Column(Integer, primary_key=True)
clan_id = Column(Integer, ForeignKey('clan.id'))
clan = relationship("ClanModel", foreign_keys='PlayerModel.clan_id', back_populates="players", primaryjoin="PlayerModel.clan_id==ClanModel.id")
def __repr__(self):
return "<User(name='{}',level={})>".format(self.name, self.level)
class ClanModel(Base):
__tablename__ = 'clan'
id = Column(Integer, primary_key=True)
war_with_id = Column(Integer, ForeignKey('clan.id'))
war_with_from = relationship(
'ClanModel',
uselist=False,
remote_side=[id],
backref=backref('war_with_to', uselist=False),
)
owner_id = Column(Integer, ForeignKey('player.chat_id'))
owner = relationship("PlayerModel", uselist=False, foreign_keys=owner_id)
commander_id = Column(Integer, ForeignKey('player.chat_id'))
commander = relationship("PlayerModel", uselist=False, foreign_keys=commander_id)
players = relationship("PlayerModel", back_populates="clan", foreign_keys=PlayerModel.clan_id)

Related

Fastapi delete only one record in many to many table

I am building a Fastapi application that uses SQLAlchemy, and I am trying to implement a many-to-many relationship. My problem is when I try to delete a record in intermediate table it deletes all records.
Example of table 'device_protocol'
device_id
protocol_id
status_id
1
1
1
1
2
1
1
3
3
If I try to remove only device_id=1 with protocol_id=2 it actually removes all records with device_id=1
Models:
class DeviceProtocolAssociation(Base):
__tablename__ = "device_protocol"
device_id = Column(Integer, ForeignKey("device.id", ondelete="CASCADE"), primary_key=True)
device = relationship("Device", back_populates="device_protocols")
protocol_id = Column(Integer, ForeignKey("protocol.id"), primary_key=True)
protocol = relationship("Protocol", back_populates="device_protocols")
status_id = Column(Integer, ForeignKey("status.id"), nullable=True)
status = relationship("Status", back_populates="device_protocols")
class Device(Base):
__tablename__ = "device"
id = Column(Integer, primary_key=True, unique=True, index=True)
name = Column(String(255))
status_id = Column(Integer, ForeignKey('status.id'))
status = relationship("Status", back_populates="devices")
device_protocols = relationship(DeviceProtocolAssociation, back_populates="device")
protocols = association_proxy("device_protocols", "protocols")
class Protocol(Base):
__tablename__ = "protocol"
id = Column(Integer, primary_key=True, unique=True, index=True)
name = Column(String(255))
device_protocols = relationship(DeviceProtocolAssociation, back_populates="protocol")
devices = association_proxy("device_protocols", "devices")
class Status(Base):
__tablename__ = "status"
id = Column(Integer, primary_key=True, unique=True, index=True)
name = Column(String(255))
description = Column(String(255), nullable=True)
devices = relationship("Device", back_populates="status")
device_protocols = relationship(DeviceProtocolAssociation, back_populates="status")
Router:
#router.delete('/{device_id}/{protocol_id}')
async def delete_status(device_id: int, protocol_id: int, db:Session=Depends(get_db)):
relation_query = db.query(DeviceProtocolAssociation).filter(DeviceProtocolAssociation.device_id==device_id and DeviceProtocolAssociation.protocol== protocol_id)
db_relation = relation_query.first()
if not db_relation:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f'No relation with this id: {id} found')
relation_query.delete(db_relation)
db.commit()
return {"relation": "deleted"}
How can I remove only a record?
You can't use python's and when filtering in SQLAlchemy. You can either pass several conditions to the filter
relation_query = db.query(DeviceProtocolAssociation).filter(
DeviceProtocolAssociation.device_id == device_id,
DeviceProtocolAssociation.protocol == protocol_id,
)
or use the binary and-operator &:
relation_query = db.query(DeviceProtocolAssociation).filter(
(DeviceProtocolAssociation.device_id == device_id)
& (DeviceProtocolAssociation.protocol == protocol_id)
)
Solution to my problem is:
relation_query = db.query(DeviceProtocolAssociation).filter(DeviceProtocolAssociation.device_id==device_id, DeviceProtocolAssociation.protocol_id==protocol_id)

SQLAlchemy - how to make column unique per ForeignKey relationship?

In the Todo model below there is a unique constraint put on text column.
How can I narrow this constraint to validate uniqueness per "foreign-keyed" user only, not per all users as it is now?
I use SQLite.
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class Todo(Base):
__tablename__ = 'todos'
id = Column(Integer, primary_key=True, index=True)
text = Column(String, unique=True)
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship("User", back_populates="todos")
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
todos = relationship("Todo", back_populates="user")
If you use postgresql, you can use the Partial Index to implement this:
class Todo(Base):
__tablename__ = 'todos'
id = Column(Integer, primary_key=True, index=True)
text = Column(String) # ! removed the unique from here
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship("User", back_populates="todos")
# ! added unique index with
__table_args__ = (
Index(
"todo_text_uc",
"text",
unique=True,
postgresql_where=(user_id != None),
# postgresql_where=(~user_id.is_(None)), # equivalent to the row above, but no linting warnings
),
)
For sqlite just replace postgresql_where with sqlite_where.

Many-to-One relationships in SQLAlchemy

I'm having trouble with Many-to-One relationships between my SQLAlchemy models. The relationship between ChangeOrder (many) and Contract (one) is fine, but the one between LineItem (many) and ChangeOrder (one) isn't working.
I've tried both approaches suggested in the basic relationships docs and both fail for the LineItem to ChangeOrder relationship.
# using back_populates
class Contract(Base):
__tablename__ = "contract"
id = Column(Integer, primary_key=True)
change_orders = relationship("ChangeOrder", back_populates="contract")
class ChangeOrder(Base):
__tablename__ = "changeorder"
id = Column(Integer, primary_key=True)
line_items = relationship("LineItem", back_populates="change_order")
contract_id = Column(Integer, ForeignKey("contract.id"))
contract = relationship("Contract", back_populates="change_orders")
class LineItem(Base):
__tablename__ = "lineitem"
id = Column(Integer, primary_key=True)
change_order_id = Column(Integer, ForeignKey("changeorder.id"))
change_order = relationship("ChangeOrder", back_populates="line_items")
def test_insert_change_order(db_session, item):
c = Contract()
db_session.add(c)
db_session.commit()
co = ChangeOrder(contract_id=c.id)
db_session.add(co)
db_session.commit()
row = db_session.query(Contract).get(c.id)
assert len(row.change_orders) == 1 # this Many-to-One works
li = LineItem(change_order_id=co.id)
db_session.add(li)
db_session.commit()
row = db_session.query(ChangeOrder).get(co.id)
assert len(row.line_items) == 1 # this Many-to-One does not
I also tried the backref approach, but it has the same problem.
# using backref
class LineItem(Base):
__tablename__ = "lineitem"
id = Column(Integer, primary_key=True)
change_order_id = Column(Integer, ForeignKey("changeorder.id"))
change_order = relationship("ChangeOrder", backref="line_items")
class ChangeOrder(Base):
__tablename__ = "changeorder"
id = Column(Integer, primary_key=True)
contract_id = Column(Integer, ForeignKey("contract.id"))
contract = relationship("Contract", backref="change_orders")
class Contract(Base):
__tablename__ = "contract"
id = Column(Integer, primary_key=True)
conftest.py
import pytest
from flask_sqlalchemy import SQLAlchemy
from frontend.app import app
#pytest.fixture
def testapp():
db = SQLAlchemy()
app.config["SQLALCHEMY_ECHO"] = True
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
#pytest.fixture(scope="session")
def database():
db = SQLAlchemy()
with app.app_context():
db.create_all()
yield db
#pytest.fixture(scope="session")
def _db(database):
return database

How to query a relationship on multiple polymorphic-inheritance tables?

Let's say you have the following simplified example schema, which uses SQLAlchemy joined table polymorphic inheritance. Engineer and Analyst models have a Role relationship. The Intern model does not.
class Role(db.Model):
__tablename__ = 'role'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(16), index=True)
class EmployeeBase(db.Model):
__tablename__ = 'employee_base'
id = db.Column(db.Integer, primary_key=True)
some_attr = db.Column(db.String(16))
another_attr = db.Column(db.String(16))
type = db.Column(db.String(50), index=True)
__mapper_args__ = {
'polymorphic_identity': 'employee',
'polymorphic_on': type
}
class Engineer(EmployeeBase):
__tablename__ = 'engineer'
id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)
role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)
role = db.relationship('Role', backref='engineers')
__mapper_args__ = {
'polymorphic_identity': 'engineer',
}
class Analyst(EmployeeBase):
__tablename__ = 'analyst'
id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)
role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)
role = db.relationship('Role', backref='analysts')
__mapper_args__ = {
'polymorphic_identity': 'analyst',
}
class Intern(EmployeeBase):
__tablename__ = 'intern'
id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)
term_ends = db.Column(db.DateTime, index=True, nullable=False)
__mapper_args__ = {
'polymorphic_identity': 'intern',
}
If I want to find Employees with a Role name having "petroleum" somewhere in the name, how would I do that?
I've tried many, many approaches. The closest I've come is this, which only returns Analyst matches:
employee_role_join = with_polymorphic(EmployeeBase,
[Engineer, Analyst])
results = db.session.query(employee_role_join).join(Role).filter(Role.name.ilike('%petroleum%'))
If I try to do something like this, I get an AttributeError, because I'm searching on an attribute of the joined Role table:
employee_role_join = with_polymorphic(EmployeeBase,
[Engineer, Analyst])
results = db.session.query(employee_role_join).filter(or_(
Engineer.role.name.ilike('%petroleum%'),
Analyst.role.name.ilike('%petroleum%')))
You can try specifying the join ON clause explicitly since the issue with your first query seems to be that Role is joining only on the analyst.role_id column:
employee_role_join = with_polymorphic(EmployeeBase, [Engineer, Analyst])
results = session.query(employee_role_join).join(Role).filter(Role.name.ilike('%petroleum%'))
print(str(results))
SELECT employee_base.id AS employee_base_id,
employee_base.some_attr AS employee_base_some_attr,
employee_base.another_attr AS employee_base_another_attr,
employee_base.type AS employee_base_type,
engineer.id AS engineer_id,
engineer.role_id AS engineer_role_id,
analyst.id AS analyst_id,
analyst.role_id AS analyst_role_id
FROM employee_base
LEFT OUTER JOIN engineer ON employee_base.id = engineer.id
LEFT OUTER JOIN analyst ON employee_base.id = analyst.id
JOIN role ON role.id = analyst.role_id
WHERE lower(role.name) LIKE lower(?)
employee_role_join is an AliasedClass that exposes both Analyst and Engineer, which we can then use to create a join-ON clause like so:
results = session.query(employee_role_join)\
.join(Role, or_( \
employee_role_join.Engineer.role_id==Role.id, \
employee_role_join.Analyst.role_id==Role.id \
))\
.filter(Role.name.ilike('%petroleum%'))
which changes the resulting SQL to JOIN role ON engineer.role_id = role.id OR analyst.role_id = role.id
Define the role_id on EmployeeBase. Even though Intern doesn't have the relationship back to the role table, the field can be null for that case.
I changed EmployeeBase to this:
class EmployeeBase(db.Model):
__tablename__ = 'employee_base'
id = db.Column(db.Integer, primary_key=True)
role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)
given_name = db.Column(db.String(16))
surname = db.Column(db.String(16))
type = db.Column(db.String(50), index=True)
__mapper_args__ = {
'polymorphic_identity': 'employee',
'polymorphic_on': type
}
And removed the role_id column definition from all other employee models.
db.create_all()
petrolium_engineer = Role(name='Petrolium Engineer')
geotech_engineer = Role(name='Geotech Engineer')
analyst_petrolium = Role(name='Analyst of Petrolium')
db.session.add(petrolium_engineer)
db.session.add(geotech_engineer)
db.session.add(analyst_petrolium)
db.session.add(
Intern(given_name='Joe', surname='Blogs', term_ends=datetime.now())
)
db.session.add(
Engineer(given_name='Mark', surname='Fume', role=petrolium_engineer)
)
db.session.add(
Engineer(given_name='Steve', surname='Rocks', role=geotech_engineer)
)
db.session.add(
Analyst(given_name='Cindy', surname='Booker', role=analyst_petrolium)
)
db.session.commit()
petrolium_roles = db.session.query(EmployeeBase).join(Role).\
filter(Role.name.contains('Petrolium')).all()
for emp in petrolium_roles:
print(f'{emp.given_name} {emp.surname} is {emp.role.name}')
# Mark Fume is Petrolium Engineer
# Cindy Booker is Analyst of Petrolium

Abstract Table Concrete Inheritance. Could not determine join condition between parent/child tables

I've got following example code:
models.py
class CadastralObject(Base):
__tablename__ = 'cadastral_object'
def __init__(self, cadastral_region, cadastral_district, cadastral_block, cadastral_object):
self.cadastral_region = cadastral_region
self.cadastral_district = cadastral_district
self.cadastral_block = cadastral_block
self.cadastral_object = cadastral_object
# this is a combined PK
cadastral_region = Column(Integer, primary_key=True, index=True)
cadastral_district = Column(Integer, primary_key=True, index=True)
cadastral_block = Column(Integer, primary_key=True, index=True)
cadastral_object = Column(Integer, primary_key=True, index=True)
encumbrances = relationship("Encumbrance")
class Encumbrance(Base):
__tablename__ = 'encumbrance'
id = Column(Integer, primary_key=True, index=True)
def __init__(self, cadastral_object):
self.parent_cadastral_region = cadastral_object.cadastral_region
self.parent_cadastral_district = cadastral_object.cadastral_district
self.parent_cadastral_block = cadastral_object.cadastral_block
self.parent_cadastral_object = cadastral_object.cadastral_object
# FK fields
parent_cadastral_region = Column(Integer, nullable=False)
parent_cadastral_district = Column(Integer, nullable=False)
parent_cadastral_block = Column(Integer, nullable=False)
parent_cadastral_object = Column(Integer, nullable=False)
parent_object = relationship(CadastralObject)
__table_args__ = (ForeignKeyConstraint(
[
parent_cadastral_region,
parent_cadastral_district,
parent_cadastral_block,
parent_cadastral_object],
[
CadastralObject.cadastral_region,
CadastralObject.cadastral_district,
CadastralObject.cadastral_block,
CadastralObject.cadastral_object]),
{}
)
this code works as intended:
main.py
c = CadastralObject(1, 2, 3, 4)
session.add(c)
e = Encumbrance(c)
session.add(e)
session.commit()
print(c.encumbrances)
print(e.parent_object)
results:
[<app.models.Encumbrance object at 0x000001C9B820BCC0>]
<app.models.CadastralObject object at 0x000001C9B820BB00>
however, when I'm trying convert my code to Concrete Inheritance:
imodels.py
class iCadastralObject(AbstractConcreteBase, Base):
def __init__(self, cadastral_region, cadastral_district, cadastral_block, cadastral_object):
self.cadastral_region = cadastral_region
self.cadastral_district = cadastral_district
self.cadastral_block = cadastral_block
self.cadastral_object = cadastral_object
# this is a combined PK
cadastral_region = Column(Integer, primary_key=True, index=True)
cadastral_district = Column(Integer, primary_key=True, index=True)
cadastral_block = Column(Integer, primary_key=True, index=True)
cadastral_object = Column(Integer, primary_key=True, index=True)
#declared_attr
def encumbrances(self):
return relationship("iEncumbrance")
class Building(iCadastralObject):
__tablename__ = 'building'
__mapper_args__ = {
'polymorphic_identity': 'building',
'concrete': True
}
#declared_attr
def encumbrances(self):
return relationship("iEncumbrance")
class Flat(iCadastralObject):
__tablename__ = 'flat'
__mapper_args__ = {
'polymorphic_identity': 'flat',
'concrete': True
}
#declared_attr
def encumbrances(self):
return relationship("iEncumbrance")
class Construction(iCadastralObject):
__tablename__ = 'construction'
__mapper_args__ = {
'polymorphic_identity': 'construction',
'concrete': True
}
class iEncumbrance(Base):
__tablename__ = 'iencumbrance'
id = Column(Integer, primary_key=True, index=True)
def __init__(self, cadastral_object):
self.parent_cadastral_region = cadastral_object.cadastral_region
self.parent_cadastral_district = cadastral_object.cadastral_district
self.parent_cadastral_block = cadastral_object.cadastral_block
self.parent_cadastral_object = cadastral_object.cadastral_object
# FK fields
parent_cadastral_region = Column(Integer, nullable=False)
parent_cadastral_district = Column(Integer, nullable=False)
parent_cadastral_block = Column(Integer, nullable=False)
parent_cadastral_object = Column(Integer, nullable=False)
parent_object = relationship(iCadastralObject)
__table_args__ = (ForeignKeyConstraint(
[
parent_cadastral_region,
parent_cadastral_district,
parent_cadastral_block,
parent_cadastral_object],
[
iCadastralObject.cadastral_region,
iCadastralObject.cadastral_district,
iCadastralObject.cadastral_block,
iCadastralObject.cadastral_object]),
{}
)
I'm getting an error on "from app.imodels import Building"
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on relationship iCadastralObject.encumbrances - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.