2012-02-02 14 views
16

Sto cercando di implementare una relazione molti-a-molti autoreferenziale usando dichiarative su SQLAlchemy.Come posso ottenere una relazione molti-a-molti autoreferenziale sull'ORM di SQLAlchemy che fa riferimento allo stesso attributo?

La relazione rappresenta l'amicizia tra due utenti. Ho scoperto online (sia nella documentazione che su Google) come creare una relazione m2m autoreferenziale in cui, in qualche modo, i ruoli sono differenziati. Ciò significa che in queste relazioni m2m UserA è, ad esempio, il capo di UserB, quindi lo elenca sotto un attributo 'subordinati' o cosa hai. Allo stesso modo UtenteB elenca UtenteA sotto 'superiori'.

Questo costituisce un problema, perché siamo in grado di dichiarare un backref allo stesso tavolo in questo modo:

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

Quindi c'è, ovviamente, l'attributo 'superiori' non è esplicito all'interno della classe.

In ogni caso, ecco il mio problema: cosa succede se voglio tornare allo stesso attributo dove sto chiamando il backref? Come questo:

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=?????? 
         ) 

Questo ha senso, perché se una amicizia con B i ruoli di relazione sono gli stessi, e se invoco gli amici di B dovrei ottenere una lista con A in esso. Questo è il codice problematico in piena:

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 
          ) 

Scusate se questo è troppo testo, voglio solo essere il più esplicito che posso con questo. Non riesco a trovare alcun materiale di riferimento a questo sul web.

risposta

15

Ecco l'approccio UNION che ho accennato alla mailing list all'inizio di oggi.

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

Questo sembra essere un po 'soggetto a errori: si può accidentalmente accodare a 'all_friends' e non sarà possibile ottenere alcun preavviso. Eventuali suggerimenti? – Halst

+0

Anche questo consente di duplicare le amicizie con ID scambiati (come '1, 2' e' 2, 1'). Puoi mettere un vincolo che un ID sia maggiore di un altro, ma poi devi tenere traccia di quali utenti possono essere aggiunti a quale attributo 'amici' degli utenti. – Halst

+1

viewonly = True non ha alcuna influenza sul comportamento della raccolta in Python. Se sei veramente preoccupato per gli allegati a questa raccolta, puoi utilizzare collection_cls e applicare un elenco o un tipo di set con metodi di mutazione sovrascritti per lanciare NotImplementedError o simili. – zzzeek

4

avevo bisogno di risolvere questo stesso problema e scompigliato su un bel po 'con l'auto referenziale molti-a-molti in cui mi è stato anche sottoclasse della classe User con una classe Friend e in esecuzione in sqlalchemy.orm.exc.FlushError. Alla fine, invece di creare una relazione molti-a-molti autoreferenziale, ho creato una relazione uno-a-molti autoreferenziale usando una tabella di join (o una tabella secondaria).

Se ci pensate, con oggetti autoreferenziali, gli uni-a-molti IS molti-a-molti. Ha risolto il problema del backref nella domanda originale.

Ho anche un istinto working example se si desidera vederlo in azione. Inoltre, sembra che gli schemi di formato github contengano i taccuini ipython adesso. Neat.

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 
Problemi correlati