2010-03-26 5 views
8

Also, ich habe drei Tabellen:Wie verknüpfe ich drei Tabellen mit SQLalchemy und behält alle Spalten in einer der Tabellen?

Die Klasse defenitions:

engine = create_engine('sqlite://test.db', echo=False) 
SQLSession = sessionmaker(bind=engine) 
Base = declarative_base() 

class Channel(Base): 
    __tablename__ = 'channel' 

    id = Column(Integer, primary_key = True) 
    title = Column(String) 
    description = Column(String) 
    link = Column(String) 
    pubDate = Column(DateTime) 

class User(Base): 
    __tablename__ = 'user' 

    id = Column(Integer, primary_key = True) 
    username = Column(String) 
    password = Column(String) 
    sessionId = Column(String) 

class Subscription(Base): 
    __tablename__ = 'subscription' 

    userId = Column(Integer, ForeignKey('user.id'), primary_key=True) 
    channelId = Column(Integer, ForeignKey('channel.id'), primary_key=True) 

HINWEIS: Ich weiß user.username eindeutig sein sollte, müssen das beheben, und ich bin nicht sicher, warum sqlalchemy erstellt einige Zeilennamen mit den Anführungszeichen.

Und ich versuche, einen Weg zu finden, alle Kanäle abzurufen, sowie eine Angabe darüber, welche Kanäle ein bestimmter Benutzer (identifiziert durch user.sessionId zusammen mit user.id) hat ein Abonnement auf.

Zum Beispiel sagen wir haben vier Kanäle: channel1, channel2, channel3, channel4; ein Benutzer: user1; Wer hat ein Abonnement auf channel1 und channel4? Die Abfrage für user1 zurückkehren würde so etwas wie:

channel.id | channel.title | subscribed 
--------------------------------------- 
1   channel1  True 
2   channel2  False 
3   channel3  False 
4   channel4  True 

Dies ist ein Best-Case-Ergebnis, aber da ich absolut keine Ahnung, wie haben, wie das gezeichnete Säule zu erreichen, habe ich versucht worden, anstatt die bestimmte Benutzer zu erhalten ID in den Zeilen, in denen der Benutzer eine Subskription hat und wo eine Subskription fehlt, lassen Sie sie einfach leer.

Die Datenbank-Engine, die ich zusammen mit SQLalchemy atm verwenden. ist sqlite3

Ich habe seit zwei Tagen meinen Kopf über dies gekratzt, ich habe kein Problem, alle drei über die Abonnementtabelle zusammenzufügen, aber dann alle Kanäle, wo der Benutzer kein Abonnement hat, bekommt weggelassen.

Ich hoffe, ich habe es geschafft, mein Problem ausreichend zu beschreiben, danke im Voraus.

EDIT: Managed dies in einer etwas klobig Weise zu lösen, um eine Unter Abfrage mit:

# What a messy SQL query! 
stmt = query(Subscription).filter_by(userId = uid()).join((User, Subscription.userId == User.id)).filter_by(sessionId = id()).subquery() 
subs = aliased(Subscription, stmt) 
results = query(Channel.id, Channel.title, subs.userId).outerjoin((subs, subs.channelId == Channel.id)) 

Allerdings werde ich für eine elegantere Lösung suchen fortsetzen, so antwortet immer noch sehr beliebt sind sehr willkommen.

+0

Können Sie Ihre Modell-/Tabellendefinitionen hinzufügen? Abhängig davon, ob Sie den Declarational Mapper, den Normal Mapper oder die Plain Tabellen verwenden, unterscheidet sich die Syntax ein wenig. – Wolph

+0

Fertig und aktualisierte Frage, das wäre der Deklarationsmapper, wenn ich mich nicht irre. – jimka

+0

Das ist die Deklaration Mapper in der Tat :) – Wolph

Antwort

13

Option-1:

