2012-02-02 12 views
16

Estoy tratando de implementar una relación autorreferencial de muchos a muchos usando declarativo en SQLAlchemy.¿Cómo puedo lograr una relación autorreferencial de varios a varios en el SQLAlchemy ORM haciendo referencia remota al mismo atributo?

La relación representa la amistad entre dos usuarios. En línea encontré (tanto en la documentación como en Google) cómo hacer una relación auto-referencial m2m donde de alguna manera los roles se diferencian. Esto significa que en estas relaciones m2m UserA es, por ejemplo, el jefe de UserB, por lo que lo enumera bajo un atributo 'subordinados' o lo que sea. De la misma manera, UserB lista UserA en 'superiores'.

Esto constituye ningún problema, ya que se puede declarar una backref a la misma mesa de esta manera:

subordinates = relationship('User', backref='superiors') 

Así que, por supuesto, el atributo 'superiores' no es explícita dentro de la clase.

De todos modos, aquí está mi problema: ¿y si quiero volver al mismo atributo donde estoy llamando al backref? De esta manera:

friends = relationship('User', 
         secondary=friendship, #this is the table that breaks the m2m 
         primaryjoin=id==friendship.c.friend_a_id, 
         secondaryjoin=id==friendship.c.friend_b_id 
         backref=?????? 
         ) 

Esto tiene sentido, ya que si se hace amigo de un B los roles de relación son los mismos, y si invoco amigos B que debe recibir una lista con A en ella. Este es el código problemático en su totalidad:

friendship = Table(
    'friendships', Base.metadata, 
    Column('friend_a_id', Integer, ForeignKey('users.id'), primary_key=True), 
    Column('friend_b_id', Integer, ForeignKey('users.id'), primary_key=True) 
) 

class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True) 

    friends = relationship('User', 
          secondary=friendship, 
          primaryjoin=id==friendship.c.friend_a_id, 
          secondaryjoin=id==friendship.c.friend_b_id, 
          #HELP NEEDED HERE 
          ) 

Lo siento si esto es demasiado texto, sólo quiero ser tan explícito como pueda con esto. Parece que no puedo encontrar ningún material de referencia para esto en la web.

Respuesta

15

Aquí está el enfoque de UNION que insinué en la lista de correo de hoy.

from sqlalchemy import Integer, Table, Column, ForeignKey, \ 
    create_engine, String, select 
from sqlalchemy.orm import Session, relationship 
from sqlalchemy.ext.declarative import declarative_base 

Base= declarative_base() 

friendship = Table(
    'friendships', Base.metadata, 
    Column('friend_a_id', Integer, ForeignKey('users.id'), 
             primary_key=True), 
    Column('friend_b_id', Integer, ForeignKey('users.id'), 
             primary_key=True) 
) 


class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True) 
    name = Column(String) 

    # this relationship is used for persistence 
    friends = relationship("User", secondary=friendship, 
          primaryjoin=id==friendship.c.friend_a_id, 
          secondaryjoin=id==friendship.c.friend_b_id, 
    ) 

    def __repr__(self): 
     return "User(%r)" % self.name 

# this relationship is viewonly and selects across the union of all 
# friends 
friendship_union = select([ 
         friendship.c.friend_a_id, 
         friendship.c.friend_b_id 
         ]).union(
          select([ 
           friendship.c.friend_b_id, 
           friendship.c.friend_a_id] 
          ) 
        ).alias() 
User.all_friends = relationship('User', 
         secondary=friendship_union, 
         primaryjoin=User.id==friendship_union.c.friend_a_id, 
         secondaryjoin=User.id==friendship_union.c.friend_b_id, 
         viewonly=True) 

e = create_engine("sqlite://",echo=True) 
Base.metadata.create_all(e) 
s = Session(e) 

u1, u2, u3, u4, u5 = User(name='u1'), User(name='u2'), \ 
        User(name='u3'), User(name='u4'), User(name='u5') 

u1.friends = [u2, u3] 
u4.friends = [u2, u5] 
u3.friends.append(u5) 
s.add_all([u1, u2, u3, u4, u5]) 
s.commit() 

print u2.all_friends 
print u5.all_friends 
+0

Esto parece ser un poco propenso a errores: puede anexar accidentalmente a 'all_friends' y no recibirá ninguna advertencia. ¿Alguna sugerencia? – Halst

+0

También esto permite tener amistades duplicadas con identificadores intercambiados (como '1, 2' y' 2, 1'). Puede poner una restricción de que un ID es mayor que otro, pero luego necesita hacer un seguimiento de qué usuarios pueden agregarse al atributo 'friends' de los usuarios. – Halst

+1

viewonly = True no tiene relación con el comportamiento de la colección en Python. Si realmente le preocupa la incorporación a esta colección, puede usar collection_cls y aplicar una lista o un tipo de conjunto que tenga los métodos de mutación anulados para arrojar un NotImplementedError o similar. – zzzeek

4

que necesitaba para resolver este mismo problema y en mal estado acerca bastante con uno mismo de referencia muchos-a-muchos relación en la que yo también estaba de subclases de la clase User con una clase Friend y en funcionamiento en sqlalchemy.orm.exc.FlushError. Al final, en lugar de crear una relación autorreferencial de muchos a muchos, creé una relación autorreferencial de uno a muchos usando una tabla de unión (o tabla secundaria).

Si lo piensas bien, con objetos autorreferenciales, uno a muchos ES de muchos a muchos. Resolvió el problema del backref en la pregunta original.

También tengo una gisted working example si quieres verla en acción. También parece que github formatea los gists que contienen los portátiles ipython ahora. Ordenado.

friendship = Table(
    'friendships', Base.metadata, 
    Column('user_id', Integer, ForeignKey('users.id'), index=True), 
    Column('friend_id', Integer, ForeignKey('users.id')), 
    UniqueConstraint('user_id', 'friend_id', name='unique_friendships')) 


class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True) 
    name = Column(String(255)) 

    friends = relationship('User', 
          secondary=friendship, 
          primaryjoin=id==friendship.c.user_id, 
          secondaryjoin=id==friendship.c.friend_id) 

    def befriend(self, friend): 
     if friend not in self.friends: 
      self.friends.append(friend) 
      friend.friends.append(self) 

    def unfriend(self, friend): 
     if friend in self.friends: 
      self.friends.remove(friend) 
      friend.friends.remove(self) 

    def __repr__(self): 
     return '<User(name=|%s|)>' % self.name 
Cuestiones relacionadas