views:

343

answers:

3

So, I have three tables:

The class 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)

NOTE: I know user.username should be unique, need to fix that, and I'm not sure why SQLalchemy creates some row names with the double-quotes.

And I'm trying to come up with a way to retrieve all of the channels, as well as an indication on what channels one particular user (identified by user.sessionId together with user.id) has a subscription on.

For example, say we have four channels: channel1, channel2, channel3, channel4; a user: user1; who has a subscription on channel1 and channel4. The query for user1 would return something like:

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

This is a best-case result, but since I have absolutely no clue as how to accomplish the subscribed column, I've been instead trying to get the particular users id in the rows where the user has a subscription and where a subscription is missing, just leave it blank.

The database engine that I'm using together with SQLalchemy atm. is sqlite3

I've been scratching my head over this for two days now, I've no problem joining together all three by way of the subscription table but then all of the channels where the user does not have a subscription gets omitted.

I hope I've managed to describe my problem sufficiently, thanks in advance.

EDIT: Managed to solve this in a slightly clunky way involving a sub-query:

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

However, I'll be continuing to search for a more elegant solution, so answers are still very much welcomed.

+1  A: 

To make this a little easyer I've added relationships to your model, that way you can just do user.subscriptions to get all the subscriptions.

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
WoLpH
Very elegant, however, any() dosn't seem to work, gives me the error: AttributeError: 'InstrumentedList' object has no attribute 'any'.
jimka
Yes, apparently it only works in filters. so... `session.query(Channel).filter(Channel.subscriptions.any())` would work.Not too surprising actually, it should be selected as an extra column for it to work. Something like `session.query(Channel.id, Channel.title, Channel.subscriptions.any().label('subscribed'))` should work.
WoLpH
A: 

Don't query from the user. Query from the Channel.

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

That way you get all the channels, not just the ones that are associated with the user in question.

jcdyer
AttributeError: 'InstrumentedList' object has no attribute 'user' when trying 'channel.subscriptions.user'
jimka
Yup. It's been a little while since I used sqlalchemy; the syntax might not be quite right. Correct syntax left as an exercise for the reader. (hint: it will be easier if you use relationships like WoLpH suggests)
jcdyer
+1  A: 

Option-1:

Subscription is just a many-to-many relation object, and I would suggest that you model it as such rather then as a separate class. See Configuring Many-to-Many Relationships documentation of SQLAlchemy/declarative.

You model with the test code becomes:

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

which produces following output:

(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)

Please note the use of eagerload, which will issue only 1 SELECT statement instead of 1 for each User when channels are asked for.

Option-2:

But if you want to keep you model and just create an SA query that would give you the columns as you ask, following query should do the job:

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

The output is absolutely the same as in the option-1 above.

van
Yes, I think I've read somewhere in the SQLAlchemy manual about modeling Many-to-Many relationships that way (Option-1), perhaps I should look into it more. I'm not sure how I like the extra loop logic at the end in Option-1 though.Option-2 introduces 'case()' which is new to me, it's almost what I want, with one small detail, it dosn't match against the sessionId column in the User-table.
jimka
@jimka. `sessionId` missing - come on man, this is one small detail that you can handle, right? Anyways, just added the missing filter on `sessionId` for completeness.
van
@van, Offcourse, but it was part of the question so I believe it should be part of the answer aswell.
jimka
Fair, even though the main part of the question (as I understood it) was to get you `unstuck`. In any case, glad I could help.
van
@van, Yeah, I guess you're right, but I can be a bit anal about these sorts of things.
jimka