Reputation: 23
I am attempting to model a friendship using SQLAlchemy ORM. The relationship that I am trying to model is symmetric. Similar to Facebook, if user a is to add user b, user b must approve that friendship request. My current model is as follows.
class User(db.Model):
__tablename__ = 'User'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(35), unique=False)
username = db.Column(db.String(25), index=True, unique=True)
password = db.Column(db.String(35), unique=False)
email = db.Column(db.String(35), unique=True)
phone_number = db.Column(db.String(22))
# define relationships
requester = db.relationship('Relationship', foreign_keys='Relationship.requesting_user', backref='requester')
receiver = db.relationship('Relationship', foreign_keys='Relationship.receiving_user', backref='received')
def __repr__(self):
return '<User %r>' % (self.username)
class Relationship(db.Model):
__tablename__ = 'Relationship'
id = db.Column(db.Integer, primary_key=True)
requesting_user = db.Column(db.Integer, db.ForeignKey('User.id'))
receiving_user = db.Column(db.Integer, db.ForeignKey("User.id"))
status = db.Column(db.Integer)
__table_args__ = (db.UniqueConstraint('receiving_user', 'requesting_user', name='_receiving_user_uc'), )
The model works, however, I don't think that it is properly modeled. Is it even required that I use a status? I'm assuming it can be modeled so that each friend relationship gets its own entry. Currently, a user can initiate a friend request with another user. When the other user approves the request, the status changes to accepted. I have looked a little into association tables but am not too sure how they would play into a model like this. Any advice on my current model and how it can be improved would be greatly appreciated.
Upvotes: 2
Views: 1245
Reputation: 4366
Among other things, you may want to learn about association proxies. An association proxy tells SQLAlchemy that you have a many-to-many relationship mediated by an intermediate table which may contain additional data. In your case, each User
can send multiple requests and also receive multiple requests and Relationship
is the mediating table which contains the status
column as additional data.
Here is a variant of your code which stays relatively close to what you wrote:
from sqlalchemy.ext.associationproxy import association_proxy
class User(db.Model):
__tablename__ = 'User'
# The above is not necessary. If omitted, __tablename__ will be
# automatically inferred to be 'user', which is fine.
# (It is necessary if you have a __table_args__, though.)
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(35), unique=False)
# and so forth
requested_rels = db.relationship(
'Relationship',
foreign_keys='Relationship.requesting_user_id',
backref='requesting_user'
)
received_rels = db.relationship(
'Relationship',
foreign_keys='Relationship.receiving_user_id',
backref='receiving_user'
)
aspiring_friends = association_proxy('received_rels', 'requesting_user')
desired_friends = association_proxy('requested_rels', 'receiving_user')
def __repr__(self):
# and so forth
class Relationship(db.Model):
# __tablename__ removed, becomes 'relationship'
# __table_args__ removed, see below
requesting_user_id = db.Column(db.Integer, db.ForeignKey('User.id'), primary_key=True)
receiving_user_id = db.Column(db.Integer, db.ForeignKey('User.id'), primary_key=True)
# Marking both columns above as primary_key creates a compound primary
# key, which at the same time saves you the effort of defining the
# UNIQUE constraint in __table_args__
status = db.Column(db.Integer)
# Implicit one-to-many relations: requesting_user, receiving_user.
# Normally it would be more convenient to define those relations on
# this side, but since you have two outgoing relationships with the
# same table (User), you chose wisely to define them there.
(Note how I ordered the lines slightly differently and how I used the _id
suffix for foreign key columns while reserving the same name without the suffix for the corresponding db.relationship
s. I would suggest that you adopt this style, too.)
Now you have a clean way to access incoming and outgoing friendship requests as well as the corresponding users directly from your User
model. However, this is still less than ideal because you need to write the following code in order to get all confirmed friends of a user:
def get_friends(user):
requested_friends = (
db.session.query(Relationship.receiving_user)
.filter(Relationship.requesting_user == user)
.filter(Relationship.status == CONFIRMED)
)
received_friends = (
db.session.query(Relationship.requesting_user)
.filter(Relationship.receiving_user == user)
.filter(Relationship.status == CONFIRMED)
)
return requested_friends.union(received_friends).all()
(I did not test this; you might need to also join
with User
in both queries in order for the union
to work.)
To make things worse, the model name Relationship
as well as the names of several members within the models don't seem to convey very well what they actually mean.
You can improve matters by removing Relationship.status
and renaming Relationship
to FriendshipRequest
. Then, add a second User
-to-User
association model called Friendship
and add a corresponding second set of db.Relationship
s with backref
s and association_proxy
s to User
. When somebody sends a friendship request, you file a record to FriendshipRequest
. If the request is accepted, you remove the record and replace it with a new record in Friendship
. This way, instead of using a status code, the status of a friendship is encoded by the table in which you store a pair of users. The Friendship
model may look like this:
class Friendship(db.Model):
user1_id = db.Column(db.Integer, db.ForeignKey('User.id'), primary_key=True)
user2_id = db.Column(db.Integer, db.ForeignKey('User.id'), primary_key=True)
# Implicit one-to-many relations: user1, user2
# (defined as backrefs in User.)
(Corresponding db.relationship
s and association_proxy
s in User
are left as an exercise to the reader.)
This approach saves you half of the filtering operations when you need the confirmed friends of a user. Still, you need to make a union
of two queries because your user can be either user1
or user2
in each instance of Friendship
. This is inherently difficult because we are dealing with a reflexive symmetric relationship. I think it is possible to invent still more elegant ways to do it, but I think that would be complicated enough to warrant a new question here on Stack Overflow.
Upvotes: 5