Search code examples
sqlalchemypython-social-auth

SQLAlchemy: How can I define a relationship as the union of two other relationships?


How can I implement a self-referential many-to-many relationship that is effectively the union of two other relationships?

The relationship should return all FacebookFriendship models that exist between a user and other users in the network. A user may have a FacebookFriendship that points to another existing user, but due to FB API outages, privacy controls, etc, the mirror FBFriendship might not exist for the existing user to this user.

# This class is necessary for python-social-auth
# A UserSocialAuth model only exists for users who are in the network
class UserSocialAuth(_AppSession, Base, SQLAlchemyUserMixin):
    """Social Auth association model"""
    __tablename__ = 'social_auth_usersocialauth'
    __table_args__ = (UniqueConstraint('provider', 'uid'),)
    id = Column(Integer, primary_key=True)
    provider = Column(String(32))
    uid = Column(String(UID_LENGTH))
    extra_data = Column(JSONType())
    user_id = Column(
        Integer, ForeignKey(User.id), nullable=False, index=True)
    user = relationship(
        User,
        backref=backref('social_auth', lazy='dynamic')
    )

This relationship finds FacebookFriendship models that point from this user to any existing user.

    facebook_friendships = relationship(
        FacebookFriendship,
        primaryjoin=and_(
            user_id == FacebookFriendship.user_id,
            provider == 'facebook'
        ),
        secondary=FacebookFriendship.__table__,
        secondaryjoin=uid == FacebookFriendship.fb_uid_friend,
        foreign_keys=[provider, user_id, uid],
        viewonly=True,
        uselist=True,
        lazy='dynamic',
    )

This relationship finds FacebookFriendship models that point to this user.

    other_facebook_friendships = relationship(
        FacebookFriendship,
        primaryjoin=and_(
            uid == FacebookFriendship.fb_uid_friend,
            provider == 'facebook'
        ),
        foreign_keys=[provider, uid],
        viewonly=True,
        uselist=True,
        lazy='dynamic',
    )

I was able to express the union query using the hybrid_property decorator, but this prevents usage of comparators like any() or from using association proxies, at least from what I can tell.

    # Can I rewrite this using relationship()?
    @hybrid_property
    def all_facebook_friendships(self):
        return self.facebook_friendships.union(
            self.other_facebook_friendships).correlate(
            FacebookFriendship)

# FBFriendship models are created for every friend that a user has,
# regardless of whether they're in the network or not.
class FacebookFriendship(Base):
    __tablename__ = u'user_fb_friend'

    user_id = Column(Integer, sa.ForeignKey(User.id), primary_key=True)

    user = relationship(
        User, backref=backref('facebook_friendships', lazy='dynamic'),
        primaryjoin=User.id == user_id)

    fb_uid_friend = Column(sa.String(length=255), primary_key=True)

In the end, I'd like to query this relationship like any other InstrumentedAttribute: UserSocialAuth.query.filter(UserSocialAuth.all_facebook_friendships.any()).all() and define an association_proxy on the User model:

User.all_facebook_friends = association_proxy('all_facebook_friendships', 'user')

Sorry for the length of this question, but I've trialed & errored to no avail for days now.

Related:


Solution

  • Using zzzeek's solution linked above, I created a self-referential M2M relationship by using a select statement as the "secondary" argument to relationship().

    friendship_union = select([
        FacebookFriendship.dater_id,
        cast(FacebookFriendship.fb_uid_friend, Integer()).label(
            'fb_uid_friend')
    ]).union(
        select([
            cast(FacebookFriendship.fb_uid_friend, Integer()),
            FacebookFriendship.dater_id]
        )
    ).alias()
    
    cls.all_fb_friendships = relationship(
        UserSocialAuth,
        secondary=friendship_union,
        primaryjoin=UserSocialAuth.user_id == friendship_union.c.dater_id,
        secondaryjoin=and_(
            UserSocialAuth.provider == 'facebook',
            cast(UserSocialAuth.uid, Integer() ) == friendship_union.c.fb_uid_friend,
        ),
        viewonly=True
    )