Relationship between two tables, SQLAlchemy - python

I want to make a relationship between AuthorComments and Reply to his comments.
Here is my models.py:
class AuthorComments(Base):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
name = db.Column(db.String(50))
email = db.Column(db.String(50), unique=True)
comment = db.Column(db.Text)
live = db.Column(db.Boolean)
comments = db.relationship('Reply', backref='reply', lazy='joined')
def __init__(self,author, name, email, comment, live=True):
self.author_id = author.id
self.name = name
self.email = email
self.comment = comment
self.live = live
class Reply(Base):
id = db.Column(db.Integer, primary_key=True)
reply_id = db.Column(db.Integer, db.ForeignKey('author.id'))
name = db.Column(db.String(50))
email = db.Column(db.String(50), unique=True)
comment = db.Column(db.Text)
live = db.Column(db.Boolean)
def __init__(self,author, name, email, comment, live=True):
self.reply_id = author.id
self.name = name
self.email = email
self.comment = comment
self.live = live
Why am I getting this error:
sqlalchemy.exc.InvalidRequestError
InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Original exception was: Could not determine join condition between parent/child tables on relationship AuthorComments.comments - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.

Your trouble is that SQLAlchemy doesn't know, for a given row of the child table (Reply), which row of the parent table (AuthorComments) to select! You need to define a foreign-key column in Reply that references a column of its parent AuthorComments.
Here is the documentation on defining one-to-many relationships in SQLAlchemy.
Something like this:
class AuthorComments(Base):
__tablename__ = 'author_comment'
...
class Reply(Base):
...
author_comment_id = db.Column(db.Integer, db.ForeignKey('author_comment.id'))
...
author_comment = db.relationship(
'AuthorComments',
backref='replies',
lazy='joined'
)
will result in each reply acquiring a relationship to an author_comment such that some_reply.author_comment_id == some_author_comment.id, or None if no such equality exists.
The backref allows each author_comment to, reciprocally, have a relationship to a collection of replies called replies, satisfying the above condition.

Related

SQLAlchemy in Flask - How to auto update the same column in another table

I am trying to make sure that given two tables with many to many relationship, they are not only linked by the id but by another column too: the "name" column in user is referring to the "author" column in the post table.
I attempted various solutions including:
user_post = db.Table('user_post',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
)
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(95), nullable=False)
email = db.Column(db.String(180), unique=True , nullable=False)
posts = db.relationship('Post', secondary=user_post, backref='user')
posts1 = db.relationship('Post', backref='post1', lazy='dynamic',
primaryjoin="user.name == post.author")
def __repr__(self):
return f"User: {self.name}, {self.email}"
class Post(db.Model):
__tablename__ = 'post'
id = db.Column(db.Integer, primary_key=True)
author = db.Column('author', db.String(95), db.ForeignKey('user.name'))
# author = db.Column('author', db.String(95))
title = db.Column(db.String(300))
content = db.Column(db.Text(700))
date_posted = db.Column(db.DateTime, default=datetime.utcnow)
slug = db.Column(db.String(200))
users = db.relationship('User', secondary=user_post, backref='post')
def __repr__(self):
return f"Title: {self.title}"
to test that if I change the name of a user, the author's name should change as well, I wrote as below:
user3.name = "Someone New"
db.session.commit()
print(post3.author)
and this is when I got the error:
SQLAlchemy in Flask - AttributeError: 'Table' object has no attribute 'author'
I tried to use the code by ensuring that the author is updated in different ways but I wanted the author to be updated automatically through ORM.
I checked the documentation and it is mentioned that there are exceptions to the referenced columns almost always define the primary key, but among the cases I have seen, it was always referencing primary keys.
Any suggestions would be much appreciated.
Try this:
author = db.Column(db.String(95), db.ForeignKey(User.name))

Displaying joined fields in Flask-Admin ModelView

