Update a value based on Join FlaskSqlALchemy - python

I have 2 tables in db.
class Vehicle(db.Model):
__tablename__ = "vehicles"
id = db.Column(db.Integer, primary_key=True, nullable=False)
num_plate = db.Column(db.String(7), nullable=False, unique=True)
type = db.Column(db.String, nullable=False)
suspicious = db.Column(db.Boolean, nullable=False, default=False)
def __repr__(self) -> str:
return f"Vehicle(number playe={self.num_plate}, type={self.type}, suspicious={self.suspicious})"
class Registered(db.Model):
"""
Registered User
"""
__tablename__ = "registered"
regid = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String, nullable=False)
cnic = db.Column(db.String, nullable=False)
contactno = db.Column(db.String, nullable=False)
gender = db.Column(db.String, nullable=False)
dor = db.Column(db.DateTime, nullable=False)
doe = db.Column(db.DateTime, nullable=False)
vehicle_id = db.Column(
db.Integer, db.ForeignKey("vehicles.id"), unique=True, nullable=False
)
Now I want to update a specific person's car num_plate. Let's say person with regid 3.
I can get his data.
reg_visitor = (
Registered.query.filter_by(regid=3)
.join(Vehicle)
.add_columns(
Registered.name,
Registered.cnic,
Registered.contactno,
Registered.gender,
Registered.dor,
Registered.doe,
Vehicle.num_plate,
Vehicle.suspicious,
)
).first()
It is of type
<class 'sqlalchemy.engine.row.Row'>
If I print it's attribute, it gives correct answer.
print(reg_visitor.num_plate)
output is
ANB 127
Now If I want to update this attribute, I can not.
reg_visitor.num_plate = "ABC1234"
ERROR:
reg_visitor.num_plate = "ABC1234"
File "/home/ahmad/Desktop/FYP/venv/lib/python3.7/site-packages/sqlalchemy/engine/row.py", line 219, in __setattr__
raise AttributeError("can't set attribute")
AttributeError: can't set attribute
My Flask-SQL Alchemy version is '2.5.1', and SQLAlchemy version is 1.4.29.
I do not want to mess up with my versions, is there any other way to update the attribute based on join?
It works well on single tables though.
vehicle = Vehicle.query.filter_by(num_plate="ANB 127").first()
vehicle.num_plate = "ABC123"
db.session.commit()
vehicle = Vehicle.query.filter_by(num_plate="ABC123").first() # works well
print(vehicle)