Subscription ist nur eine von vielen zu vielen Beziehungsobjekt, und ich würde vorschlagen, dass man es als solches Modell eher dann als separate Klasse. Siehe Configuring Many-to-Many Relationships Dokumentation von SQLAlchemy/declarative.

Sie Modell mit dem Testcode wird:

from sqlalchemy import create_engine, Column, Integer, DateTime, String, ForeignKey, Table 
from sqlalchemy.orm import relation, scoped_session, sessionmaker, eagerload 
from sqlalchemy.ext.declarative import declarative_base 

engine = create_engine('sqlite:///:memory:', echo=True) 
session = scoped_session(sessionmaker(bind=engine, autoflush=True)) 
Base = declarative_base() 

t_subscription = Table('subscription', Base.metadata, 
    Column('userId', Integer, ForeignKey('user.id')), 
    Column('channelId', Integer, ForeignKey('channel.id')), 
) 

class Channel(Base): 
    __tablename__ = 'channel' 

    id = Column(Integer, primary_key = True) 
    title = Column(String) 
    description = Column(String) 
    link = Column(String) 
    pubDate = Column(DateTime) 

class User(Base): 
    __tablename__ = 'user' 

    id = Column(Integer, primary_key = True) 
    username = Column(String) 
    password = Column(String) 
    sessionId = Column(String) 

    channels = relation("Channel", secondary=t_subscription) 

# NOTE: no need for this class 
# class Subscription(Base): 
    # ... 

Base.metadata.create_all(engine) 


# ###################### 
# Add test data 
c1 = Channel() 
c1.title = 'channel-1' 
c2 = Channel() 
c2.title = 'channel-2' 
c3 = Channel() 
c3.title = 'channel-3' 
c4 = Channel() 
c4.title = 'channel-4' 
session.add(c1) 
session.add(c2) 
session.add(c3) 
session.add(c4) 
u1 = User() 
u1.username ='user1' 
session.add(u1) 
u1.channels.append(c1) 
u1.channels.append(c3) 
u2 = User() 
u2.username ='user2' 
session.add(u2) 
u2.channels.append(c2) 
session.commit() 


# ###################### 
# clean the session and test the code 
session.expunge_all() 

# retrieve all (I assume those are not that many) 
channels = session.query(Channel).all() 

# get subscription info for the user 
#q = session.query(User) 
# use eagerload(...) so that all 'subscription' table data is loaded with the user itself, and not as a separate query 
q = session.query(User).options(eagerload(User.channels)) 
for u in q.all(): 
    for c in channels: 
     print (c.id, c.title, (c in u.channels)) 

die folgende Ausgabe erzeugt:

(1, u'channel-1', True) 
(2, u'channel-2', False) 
(3, u'channel-3', True) 
(4, u'channel-4', False) 
(1, u'channel-1', False) 
(2, u'channel-2', True) 
(3, u'channel-3', False) 
(4, u'channel-4', False) 

Bitte beachten Sie die Verwendung von eagerload, die nur 1 SELECT-Anweisung anstelle von 1 für jede Ausgabe wird User wenn channels gefragt sind.

Option-2:

Aber wenn Sie eine SA-Abfrage behalten möchten erstellen Sie modellieren und nur, dass Sie die Spalten geben würde, wie Sie fragen, Abfrage nach sollte die Arbeit machen:

from sqlalchemy import and_ 
from sqlalchemy.sql.expression import case 
#... 
q = (session.query(#User.username, 
        Channel.id, Channel.title, 
        case([(Subscription.channelId == None, False)], else_=True) 
       ).outerjoin((Subscription, 
           and_(Subscription.userId==User.id, 
            Subscription.channelId==Channel.id)) 
          ) 
    ) 
# optionally filter by user 
q = q.filter(User.id == uid()) # assuming uid() is the function that provides user.id 
q = q.filter(User.sessionId == id()) # assuming uid() is the function that provides user.sessionId 
res = q.all() 
for r in res: 
    print r 

