Sqlalchemy: Propagation of updates across multiple (linked) relationships - orm

I show here an (artificial) example of three linked tables: ParentA, ChildA, and ChildAA. ChildA is related to the primary key (PK) of ParentA via foreign key, and ChildAA relates to the same key in ChildA. In this way ChildAA links to the primary key of the ParentA. I would expect that when I make a change to the ParentA PK this change propagates back to the corresponding ChildAA's attribute, but it doesn't.
Thanks in advance!
(I apologize if this has been answered or documented before, I really couldn't find anything.)
The Code:
from sqlalchemy import *
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class ParentA(Base):
__tablename__ = 'tbl_parentA'
pid = Column(Integer, primary_key=True)
childA = orm.relationship("ChildA", passive_updates=False, backref='parentA')
class ChildA(Base):
__tablename__ = 'tbl_childA'
attrib1 = Column(String, nullable=True)
parentA_id = Column(Integer, ForeignKey(ParentA.pid), primary_key=True)
childAA = orm.relationship("ChildAA", passive_updates=False, backref="childA")
# This class is related to Parents through ChildA
class ChildAA(Base):
__tablename__ = 'tbl_childAA'
cid = Column(Integer, primary_key=True)
attrib1 = Column(String, nullable=True)
parentA_id = Column(Integer, ForeignKey(ChildA.parentA_id))
def clear_db(db):
tmp = db.echo
db.echo = False
metadata = MetaData(bind=db)
metadata.reflect(db)
for table in reversed(metadata.sorted_tables):
table.drop(db)
metadata.clear()
db.echo = tmp
if __name__ == '__main__':
# SQLite Connection
db = create_engine('sqlite:///linked_updates.db')
# db.echo = True
# Initalize Objects
pa1 = ParentA()
ca1 = ChildA(attrib1='ca1 str')
caa1= ChildAA(attrib1='caa1 str')
# Assign a parent to ChildA
ca1.parentA = pa1
# Assign a parent to ChildAA
caa1.childA = ca1
# Initialize clean DB & session
clear_db(db)
Base.metadata.create_all(db)
session = orm.create_session(db)
# Write to DB
session.add_all([pa1, ca1, caa1])
session.flush()
print 'After flush, we have: ', caa1.parentA_id, '==', caa1.childA.parentA_id
# Induce change, check propagation
pa1.pid = 2
session.flush()
print 'I expect: ', caa1.parentA_id, '==', caa1.childA.parentA_id
print 'END'

Related

ArgumentError: Mapper mapped class Roles_users->User Roles could not assemble any primary key columns for mapped table 'User Roles'

I'm trying to write a web app with user roles and I keep getting this error no matter the changes I make.
Could you please help me find what's wrong?
Thank you in advance!!
This is my models.py file:
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey
from flask_login import UserMixin
from sqlalchemy.orm import relationship, backref
import db
class Rol(db.Base):
__tablename__ = 'Roles'
id_rol = Column(Integer, primary_key=True)
nombre_rol = Column(String, nullable=False)
user_rol = relationship("User rol", back_populates="rol", uselist =False)
def __init__(self, nombre_rol):
self.nombre_rol = nombre_rol
class User(UserMixin, db.Base):
__tablename__ = 'Users'
user_id = Column(Integer, primary_key=True)
username = Column(String, unique=True, nullable=False)
password = Column(String, unique=True, nullable=False)
is_active = Column(Boolean, default=False)
user_rol = relationship("User rol", back_populates="user", uselist =False)
def __init__(self,username,password,is_active):
self.username = username
self.password = password
self.is_active = is_active
def get_id(self):
return self.id
def is_active(self):
return self.is_active
def activate_user(self):
self.is_active = True
def get_username(self):
return self.username
def get_rol(self):
return self.rol
class Roles_users(db.Base):
__tablename__ = 'User Roles'
user_id = Column(Integer, ForeignKey("Users.user_id"))
id_rol = Column(Integer, ForeignKey("Roles.id_rol"))
user = relationship("User", back_populates="user roles")
rol = relationship("Rol", back_populates="user roles")

How do I check if many-to-many row already exists in sqlalchemy to prevent duplicate?

Here is code for my models:
class IllustratorAsset(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
uploader = db.Column(db.Integer, db.ForeignKey('user.id'))
project = db.Column(db.Integer, db.ForeignKey('project.id'))
fragments = db.relationship('AssetFragment', backref='ai', lazy='dynamic')
file_hash = db.Column(db.String(128), unique=True)
file_name = db.Column(db.String(128))
def __repr__(self):
return '<Picture {}>'.format(self.id)
tags_association = db.Table(
'tags_association',
db.Column('fragment_id', db.Integer(), db.ForeignKey('asset_fragment.id'),index=True),
db.Column('tag_id', db.Integer(), db.ForeignKey('tag.id'),index=True),
PrimaryKeyConstraint('fragment_id', 'tag_id')
)
class AssetFragment(db.Model):
id = db.Column(db.Integer, primary_key=True)
asset = db.Column(db.Integer, db.ForeignKey('illustrator_asset.id'))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
uploader = db.Column(db.Integer, db.ForeignKey('user.id'))
tags = db.relationship('Tag', secondary=tags_association, backref='fragments', lazy='dynamic')
file_name = db.Column(db.String(128))
file_hash = db.Column(db.String(128))
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.String(64), index=True, unique=True)
def __repr__(self):
return '<Tag {}>'.format(self.tag)
The idea is illustrations storage, there is Asset (Adobe Illustrator file), it has Fragments (renders of its different fragments), the fragment has some Tags for quick search which should be working through many-to-many relationship tags_association.
Currently, I'm stuck at trying to check if an association already exists, I'm sure there is some simple way to do it, but I'm missing some point. I came up with the code below, and I still get the exception of constraints.
for tag in new_tags: #list of tags from form
t = Tag.query.filter_by(tag=tag).first()
if (t is not None) and not(t in fragment.tags):
fragment.tags.append(t)
else:
new_tag = Tag(tag=tag)
if not(new_tag in fragment.tags):
fragment.tags.append(new_tag)
DB is Postgres
Well answer came shortly after I've tried to do it in flask shell. Shell output
I've realized, that it seems to be something wrong with the comparator, simple override makes it work:
def __eq__(self, __o: object) -> bool:
return self.tag == __o.tag
Also needed to change checking by a bit:
for tag in new_tags:
t = Tag.query.filter_by(tag=tag).first()
if t is None:
new_tag = Tag(tag=tag)
print(new_tag)
if not(new_tag in fragment.tags):
fragment.tags.append(new_tag)
else:
print(t)
if not(t in fragment.tags):
fragment.tags.append(t)

sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed

I am trying to create table for some courses, wհօse last column is another table and presents the lecturers names.
class Teachers(Base):
__tablename__ = 'teacher'
full_name = Column(String, unique=False, primary_key=True)
company = Column(String, unique = False)
class Course(Base):
__tablename__ = 'course'
course_name = Column(String, nullable=False)
course_id = Column(String,primary_key=True)
level = Column(String,unique=False)
teachers = Teachers
But I found following issue when the name of one lecturer is repeated in different courses:
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: teacher.full_name
How can I solve this problem?
Your teachers column is not a column. Instead you are adding a whole table as an attribute.
What you should do, is defining a column in Course that contains the primary key of the Teacher (assuming there is only one teacher per course) and mark it as a ForeignKey. You can than also add a relationship for easier work.
from pathlib import Path
import sqlalchemy.orm
from sqlalchemy import Column, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Teacher(Base):
__tablename__ = "teachers"
full_name = Column(String, unique=False, primary_key=True)
company = Column(String, unique=False)
class Course(Base):
__tablename__ = "courses"
course_id = Column(String, primary_key=True)
course_name = Column(String, nullable=False)
level = Column(String, unique=False)
teacher_name = Column(ForeignKey("teachers.full_name"))
teacher = relationship("Teacher")
def main():
if Path("test_database.db").exists():
Path("test_database.db").unlink()
engine = create_engine("sqlite:///test_database.db")
Base.metadata.create_all(engine)
session = sqlalchemy.orm.Session(autocommit=False, autoflush=False, bind=engine)
teacher_1 = Teacher(full_name="Bob")
course_1 = Course(course_id=1, course_name="Course 1", level="beginner", teacher=teacher_1)
course_2 = Course(course_id=2, course_name="Course 2", level="advanced", teacher=teacher_1)
session.add_all([course_1, course_2])
session.commit()
if __name__ == "__main__":
main()

SqlAlchemy outerjoin query problem filtering [duplicate]

This question already has answers here:
how left outer join in sqlalchemy?
(1 answer)
sqlalchemy filter children in query, but not parent
(2 answers)
Closed 2 years ago.
I'm using sqlalchemy, and I have problem with this specific query.
I have data_template, devices_data, and device. Each device have value for each data in data_template. Those values are stored in devices_data. I want to list data_template for one device with values that this device has. If there is no value for some data_template, show None.
It has something to do with outerjoin. Here is my model:
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class DataTemplate(Base):
__tablename__ = 'data_template'
id = Column(Integer, primary_key=True)
name = Column(String)
def __repr__(self):
return f"<DataTemplate(name={self.name})>"
class Device(Base):
__tablename__ = 'device'
id = Column(Integer, primary_key=True)
name = Column(String)
def __repr__(self):
return f"<Device(name={self.name})>"
class DeviceData(Base):
__tablename__ = 'device_data'
id = Column(Integer, primary_key=True)
value = Column(Integer, nullable=False)
data_name_id = Column(Integer, ForeignKey(DataTemplate.id), nullable=False)
device_id = Column(Integer, ForeignKey(Device.id), nullable=False)
data_template = relationship('DataTemplate', backref='device_data')
device = relationship('Device', backref='device_data')
def __repr__(self):
return f"<DeviceData(device={self.device.name}, data_template={self.data_template.name}, value={self.value})>"
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
Session.configure(bind=engine)
session = Session()
dev1 = Device(name='Receiver')
dev2 = Device(name='TV')
dat_temp1 = DataTemplate(name="height")
dat_temp2 = DataTemplate(name="width")
dat_temp3 = DataTemplate(name="length")
session.add_all([dev1, dev2, dat_temp1, dat_temp2, dat_temp3])
dd1 = DeviceData(value=100, data_template=dat_temp1, device=dev1)
dd2 = DeviceData(value=50, data_template=dat_temp2, device=dev1)
dd3 = DeviceData(value=200, data_template=dat_temp1, device=dev2)
dd4 = DeviceData(value=40, data_template=dat_temp2, device=dev2)
dd5 = DeviceData(value=30, data_template=dat_temp3, device=dev2)
session.add_all([dd1, dd2, dd3, dd4, dd5])
s = session.query(DataTemplate, DeviceData).outerjoin(DeviceData).filter(DeviceData.device==dev1)
for x in s:
print(x)
with this outerjoin I'm getting:
(<DataTemplate(name=height)>, <DeviceData(device=Receiver, data_template=height, value=100)>)
(<DataTemplate(name=width)>, <DeviceData(device=Receiver, data_template=width, value=50)>)
and is equal to:
SELECT "d"."id", "val"."id"
FROM "DataTemplate" "d"
LEFT JOIN "DeviceData" "val"
ON "d"."id" = "val"."data_name_id"
WHERE "val"."device_id" = 1
but I want to get:
(<DataTemplate(name=height)>, <DeviceData(device=Receiver, data_template=height, value=100)>)
(<DataTemplate(name=width)>, <DeviceData(device=Receiver, data_template=width, value=50)>)
(<DataTemplate(name=length)>, None)
and that query should be:
SELECT "d"."id", "val"."id"
FROM "DataTemplate" "d"
LEFT JOIN "DeviceData" "val"
ON "d"."id" = "val"."data_name_id" AND "val"."device_id" = 1
how do I write this specific query?

Join after outerjoin in SQLAlchemy

Suppose I have a one-to-many relationship, where the parents and children are grouped by some group_id.
Note: this example is a stripped down version of my code, which is actually a many-to-many relationship. There may be some errors unrelated to the question.
class Node(Base):
__tablename__ = 'node'
id = Column(GUID, default=uuid.uuid4, primary_key=True)
group_id = Column(GUID, nullable=False, primary_key=True)
parent_id = Column(GUID)
title = Column(Text, nullable=False)
class Leaf(Base):
__tablename__ = 'leaf'
id = Column(GUID, nullable=False, primary_key=True)
group_id = Column(GUID, nullable=False, primary_key=True)
parent_id = Column(GUID, nullable=False)
The group_id is used as a way to create new versions - so nodes and leaves with the same id can exist in multiple groups.
What I want to do is compare two groups, and find all the leaves whose parents have changed. I am trying to use an outer join to do the comparison, and then two joins to filter the parent nodes:
def find_changed_leaves(group_id_a, group_id_b, session):
NodeA = model.Node
NodeB = aliased(model.Node, name='node_b')
LeafA = model.Leaf
LeafB = aliased(model.Leaf, name='leaf_b')
query = (session.query(LeafA, LeafB)
.outerjoin(LeafB, LeafA.id == LeafB.id)
.join(NodeA, (LeafA.group_id == NodeA.group_id) &
(LeafA.parent_id == NodeA.id))
.join(NodeB, (LeafB.group_id == NodeB.group_id) &
(LeafB.parent_id == NodeB.id))
# Group membership
.filter(LeafA.group_id == group_id_a,
LeafB.group_id == group_id_b)
# Filter for modified parents
.filter(NodeA.title != NodeB.title)
)
return query.all()
This works, but it doesn't show leaves that are only in one of the groups (e.g. if a leaf was added to a node in the new group). How can I show all the leaves, returning None for a leaf that is missing from one of the groups?
Edit: I see there are perils mixing join with outer join. I tried naively changing it to .outerjoin(NodeA, ..., but it didn't help.
As mentioned in the comment, it is not entirely clear what needs to be achieved. Nonetheless, the code below should at least give you some directions.
First of all, I would not try to combine it all in one query (potentially using full joins and subqueries), but split it into 3 separate queries:
get LeafA, LeafB whose' parents have changed
get LaefA that do not have corresponding LeafB
get LaefB that do not have corresponding LeafA
Below is the code which should run as it is in both sqlite and postgresql. Note that I have added relationships and use them in the queries. But you could do the same with explicit join conditions as in your code snippet.
import uuid
from sqlalchemy import (
create_engine, Column, Integer, String, ForeignKey, Text, and_,
ForeignKeyConstraint, UniqueConstraint, exists
)
from sqlalchemy.orm import sessionmaker, relationship, eagerload, aliased
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.dialects.postgresql import UUID as GUID
_db_uri = 'sqlite:///:memory:'; GUID = String
# _db_uri = "postgresql://aaa:bbb#localhost/mytestdb"
engine = create_engine(_db_uri, echo=True)
Session = sessionmaker(bind=engine)
Base = declarative_base(engine)
newid = lambda: str(uuid.uuid4())
# define object model
class Node(Base):
__tablename__ = 'node'
id = Column(GUID, default=newid, primary_key=True)
group_id = Column(GUID, nullable=False, primary_key=True)
# parent_id = Column(GUID)
title = Column(Text, nullable=False)
class Leaf(Base):
__tablename__ = 'leaf'
id = Column(GUID, nullable=False, primary_key=True)
group_id = Column(GUID, nullable=False, primary_key=True)
parent_id = Column(GUID, nullable=False)
title = Column(Text, nullable=False)
# define relationships - easier test data creation and querying
parent = relationship(
Node,
primaryjoin=and_(Node.id == parent_id, Node.group_id == group_id),
backref="children",
)
__table_args__ = (
ForeignKeyConstraint(
['parent_id', 'group_id'], ['node.id', 'node.group_id']
),
)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
session = Session()
g1, g2, l1, l2, l3 = [newid() for _ in range(5)]
# Create test data
def _add_test_data():
n11 = Node(
title="node1", group_id=g1,
children=[
Leaf(id=l1, title="g1 only"),
Leaf(id=l3, title="both groups"),
]
)
n21 = Node(
title="node1 changed", group_id=g2,
children=[
Leaf(id=l2, title="g2 only"),
Leaf(id=l3, title="both groups"),
]
)
session.add_all([n11, n21])
session.commit()
def find_changed_leaves(group_id_a, group_id_b):
"""
Leaves which are in both versions, but a `title` for their parents is changed.
"""
NodeA = aliased(Node, name='node_a')
NodeB = aliased(Node, name='node_b')
LeafA = aliased(Leaf, name='leaf_a')
LeafB = aliased(Leaf, name='leaf_b')
query = (
session.query(LeafA, LeafB)
.filter(LeafA.group_id == group_id_a)
# #note: group membership for LeafB is part of join now
.join(LeafB, (LeafA.id == LeafB.id) & (LeafB.group_id == group_id_b))
.join(NodeA, LeafA.parent)
.join(NodeB, LeafB.parent)
# Filter for modified parents
.filter(NodeA.title != NodeB.title)
)
return query.all()
def find_orphaned_leaves(group_id_a, group_id_b):
"""
Leaves found in group A, but not in group B.
"""
LeafA = aliased(Leaf, name='leaf_a')
LeafB = aliased(Leaf, name='leaf_b')
query = (
session.query(LeafA)
.filter(~(
session.query(LeafB)
.filter(LeafA.id == LeafB.id)
.filter(group_id_b == LeafB.group_id)
.exists()
))
# Group membership
.filter(LeafA.group_id == group_id_a)
)
return query.all()
def find_deleted_leaves(group_id_a, group_id_b):
a_s = find_orphaned_leaves(group_id_a, group_id_b)
return tuple((a, None) for a in a_s)
def find_added_leaves(group_id_a, group_id_b):
b_s = find_orphaned_leaves(group_id_b, group_id_a)
return tuple((None, b) for b in b_s)
# add test data
_add_test_data()
# check the results
changed = find_changed_leaves(g1, g2)
assert 1 == len(changed)
le, ri = changed[0]
assert le.id == ri.id == l3
added = find_added_leaves(g1, g2)
assert 1 == len(added)
le, ri = added[0]
assert le is None
assert ri.id == l2
deleted = find_deleted_leaves(g1, g2)
assert 1 == len(deleted)
le, ri = deleted[0]
assert le.id == l1
assert ri is None