2010-03-26 28 views
8

lo tanto, tengo tres tablas:¿Cómo puedo unir tres tablas con SQLalchemy y mantener todas las columnas en una de las tablas?

Los defenitions clase:

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) 

NOTA: Sé user.username debe ser único, tienen que arreglar eso, y no estoy seguro de por qué sqlalchemy crea algunos nombres de fila con las comillas dobles.

Y estoy tratando de encontrar una manera de recuperar todos los canales, así como una indicación de en qué canales tiene un usuario en particular (identificado por user.sessionId junto con user.id) tiene una suscripción.

Por ejemplo, supongamos que tenemos cuatro canales: canal1, canal2, canal3, canal4; un usuario: usuario1; quién tiene una suscripción en channel1 y channel4. La consulta de usuario1 volvería algo como:

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

Este es un resultado mejor de los casos, pero ya no tengo absolutamente ninguna pista sobre cómo llevar a cabo la columna suscrito, he estado en su lugar tratando de hacer que los usuarios particulares id en las filas donde el usuario tiene una suscripción y donde falta una suscripción, simplemente déjelo en blanco.

El motor de base de datos que estoy utilizando junto con SQLalchemy atm. es sqlite3

Me he estado rascando la cabeza por esto durante dos días, no tengo problemas para unir los tres por medio de la tabla de suscripción pero luego todos los canales donde el usuario no tiene una suscripción omitido

Espero haber logrado describir mi problema lo suficiente, gracias de antemano.

EDITAR: logró resolver esto de una manera un poco torpe que implica una sub-consulta:

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

Sin embargo, voy a seguir buscando una solución más elegante, por lo que las respuestas siguen siendo muy muy bienvenido

+0

¿Puede agregar las definiciones de su modelo/tabla? Dependiendo de si utiliza el asignador declarativo, el correlacionador normal o las tablas simples, la sintaxis difiere un poco. – Wolph

+0

Pregunta hecha y actualizada, ese sería el mapeador declarativo a menos que esté equivocado. – jimka

+0

Ese es el mapeador de declaracion de hecho :) – Wolph

Respuesta

13

Opción 1:

Subscription es sólo un objeto de relación de muchos a muchos, y yo sugeriría que lo representan como tales en lugar de como una clase separada. Consulte Configuring Many-to-Many Relationships documentación de SQLAlchemy/declarative.

modelo que con el código de prueba se convierte en:

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

que produce siguiente resultado:

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

Tenga en cuenta el uso de eagerload, que emitirá sólo el 1 instrucción SELECT en lugar de 1 por cada User cuando se solicite channels.

Opción 2:

Pero si desea mantener a modelar y acaba de crear una consulta de SA que le daría las columnas como se le pregunte, siguiente consulta debe hacer el trabajo:

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 

La salida es absolutamente la misma que en la opción-1 anterior.

+0

Sí, creo que he leído en algún lugar del manual SQLAlchemy sobre cómo modelar las relaciones de Muchos a Muchos de esa manera (Opción-1), tal vez debería considerar más. Sin embargo, no estoy seguro de cómo me gusta la lógica de bucle adicional al final en la Opción-1. Opción-2 presenta 'case()' que es nuevo para mí, es casi lo que quiero, con un pequeño detalle, no coincide con la columna sessionId en la tabla de usuario. – jimka

+2

@jimka. 'sessionId' falta - vamos hombre, este es un pequeño detalle que puede manejar, ¿verdad? De todos modos, acaba de agregar el filtro faltante en 'sessionId' para completar. – van

+0

Offcourse, pero era parte de la pregunta, así que creo que debería ser parte de la respuesta también. – jimka

1

Para hacer esto un poco fácil he agregado relaciones a su modelo, de esa manera puede hacer suscripciones de usuario para obtener todas las suscripciones.

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

Muy elegante, sin embargo, cualquiera() parece que no funciona, me da el error: AttributeError: el objeto 'InstrumentedList' no tiene atributo 'any'. – jimka

+0

Sí, aparentemente solo funciona en filtros. así que ... 'session.query (Channel) .filter (Channel.subscriptions.any())' funcionaría. No demasiado sorprendente en realidad, debe seleccionarse como una columna adicional para que funcione. Algo como 'session.query (Channel.id, Channel.title, Channel.subscriptions.any(). Label ('suscrito'))' debería funcionar. – Wolph

0

No realizar consultas por parte del usuario. Consulta del canal

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

De esta forma se obtienen todos los canales, no solo los que están asociados con el usuario en cuestión.

+0

AttributeError: el objeto 'InstrumentedList' no tiene ningún atributo 'usuario' al intentar 'channel.subscriptions.user' – jimka

+0

Sí. Ha pasado un tiempo desde que usé sqlalchemy; la sintaxis puede no ser del todo correcta. Corregir la sintaxis restante como un ejercicio para el lector. (sugerencia: será más fácil si usa relaciones como WoLpH sugiere) – jcdyer

Cuestiones relacionadas