Die Ausgabe ist absolut die gleiche wie in der Option-1 oben.

+0

Ja, ich glaube, ich habe irgendwo im SQLAlchemy-Handbuch über das Modellieren von Many-to-Many-Beziehungen gelesen (Option-1), vielleicht sollte ich nachsehen es mehr. Ich bin mir nicht sicher, wie ich die zusätzliche Schleifenlogik am Ende in Option-1 mag. Option-2 führt 'case()' ein, was für mich neu ist, es ist fast das was ich will, mit einem kleinen Detail, das nicht mit der sessionId Spalte in der User-Tabelle übereinstimmt. – jimka

+2

@jimka. 'sessionId' fehlt - komm schon, das ist ein kleines Detail, mit dem du umgehen kannst, oder? Wie auch immer, gerade hinzugefügt den fehlenden Filter auf 'sessionId' der Vollständigkeit halber. – van

+0

Natürlich, aber es war Teil der Frage, also glaube ich, dass es auch Teil der Antwort sein sollte. – jimka

1

Um dies ein wenig leichter zu machen, habe ich Beziehungen zu Ihrem Modell hinzugefügt, auf diese Weise können Sie einfach usersubscriptions tun, um alle Abonnements zu erhalten.

engine = create_engine('sqlite://test.db', echo=False) 
SQLSession = sessionmaker(bind=engine) 
Base = declarative_base() 

class Channel(Base): 
    __tablename__ = 'channel' 

    id = Column(Integer, primary_key = True) 
    title = Column(String) 
    description = Column(String) 
    link = Column(String) 
    pubDate = Column(DateTime) 

class User(Base): 
    __tablename__ = 'user' 

    id = Column(Integer, primary_key = True) 
    username = Column(String) 
    password = Column(String) 
    sessionId = Column(String) 

class Subscription(Base): 
    __tablename__ = 'subscription' 

    userId = Column(Integer, ForeignKey('user.id'), primary_key=True) 
    user = relationship(User, primaryjoin=userId == User.id, backref='subscriptions') 
    channelId = Column(Integer, ForeignKey('channel.id'), primary_key=True) 
    channel = relationship(channel, primaryjoin=channelId == channel.id, backref='subscriptions') 

results = session.query(
    Channel.id, 
    Channel.title, 
    Channel.subscriptions.any().label('subscribed'), 
) 

for channel in results: 
    print channel.id, channel.title, channel.subscribed 
+0

Sehr elegant, jedoch, any() scheint nicht zu funktionieren, gibt mir den Fehler: AttributeError: 'InstrumentedList' Objekt hat kein Attribut 'any'. – jimka

+0

Ja, anscheinend funktioniert es nur in Filtern. Also ... 'session.query (Channel) .filter (Channel.subscriptions.any())' würde funktionieren. Eigentlich nicht zu überraschend, sollte es als eine zusätzliche Spalte ausgewählt werden, damit es funktioniert. Etwas wie 'session.query (Channel.id, Channel.title, Channel.subscriptions.any(). Label ('subscribed')) sollte funktionieren. – Wolph

0

Fragen Sie nicht vom Benutzer ab. Abfrage vom Kanal.

user = query(User).filter_by(id=1).one() 
for channel in query(Channel).all(): 
    print channel.id, channel.title, user in channel.subscriptions.user 

Auf diese Weise erhalten Sie alle Kanäle, nicht nur diejenigen, die mit dem betreffenden Benutzer verbunden sind.

+0

AttributeError: 'InstrumentedList' -Objekt hat kein Attribut 'user' beim Versuch 'channelsubscriptions.user' – jimka

+0

Yup. Es ist schon eine Weile her, seit ich Sqlalchemy benutzt habe; Die Syntax ist möglicherweise nicht ganz richtig. Korrekte Syntax als Übung für den Leser. (Hinweis: Es wird einfacher, wenn Sie Beziehungen wie WoLpH verwenden) – jcdyer

Verwandte Themen