I have a Flask app using Flask-SQLAlchemy with some simple relational data mapping, e.g. between Orders and OrderItems belonging to those orders.
In my Flask-Admin backend I would like to show some of the order attributes in the list of OrderItems — as opposed to having the entire order object. E.g. make the "Order.email" listed (can be read-only) in the OrderItems' rows.
I've looked into the inline_models attribute of the ModelView, but this seems to be more feared towards actually editing the relational object — I just want to display (and sort/search by) some value of the "parent".
Is there a way to achieve this?
You can easily include fields via a foreign key relationship by including them in column_list value - documentation. Consider the two simplified models, note the company back reference in the Address model:
class Company(db.Model):
__tablename__ = 'companies'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(255), nullable=False, unique=True, index=True)
website = db.Column(db.Unicode(255), nullable=True)
notes = db.Column(db.UnicodeText())
#hybrid_property
def address_count(self):
return len(self.addresses)
#address_count.expression
def address_count(cls):
return select([func.count(Address.id)]).where(Address.company_id == cls.id).label("address_count")
def __str__(self):
return self.name
class Address(db.Model):
__tablename__ = 'addresses'
id = db.Column(db.Integer, primary_key=True)
address1 = db.Column(db.Unicode(255), nullable=False)
town = db.Column(db.Unicode(255), index=True, nullable=False)
county = db.Column(db.Unicode(255))
country = db.Column(db.Unicode(255))
post_code = db.Column(db.Unicode(10))
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), index=True)
company = db.relationship(Company, backref=db.backref('addresses', uselist=True, lazy='select', cascade='delete-orphan,all'))
def __str__(self):
return ', '.join(filter(None, [self.address1, self.town, self.county, self.post_code, self.country]))
In the Address view you can access a "parent" company using dotted notation. For example:
class AddressView(ModelAdmin):
column_list = (
'company.name',
'company.website',
'address1',
'address2'
)

Flask SQL-Alchemy creating hierarchy table many foreign keys to same table