As you pointed out, the result of your query reg_visitor is a sqlalchemy.engine.row.Row, meaning it does not map to a single class.
When you try to update an attribute in it, SQLAlchemy doesn't know how it could update the tables based on the query.
You need to query the Vehicle directly (like again you've shown) to be able to update.

add this column to your Vehicle Class
registers = db.relationship('Registered', backref='vehicles',lazy='dynamic')
read about that in SqlAlchemy Relationship Docs
after that you can execute
reg_visitor=db.session.query(Vehicle).join(Registered,Registered.regid==3).one_or_none()
reg_visitor.num_plate=2331
db.session.commit()

Related

SqlAlchemy: No Foreign Key error, although the foreign key is defined

I developed a Framework based on SQLAlchemy.
The code to import a table in my mapper is the following:
for table in tables:
columns = []
relationships = {}
for column in inspect.getmembers(table,
lambda a: not (inspect.isroutine(a)) and (
type(a) == Column or type(a) == RelationshipProperty)):
if type(column[1]) == Column:
column[1].name = column[0]
columns.append(column[1])
else:
column[1].argument = list(filter(lambda t: t.__name__ == column[1].argument, tables))[0]
relationships[column[0]] = column[1]
sql_table = Table(table.__tablename__,
base.metadata,
*columns,
extend_existing=True)
mapper(table, sql_table, properties=relationships)
base.metadata.create_all()
Then I defined various tables:
Permission.py:
class Permission(TableModel):
__tablename__ = 'permission'
network_id = Column(String(255), ForeignKey("network.id", ondelete="CASCADE"), primary_key=True,
default=check_network_validity)
user_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
user = relationship("User", back_populates="permissions",
foreign_keys=user_id)
network = relationship("Network", back_populates="permissions",
foreign_keys=network_id)
type = Column(Enum(TypeEnum), primary_key=True)
validated_by = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=True, default=None)
end_validity = Column(DateTime, nullable=True)
created = Column(DateTime, default=datetime.utcnow())
last_updated = Column(DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow())
and Network.py:
class Network(TableModel):
__tablename__ = 'network'
id = Column(String(255), primary_key=True)
customer = Column(String(255))
url = Column(String(50))
rollout_api_key = Column(String(20))
rollout_password = Column(String(20))
is_sandbox = Column(Boolean, default=False)
is_new_stack = Column(Boolean)
date_deleted_ = Column(Date, nullable=True)
csm = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=True)
sol = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=True)
# host = Column(String(50), ForeignKey("keycloack.name"), nullable=True)
permissions = relationship("Permission", back_populates="network",
cascade="all, delete-orphan", single_parent=True)
requested_logs = relationship("RequestedLog", back_populates="network",
cascade="all, delete-orphan",
single_parent=True)
When I start the system, no problem: Tables are created with the right columns, and the right foreign keys. But as soon as I try to write in one table (Anyone, even another one than Permission or Network), I have this issue:
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\orm\interfaces.py", line 197, in init
self.do_init()
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\orm\relationships.py", line 2077, in do_init
self._setup_join_conditions()
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\orm\relationships.py", line 2141, in _setup_join_conditions
self._join_condition = jc = JoinCondition(
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\orm\relationships.py", line 2524, in init
self.determine_joins()
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\orm\relationships.py", line 2657, in determine_joins
util.raise(
File "D:\Github\sol-rollout-management-service\venv\lib\site-packages\sqlalchemy\util\compat.py", line 178, in raise
raise exception
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on relationship Permission.network - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
To add about it: The program worked without problems 2 weeks ago. In the meanwhile, I checked that Python updated from version 1.3.19. I tried to force back to 1.3.18, but it didn't change anything
Any idea? even top test something?
The issue was actually that the same module was loaded with different paths. So my foreign key didn't refer to the correct table.

sqlalchemy column_property in self-referential

I can't describe the column "invited_name" (column_property). I don't know how to do this correctly.
class Worker(declarative_base()):
__tablename__ = "staff_worker_info"
id = Column(Integer, primary_key=True)
first_name = Column(String(40), nullable=False)
last_name = Column(String(40), nullable=False)
invited_id = Column(Integer, ForeignKey('staff_worker_info.id'))
invited = relationship("Worker", uselist=False, remote_side=[id], join_depth=1)
# I don't know how to describe this column
invited_name = column_property(
select([Worker.first_name]). \
where(Worker.id == invited_id).\
label('invited_n'))
I understand why this doesn't work, but I don't know how to write it differently.
I should get such a SQL query.
SELECT staff_worker_info.id, staff_worker_info.first_name staff_worker_info.last_name, staff_worker_info.invited_id,
(SELECT worker_invited.first_name
FROM staff_worker_info AS worker_invited
WHERE staff_worker_info.invited_id = worker_invited.id) AS invited_n,
FROM staff_worker_info
Might be a bit late, but I recently faced a similar question. I think your problem is quite easy to solve with only the relationship. If you want you can also solve it by using a column_property.
First, using the relationship. If you make the invited relationship joined, then the actual query that is send to the database is a self-join. You can access the first name via that relationship (reference https://docs.sqlalchemy.org/en/14/orm/self_referential.html).
class Worker(declarative_base()):
__tablename__ = "staff_worker_info"
id = Column(Integer, primary_key=True)
first_name = Column(String(40), nullable=False)
last_name = Column(String(40), nullable=False)
invited_id = Column(Integer, ForeignKey('staff_worker_info.id'))
invited = relationship("Worker", uselist=False, remote_side=[id], join_depth=1, lazy='joined')
#property
def invited_name(self):
return self.invited.first_name
Then, if the query you want to do is more complex, and it requires you to create a column_property, you can also do it as follows (reference https://docs.sqlalchemy.org/en/14/orm/mapped_sql_expr.html):
from sqlalchemy import inspect
from sqlalchemy.orm import aliased
class Worker(declarative_base()):
__tablename__ = "staff_worker_info"
id = Column(Integer, primary_key=True)
first_name = Column(String(40), nullable=False)
last_name = Column(String(40), nullable=False)
invited_id = Column(Integer, ForeignKey('staff_worker_info.id'))
invited = relationship("Worker", uselist=False, remote_side=[id], join_depth=1)
# Requires alias
worker = aliased(Worker)
inspect(Worker).add_property(
"invited_name",
column_property(
select([worker.first_name]). \
where(worker.id == Worker.invited_id)
)
)
I found a method. But he did not like it.
invited_name = column_property(
select([text("invited_table.first_name")]).
where(text("invited_table.id = staff_worker_info.invited_id")).
select_from(text("staff_worker_info AS invited_table")).
label('invited_n'))

How to correctly define relationships in a Python SQLAlchemy class with multiple base classes?

I'm trying to recreate my database using SQLAlchemy and Flask.
I've created all the models, but now have some problems with the relationships between the models. When inserting or updating an object Flask returns the following error message:
File "C:\Users\Lenna\SchoolMi\api-server-v4\venv\lib\site-packages\sqlalchemy\ext\declarative\clsregistry.py", line 326, in __call__
x = eval(self.arg, globals(), self._dict)
File "<string>", line 1, in <module>
# ext/declarative/clsregistry.py
AttributeError: 'Table' object has no attribute 'id'
The error message references to the active_channel relationship in the profile class and indicates that the channel class has no id attribute. However I've already defined this attribute in the ObjectWithDefaultProps class. After inspection of the SQL file in a SQL browser, the id attribute is indeed present on the Channel entity.
My first assumption was a misconfiguration of the foreignkey, so I tried to change the foreignkey:
active_channel = db.relationship("Channel", foreign_keys="Channel.id")
instead of
active_channel = db.relationship("Channel", foreign_keys="channel.id")
Unfortunately, this did not work. The error changes to the following:
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on
relationship Profile.active_channel - there are no foreign keys linking these tables. Ensure that referencing
columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression. //
Werkzeug Debugger
I've looked further into the polymorphism aspects of SQLAlchemy and thought it might had something to do with the mapperargs but couldn't figure out the correct way of implementing this.
In my code I have the following classes:
My entities derive from multiple objects, that represent shared attributes or relationships.
class Profile(db.Model, ObjectWithDefaultProps, ObjectWithAvatar, ObjectWithNotificationProfile):
__tablename__ = "profile"
firebase_uid = db.Column(db.String, unique=True, nullable=False, primary_key=True)
username = db.Column(db.String, unique=True, nullable=False)
firstname = db.Column(db.String, nullable=False)
lastname = db.Column(db.String, nullable=False)
about = db.Column(db.String)
score = db.Column(db.Integer)
email = db.Column(db.String, nullable=False)
active_channel_id = db.Column(db.Integer, db.ForeignKey("channel.id"))
active_channel = db.relationship("Channel", foreign_keys="channel.id")
Channel.py
class Channel(db.Model, ObjectBase, ObjectWithAvatar, ObjectWithName, ProfileLinkedObject):
__tablename__ = "channel"
description = db.Column(db.String)
can_add_tags = db.Column(db.Boolean, default=False, nullable=False)
can_public_join = db.Column(db.Boolean, default=False, nullable=False)
from database.provider import db
from datetime import datetime
class ObjectWithDefaultProps:
deleted = db.Column(db.Boolean, nullable=False, default=False)
date_modified = db.Column(db.DateTime, default=datetime.utcnow)
date_added = db.Column(db.DateTime, onupdate=datetime.utcnow)
from database.provider import db
from database.extensions.object_with_color import ObjectWithColor
class ObjectWithAvatar(ObjectWithColor):
image_url = db.Column(db.String)
from database.provider import db
class ObjectWithColor:
color_index = db.Column(db.Integer, default=0)
from database.provider import db
from sqlalchemy.ext.declarative import declared_attr
class ObjectWithNotificationProfile:
auto_follow_questions = db.Column(db.Integer)
auto_follow_answers = db.Column(db.Integer)
auto_follow_comments = db.Column(db.Integer)
auto_follow_questions_on_comment = db.Column(db.Integer)
auto_follow_questions_on_answer = db.Column(db.Integer)
auto_follow_answers_on_comment = db.Column(db.Integer)
send_new_data_notification = db.Column(db.Boolean)
send_new_members_notification = db.Column(db.Boolean)
#declared_attr
def question_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def question_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def answer_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def answer_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def comment_event_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_event_preferences.id"))
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
#declared_attr
def question_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def question_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
#declared_attr
def answer_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def answer_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
#declared_attr
def comment_tagging_preferences_id(cls):
return db.Column(db.Integer, db.ForeignKey("custom_tagging_preferences.id"))
#declared_attr
def comment_tagging_preferences(cls):
return db.relationship("CustomTaggingPreferences", foreign_keys="custom_tagging_preferences.id")
For those facing the same or similar issues as described in my question:
I've managed to get my code working by replacing the string variables in the foreign_keys attribute with the column variables itself.
After testing, I found that using a string as foreign_keys attribute always trigged an error for me, but the alternative methods using primaryjoin and foreign_keys=cls.foreign_key worked.
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys="custom_event_preferences.id")
becomes:
#declared_attr
def comment_event_preferences(cls):
return db.relationship("CustomEventPreferences", foreign_keys=cls.comment_event_preferences)

When do relationships / backrefs become usable

I'm having a difficult time understanding how relationships / backrefs work.
I seem to be missing the point regarding how to make them 'live' so I keep getting errors like:
'NoneType' object has no attribute 'decid'.
These tables are 1 to 1 forming a heirachy.
I have an SQLite db and the following classes defined.
class Person(DECLARATIVE_BASE):
__tablename__ = 'person'
__table_args__ = ({'sqlite_autoincrement': True})
idperson = Column(INTEGER, autoincrement=True,
primary_key=True, nullable=False)
lastname = Column(VARCHAR(45), index=True, nullable=False)
firstname = Column(VARCHAR(45), index=True, nullable=False)
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Person(%(idperson)s)>" % self.__dict__
class Schoolmember(DECLARATIVE_BASE):
__tablename__ = 'schoolmember'
person_id = Column(INTEGER, ForeignKey("person.idperson"),
index=True, primary_key=True, nullable=False)
decid = Column(VARCHAR(45), unique=True, nullable=False)
type = Column(VARCHAR(20), nullable=False)
person = relationship("Person", foreign_keys=[person_id],
backref=backref("schoolmember", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Schoolmember(%(person_id)s)>" % self.__dict__
class Student(DECLARATIVE_BASE):
__tablename__ = 'student'
person_id = Column(INTEGER, ForeignKey("schoolmember.person_id"),
autoincrement=False, primary_key=True, nullable=False)
studentnum = Column(VARCHAR(30), unique=True, nullable=False)
year = Column(INTEGER, nullable=False)
graduated = Column(BOOLEAN, default=0, nullable=False)
schoolmember = relationship("Schoolmember", foreign_keys=[person_id],
backref=backref("student", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Student(%(person_id)s)>" % self.__dict__
I don't understand why here I can't access schoolmember from Student.
I was expecting declaritive to cascade up the relationships.
newstu = Student()
newstu.studentnum = '3456'
newstu.schoolmember.decid = 'fred.frog' # Error, 'NoneType' object
The following works, but only by stomping over the relationships defined in the class?
Do I need to do it this way?
s = Schoolmember(decid = 'fred.frog')
newstu = Student(schoolmember=s, studentnum='3456')
I don't 'get' what's is going on. I'm trying to understand the principals involved so I don't get bamboozled by the next problem
The reason your first example doesn't work is because when you initialize student there is no schoolmember associated with it. SQLAlchemy doesn't automatically generate this for you. If you wanted to, every time you create a Student object for it to automatically create a new schoolmember, you could do that inside of an __init__. In addition, if you wanted it to work you could do something like:
student = Student()
schoolmember = Schoolmember()
student.studentnum = 3456
student.schoolmember = schoolmember
student.schoolmember.decid = 'fred.frog'
An __init__ method could help also, if this is behavior you want every time.
def __init__(self, studentnum=None, year=None, graduated=None, schoolmember=None):
# If no schooolmember was provided, automatically generate one.
self.schoolmember = schoolmember or Schoolmember()
Hope this helps.

How can I use autoincrementing column value as default value for another not nullable column?

Lets say that we have UserModel with following columns:
class UserModel(declarative_base(bind=engine)):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname = Column(String, unique=True, nullable=False)
Is there a way to set id as default value for nickname without changing anything outside the model class?
I tried using default=id and server_default=id, but IntegrityError is always raised on commit.
Also, I know that there is no id until commit or flush is performed, but calling flush outside is not an option for me.
Thanks.
You should use hybrid_property for this. Here example:
class UserModel(Base):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname_str = Column(String, unique=True)
#hybrid_property
def nickname(self):
return self.nickname_str or str(self.id)
#nickname.expression
def nickname(self):
return case(
[(self.nickname_str != None, self.nickname_str)],
else_=cast(self.id, String)
).label('nickname')
#nickname.setter
def nickname(self, value):
self.nickname_str = value
Full example here.

Categories