SQLAlchemy User Many to Many Relationship

Question:

I am working on a user structure where a user can have parents and children that consist of user objects. I have been trying to get the following to work in multiple different ways in SQLAlchemy and Flask.

Here is an example of how I want to structure this:

UserTable
id | name
---+-----
1  | Kim
2  | Tammy
3  | John
4  | Casey
5  | Kyle

UserRelationship
id | parent_user_id | child_user_id
---+----------------+---------------
1  | 1              | 2
2  | 1              | 3
3  | 4              | 2

Where Kim is the parent to Tammy and John. Casey is the parent of Tammy. Tammy is the child of Kim and Casey. John is the child of Kim. Kyle has no children nor parents.


My error reads:

sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition 
between parent/child tables on relationship User.parents - there are multiple 
foreign key paths linking the tables via secondary table 'user_relationship'. 
Specify the 'foreign_keys' argument, providing a list of those columns which 
should be counted as containing a foreign key reference from the secondary 
table to each of the parent and child tables.

My model.py file looks like:

user_relationship = db.Table(
    'user_relationship',
    db.Model.metadata,
    db.Column('child_user_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('parent_user_id', db.Integer, db.ForeignKey('user.id'))
)

class User(db.Model):
    __tablename__ = 'user'

    id = db.Column(db.Integer, primary_key=True, unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True)
    pwdhash = db.Column(db.String(54))

    parents = relationship('User',
        secondary=user_relationship,
        backref=db.backref('children'),
        cascade="all,delete")

This might not be the best way to handle many to many hierarchical user relationships in Flask or SQLAlchemy. Any insight on how to build this out would be great.

Thank you

Asked By: Adnan Dossaji

||

Answers:

db.Table is mainly used when there is a Many to Many relation between two different entities. Here both parent and child are Users.

In your case, below code would be fine:

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True, unique=True,     nullable=False)
    email = db.Column(db.String(120), unique=True)
    pwdhash = db.Column(db.String(54))


class Parent(db.Model):
    __tablename__ = 'parents'
    child_id = db.Column(db.Integer, db.Foreignkey('users.id'))
    parent_id = db.Column(db.Integer, db.Foreignkey('users.id'))
    child = db.relationship('User', foreign_keys=[child_id], backref = 'parents')

flask_sqlalchemy is written on top of SQLAlchemy, and there is a nice elaboration of this issue here (Handling Multiple Join Paths) in SQLAlchemy’s website.

Answered By: Shahryar Saljoughi

Use primaryjoin and secondaryjoin args.

Recently I faced similar issue, although I see documentation and accepted answer s recommend to user Parent model instead of creating a relationship table, it didn’t make sense for me to create a separate model for just storing a relationship since my all models inherit from BaseModel and I don’t want this relationship to get BaseModel‘s additional features. Mainly because my BaseModel has id by default and BaseTable does not contain id

So, I prefer creating a separate Sqlalchemy Table for relationship instead of Model.

To accomplish it you may use primaryjoin and secondaryjoin arguments as following. I have many-to-one relationship of investors with relationship_manager. To make it many-to-many you may remove unique=True from investor_id in relation table and remove uselist=False from relationship_manager in User model.

# user.py 
from database.relationships.relationship_managers import relationship_managers

class User(BaseModel):
    id = Column(Integer, primary_key=True)
    relationship_manager = relationship(
        "User",
        secondary=relationship_managers,
        uselist=False,
        primaryjoin=("relationship_managers.c.investor_id == User.id"),
        secondaryjoin=("relationship_managers.c.rel_manager_id == User.id"),
        backref="investors"
    )
# relationship_managers.py
relationship_managers = BaseTable(
    "relationship_managers",
    Column("investor_id", Integer, ForeignKey('user.id', ondelete="CASCADE"), unique=True),
    Column("rel_manager_id", Integer, ForeignKey('user.id', ondelete="CASCADE")),
)

Here, primaryjoin performs the first join with investor_id and gets the record User record, but this is not sufficient for us. So, we need secondaryjoin to specifically get the relationship_manager’s id.

Feel free to comment and suggest if any doubts. Hopefully this helps anyone who has Models and Tables both in their Project using Sqlalchemy. 🙂

Answered By: Karishma Sukhwani
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.