Have got a roles table:
from datetime import datetime
# from models.user_role import UserRoleModel
from models.role_hierarchy import RoleHierarchyModel
class RoleModel(db.Model):
__tablename__ = "security_role"
role_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255))
description = db.Column(db.Text)
created_timestamp = db.Column(db.DateTime, default=datetime.now())
updated_timestamp = db.Column(db.DateTime, default=datetime.now())
role = db.relationship('UserRoleModel', back_populates='role')
parent_role = db.relationship('RoleHierarchyModel',
back_populates='parent_role',
primaryjoin="RoleHierarchyModel.parent_role_id == RoleModel.role_id")
child_role = db.relationship('RoleHierarchyModel',
back_populates='child_role',
primaryjoin="RoleHierarchyModel.child_role_id == RoleModel.role_id")
and a Roles Hierarchy table
class RoleHierarchyModel(db.Model):
__tablename__ = "security_role_hierarchy"
role_h_id = db.Column(db.Integer, primary_key=True)
parent_role_id = db.Column(db.Integer, db.ForeignKey('security_role.role_id'))
child_role_id = db.Column(db.Integer, db.ForeignKey('security_role.role_id'))
created_at = db.Column(db.DateTime, default=datetime.now())
updated_at = db.Column(db.DateTime, default=datetime.now())
parent_role = db.relationship('RoleModel', back_populates='parent_role'
, foreign_keys='RoleHierarchyModel.parent_role_id')
child_role = db.relationship('RoleModel', back_populates='child_role'
, foreign_keys='RoleHierarchyModel.child_role_id')
Not sure if I have set this up correctly nor how to get it into the database correctly once I have done this. What I want to create is a role called super user that has other roles associated with it
I try:
base_role = RoleModel(title='Read')
other_base_role = RoleModel(title='Write')
base_role.save_to_db()
other_base_role.save_to_db()
super_user = RoleModel(title='Super)
hierarchy1 = RoleHierarchyModel()
hierarchy2 = RoleHierarchyModel()
hierarchy1.child_role(base_role)
hierarchy2.child_role(other_base_role)
super_user.parent_role.append(hierarchy1)
super_user.parent_role.append(hierarchy2)
but I keep getting the following error:
sqlalchemy.exc.InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original
exception was: Attempting to flush an item of type <class 'models.role.RoleModel'> as a member of collection "RoleModel.child_role". Expected an object of type <class 'models.role_hierarchy.RoleHierarchyModel'> or
a polymorphic subclass of this type. (Background on this error at: http://sqlalche.me/e/7s2a)
what am I doing wrong?
In general, there should be no need to have two classes, which is kind of where your problem started.
To make this a proper tree, have a look at this answer. You only need to reference the parent_id through a column, and let SQLAlchemy get you the children through a relationship object.
Suppose you store the rights a role has in a list somehow. Then, if you want to see if someone has the right to do something, or if you want to return a list of all rights a person has (directly and indirectly):
class Role:
_rights = ['canChangePassword', 'canEditPosts', 'canDeletePosts'...]
def has_right(self, rightName):
if rightName in self._rights
return True
for child in self.children:
if child.has_right(rightName):
return True
return False
def get_rights(self):
all_rights = self._rights
for child in self.children:
all_rights += self.children.get_rights()
# there can be duplicates, cast to a set to remove them
return list(set(all_rights))
I can also see how rights are granted based on the description or title, but in that case, just iterate the same way and construct a list of role titles from itself and all children. Use recursion to your advantage here.
Found the answer hidden in the SQLAlchemy documentation: https://docs.sqlalchemy.org/en/13/orm/join_conditions.html#handling-multiple-join-paths
https://docs.sqlalchemy.org/en/13/orm/join_conditions.html#handling-multiple-join-paths
Roles table now looks like:
class RoleModel(db.Model):
__tablename__ = "security_role"
role_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255))
description = db.Column(db.Text)
created_timestamp = db.Column(db.DateTime, default=datetime.now())
updated_timestamp = db.Column(db.DateTime, default=datetime.now())
role = db.relationship('UserRoleModel', back_populates='role')
child_roles = db.relationship('RoleModel', secondary='security_role_hierarchy'
, primaryjoin='RoleModel.role_id==RoleHierarchyModel.parent_role_id'
, secondaryjoin="RoleModel.role_id==RoleHierarchyModel.child_role_id",
backref="parent_roles")
and the hierarchy table is now:
class RoleHierarchyModel(db.Model):
__tablename__ = "security_role_hierarchy"
role_h_id = db.Column(db.Integer, primary_key=True)
parent_role_id = db.Column(db.Integer, db.ForeignKey('security_role.role_id'))
child_role_id = db.Column(db.Integer, db.ForeignKey('security_role.role_id'))
created_at = db.Column(db.DateTime, default=datetime.now())
updated_at = db.Column(db.DateTime, default=datetime.now())

How to properly define SQLAlchemy backrefs so that they can be reflected?

