Relación de muchos a muchos, autorreferencial, no simétrica (modelo de twitter) a través del objeto de asociación en SqlAlchemy

¿Cuál sería la mejor manera de implementar una relación de muchos a muchos, auto-referencial, no simétrica (piense en Twitter) en SqlAlchemy? Quiero usar un objeto de asociación (llamemos a esta clase “Seguir”) para poder tener atributos adicionales asociados con la relación.

He visto muchos ejemplos que utilizan tablas de asociación, pero ninguno como el que he descrito anteriormente. Esto es lo que tengo hasta ahora:

class UserProfile(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) full_name = Column(Unicode(80)) gender = Column(Enum(u'M',u'F','D', name='gender'), nullable=False) description = Column(Unicode(280)) followed = relationship(Follow, backref="followers") class Follow(Base): __tablename__ = 'follow' follower_id = Column(Integer, ForeignKey('user.id'), primary_key=True) followee_id = Column(Integer, ForeignKey('user.id'), primary_key=True) status = Column(Enum(u'A',u'B', name=u'status'), default=u'A') created = Column(DateTime, default=func.now()) followee = relationship(UserProfile, backref="follower") 

¿Pensamientos?

Esto ya está casi respondido aquí . Aquí, esto se mejora al tener las ventajas de un muchos a muchos hecho con una tabla de enlaces simple.

No soy bueno en SQL y tampoco en SqlAlchemy, pero como tuve este problema en mente durante más tiempo, traté de encontrar una solución que tuviera ambas ventajas: un objeto de asociación con atributos adicionales y una asociación directa como con un enlace simple tabla (que no proporciona un objeto por sí solo para la asociación). Estimulado por sugerencias adicionales de la operación, lo siguiente me parece tranquilo:

 #!/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)) 

Parece indispensable definir dos relaciones para el objeto de asociación . El método association_proxy no parece estar adaptado idealmente para las relaciones autorreferenciales. El argumento oder del constructor Follow no me parece lógico, pero funciona solo de esta manera (esto se explica aquí ).

En el libro Rick Copeland – Essential Sqlalchemy en la página 117, encontrará la siguiente nota sobre el parámetro secondary a la relationship() :

Tenga en cuenta que, si está utilizando la capacidad de SQLAlchemy para realizar relaciones M: N, la tabla de unión solo debe usarse para unir las dos tablas, no para almacenar propiedades auxiliares. Si necesita usar la tabla de unión intermedia para almacenar propiedades adicionales de la relación, debe usar dos relaciones 1: N en su lugar.

Lo siento, esto es un poco detallado, pero me gusta el código que puedo copiar, pegar y ejecutar directamente. Esto funciona con Python 3.4 y SqlAlchemy 0.9 pero probablemente también con otras versiones.