Fastapi delete only one record in many to many table - sqlalchemy

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)

Related

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.

SqlAlchemy error: Foreign key could not find table

So I'm building an app and I'm trying to save new changes to my database but when I try to commit the changes in the flask using db.session.commit() it returns me the following error:
sqlalchemy.exc.NoReferencedTableError: Foreign key associated with column 'products.country_id' could not find table 'countries' with which to generate a foreign key to target column 'id'
In my models.py I have the following:
from app import db
from . import db
from datetime import datetime
def now():
return datetime.now()
class Countries(db.Model):
__tablename__ = 'countries'
__table_args__ = {'schema': 'products_data'}
id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True)
name = db.Column(db.String(255))
code = db.Column(db.String(45))
def __repr__(self):
return f'Id {self.id}'
class Categories(db.Model):
__tablename__ = 'categories'
__table_args__ = {'schema': 'products_data'}
id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True)
name = db.Column(db.String(255))
def __repr__(self):
return f'Id {self.id}'
class Brands(db.Model):
__tablename__ = 'brands'
__table_args__ = {'schema': 'products_data'}
id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True)
name = db.Column(db.String(255))
logo = db.Column(db.String(5000))
feed = db.Column(db.String(5000))
feed_type = db.Column(db.String(45))
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
country_id = db.Column(db.Integer, db.ForeignKey('countries.id'))
awinmid = db.Column(db.Integer)
def __repr__(self):
return f'Id {self.id}'
class Products(db.Model):
__tablename__ = 'products'
__table_args__ = {'schema': 'products_data'}
id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True)
name = db.Column(db.String(255))
url = db.Column(db.Text)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
country_id = db.Column(db.Integer, db.ForeignKey('countries.id'))
price = db.Column(db.Float)
currency = db.Column(db.String(45))
discount_price = db.Column(db.Float)
shipping = db.Column(db.Float)
brand_id = db.Column(db.Integer, db.ForeignKey('brands.id'))
Am I doing anything wrong when associating a column in products with a foreign key? This is the first time I encounter this error so I'm really lost on what to do right now.
To fix I just added the schema to the db.ForeignKey and it worked
Example:
db.ForeignKey('products_data.countries.id')
PS:
Not my idea. Just wanted to post the answer in case someone visits the post later with the same problem.
Gord Thompson thanks for the help!
First of all, I don t see any table Categories. Secondly, you copy pasted your schema from the Products table into your Countries one.
PS: By default sqlalchemy gives the tables the name of the class (lower cased). So your __tablename__='products' does nothing actually.
EDIT:
The problem with your code lies in how you set the __table_args__ attribute. You assign an object to it, which by their specifications is wrong.
Take a look at the following example and modify your code accordingly
__table_args__ = ({'schema': 'products_data'})
Also for further reference, take a look at this https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/table_config.html

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

SQLAlchemy: How to delete a item on one-to-many relation?

I am using SQLAlchemy, and i want to delete certain item in a one-to-many-relationship. Well there are two pictures for you. First you can see an EER-Model. Its a one (gender) to many (person) relation.
Second, you can see a table with fictional data. In gender-table we have two gender currently, and in person-table we have three persons.
Imagine, you will just delete a gender, let us say 'male'. But we see, that 'male' is used as foreign by person-table.
My current source code looks as follows:
class PERSON_GENDER(Base):
__tablename__ = "person_gender"
id = Column(Integer, primary_key=True, unique=True, autoincrement=True)
gender = Column(String(50), nullable=False, unique=True)
class PERSON(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True, unique=True, autoincrement=True)
nickname = Column(String(255))
alias_name = Column(String (255))
name_normally_used = Column(String(50), nullable=False)
first_middle_name = Column(String(255))
last_name = Column(String(100))
birth_name = Column(String(100))
body_height = Column(String(10))
wedding_anniversary = Column(Date)
birthday = Column(Date)
day_of_death = Column(Date)
notice = Column(Text())
gender_id = Column(Integer, ForeignKey('person_gender.id', ondelete='CASCADE'))
gender = relationship("PERSON_GENDER", single_parent=True, cascade="all, delete, delete-orphan")
When I run this one, then not only the gender is deleted, but also the person. I want that only the gender is deleted, not the person.
ondelete='CASCADE')=> ondelete='SET NULL')

SQLAlchemy Circular dependency detected

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)