Let's assume we have the following code in some Models.py file:
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True, nullable=False)
Name = db.Column(db.String(255), nullable=False)
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True,nullable=False)
OwnerID = db.Column(db.Integer, nullable=False)
TenantID = db.Column(db.Integer, nullable=False)
__table_args__ = (
db.ForeignKeyConstraint(
['OwnerID'],
['Persons.ID'],
),
db.ForeignKeyConstraint(
['TenantID'],
['Persons.ID'],
),
)
OwnerBackref = db.relationship('Person', backref='OwnerBackref', lazy=True, foreign_keys=[OwnerID])
TenantBackref = db.relationship('Person', backref='TenantBackref', lazy=True, foreign_keys=[TenantID])
And we want to reflect these models using the automap base, so we have this code in another module called Database.py:
Base = automap_base()
engine = create_engine(DB_CONNECTION, pool_size=10, max_overflow=20)
db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
Base.prepare(engine, reflect=True)
Person = Base.classes.Persons
House = Base.classes.Houses
Now, when I import House in some other module I want to be able to do this:
h = db_session.query(House).first()
print(h.OwnerBackref.Name)
print(h.TenantBackref.Name)
But instead I get an error saying that those 2 backrefs do not exist and instead a field called 'persons' gets added to my House object but the problem here is that it links only 1 (either the Tenant either the Owner). By this I mean that if I do this:
print(h.persons.Name)
It will only print the Name either for the respective Tenant either for the Owner leaving me with no way of accessing the informations for the other one. (Note here that the names that I set to the backrefs are nowhere to be found)
So, my question is how can I use the backrefs I created to access my desired informations ? Am I doing something wrong here ?
The error in your code is that you are using foreign_keys= to define the relationship between the tables but you are passing the local key name not the foreign key name to the function. For your code you cannot use foreign_keys= to define the relationship within the House model as there is only one possible foreign key Person.ID but two possible local keys House.OwnerID and House.TenantID. The primaryjoin= argument should be used instead to specify this.
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True)
Name = db.Column(db.String(255), nullable=False)
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True)
OwnerID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
TenantID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
Owner = db.relationship('Person', backref='HousesOwned', primaryjoin='House.OwnerID == Person.ID')
Tenant = db.relationship('Person', backref='HousesOccupied', primaryjoin='House.TenantID == Person.ID')
If you placed the relationship statements in in the Person model rather than the House model then you could use either foreign_keys= or primaryjoin= to define the relationship. The following code will result in exactly the same relationships as in the previous code.
class Person(db.Model):
__tablename__ = 'Persons'
ID = db.Column(db.Integer, primary_key=True)
Name = db.Column(db.String(255), nullable=False)
HousesOwned = db.relationship('House', backref='Owner', foreign_keys='[House.OwnerID]')
HousesOccupied = db.relationship('House', backref='Tenant', foreign_keys='[House.TenantID]')
class House(db.Model):
__tablename__ = 'Houses'
ID = db.Column(db.Integer,primary_key=True)
OwnerID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)
TenantID = db.Column(db.Integer, db.ForeignKey('Persons.ID'), nullable=False)

Flask SQLAlchemy Foreign Key Relationships

I'm having a lot of trouble getting my head around foreign keys and relationships in SQLAlchemy. I have two tables in my database. The first one is Request and the second one is Agent. Each Request contains one Agent and each Agent has one Request.
class Request(db.Model):
__tablename__ = 'request'
reference = db.Column(db.String(10), primary_key=True)
applicationdate = db.Column(db.DateTime)
agent = db.ForeignKey('request.agent'),
class Agent(db.Model):
__tablename__ = 'agent'
id = db.relationship('Agent', backref='request', \
lazy='select')
name = db.Column(db.String(80))
company = db.Column(db.String(80))
address = db.Column(db.String(180))
When I am running db.create_all() I get the following error
Could not initialize target column for ForeignKey 'request.agent' on table 'applicant': table 'request' has no column named 'agent'
Have a look at the SqlAlchemy documentation on OneToOne relationships. First you need to supply a Primary Key for each model. Then you need to define one Foreign Key which refers to the Primary Key of the other model. Now you can define a relationship with a backref that allows direct access to the related model.
class Request(db.Model):
__tablename__ = 'request'
id = db.Column(db.Integer, primary_key=True)
applicationdate = db.Column(db.DateTime)
class Agent(db.Model):
__tablename__ = 'agent'
id = db.Column(db.Integer, primary_key=True)
request_id = db.Column(db.Integer, db.ForeignKey('request.id'))
request = db.relationship("Request", backref=backref("request", uselist=False))
name = db.Column(db.String(80))
company = db.Column(db.String(80))
address = db.Column(db.String(180))
Now you can access your models like this:
request = Request.query.first()
print(request.agent.name)
agent = Agent.query.first()
print(agent.request.applicationdate)

Categories