Many-to-many, self-referential, non-symmetrical relationship (twitter model) via Association Object in SqlAlchemy

后端 未结 1 994
面向向阳花
面向向阳花 2021-01-06 06:52

How would one best implement a many-to-many, self-referential, non symmetrical relationship (think Twitter) in SqlAlchemy? I want to use an association object (let\'s call t

相关标签:
1条回答
  • 2021-01-06 07:18

    This is already almost answered in here. Here this is improved by having the advantages of a many-to-many made with a bare link table.

    I'm not good in SQL and neither in SqlAlchemy but since I had this problem in mind for some longer time, I tried to find a solution that has both advantages: an association object with additional attributes and a direct association like with a bare link table (which doesn't provide an object on its own for the association). Stimulated by additional suggestions of the op the following seems quiet nice to me:

    #!/usr/bin/env python3
    # coding: utf-8
    
    import sqlalchemy as sqAl
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker, relationship, backref
    from sqlalchemy.ext.associationproxy import association_proxy
    
    engine = sqAl.create_engine('sqlite:///m2m-w-a2.sqlite') #, echo=True)
    metadata = sqAl.schema.MetaData(bind=engine)
    
    Base = declarative_base(metadata)
    
    class UserProfile(Base):
      __tablename__ = 'user'
    
      id            = sqAl.Column(sqAl.Integer, primary_key=True)
      full_name     = sqAl.Column(sqAl.Unicode(80))
      gender        = sqAl.Column(sqAl.Enum('M','F','D', name='gender'), default='D', nullable=False)
      description   = sqAl.Column(sqAl.Unicode(280))
      following     = association_proxy('followeds', 'followee')
      followed_by   = association_proxy('followers', 'follower')
    
      def follow(self, user, **kwargs):
        Follow(follower=self, followee=user, **kwargs)
    
      def __repr__(self):
        return 'UserProfile({})'.format(self.full_name)
    
    class Follow(Base):
      __tablename__ = 'follow'
    
      followee_id   = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
      follower_id   = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
      status        = sqAl.Column(sqAl.Enum('A','B', name=u'status'), default=u'A')
      created       = sqAl.Column(sqAl.DateTime, default=sqAl.func.now())
      followee      = relationship(UserProfile, foreign_keys=followee_id, backref='followers')
      follower      = relationship(UserProfile, foreign_keys=follower_id, backref='followeds')
    
      def __init__(self, followee=None, follower=None, **kwargs):
        """necessary for creation by append()ing to the association proxy 'following'"""
        self.followee = followee
        self.follower = follower
        for kw,arg in kwargs.items():
          setattr(self, kw, arg)
    
    Base.metadata.create_all(engine, checkfirst=True)
    session = sessionmaker(bind=engine)()
    
    def create_sample_data(sess):
      import random
      usernames, fstates, genders = ['User {}'.format(n) for n in range(4)], ('A', 'B'), ('M','F','D')
      profs = []
      for u in usernames:
        user = UserProfile(full_name=u, gender=random.choice(genders))
        profs.append(user)
        sess.add(user)
    
      for u in [profs[0], profs[3]]:
        for fu in profs:
          if u != fu:
            u.follow(fu, status=random.choice(fstates))
    
      profs[1].following.append(profs[3]) # doesn't work with followed_by
    
      sess.commit()
    
    # uncomment the next line and run script once to create some sample data
    # create_sample_data(session)
    
    profs = session.query(UserProfile).all()
    
    print(       '{} follows {}: {}'.format(profs[0], profs[3], profs[3] in profs[0].following))
    print('{} is followed by {}: {}'.format(profs[0], profs[1], profs[1] in profs[0].followed_by))
    
    for p in profs:
      print("User: {0}, following: {1}".format(
        p.full_name,  ", ".join([f.full_name for f in p.following])))
      for f in p.followeds:
        print(" " * 25 + "{0} follow.status: '{1}'"
              .format(f.followee.full_name, f.status))
      print("            followed_by: {1}".format(
        p.full_name,  ", ".join([f.full_name for f in p.followed_by])))
      for f in p.followers:
        print(" " * 25 + "{0} follow.status: '{1}'"
              .format(f.follower.full_name, f.status))
    

    It seems indispensible to define two relations for the Association Object. The association_proxy method seems to be not ideally tailored for self-referential relations. The argument oder of the Follow constructor doesn't seem logical to me but works only this way (this is explained here).

    In the book Rick Copeland - Essential Sqlalchemy on page 117 you find the following note regarding the secondary-parameter to relationship():

    Note that, if you are using SQLAlchemy’s ability to do M:N relationships, the join table should only be used to join the two tables together, not to store auxilliary properties. If you need to use the intermediate join table to store addi- tional properties of the relation, you should use two 1:N relations instead.

    Sorry for that this is a little bit verbose but I like code that I can copy, paste, and execute directly. This works with Python 3.4 and SqlAlchemy 0.9 but likely also with other versions.

    0 讨论(0)
提交回复
热议问题