2012-06-05 8 views
7

Deseo crear un atributo mapeado de un objeto que se rellena desde otra tabla.SQLAlchemy propiedad declarativa de join (atributo único, no todo el objeto)

Usando el ejemplo de la documentación SQLAlchemy, deseo hacer un campo nombre_de_usuario existe en la clase de dirección de tal manera que puede ser fácilmente consultada y de fácil acceso (sin un segundo de ida y vuelta a la base de datos)

Por ejemplo, me gustaría ser capaz de consultar y filtrar por user_nameAddress.query.filter(Address.user_name == 'wcdolphin').first() y también el acceso al atributo user_name de todos los objetos de dirección, sin penalización en el rendimiento, y tienen adecuadamente persisten escribe como se esperaría de un atributo en el __tablename__

class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True) 
    name = Column(String(50)) 
    addresses = relation("Address", backref="user") 

class Address(Base): 
    __tablename__ = 'addresses' 

    id = Column(Integer, primary_key=True) 
    email = Column(String(50)) 
    user_name = Column(Integer, ForeignKey('users.name'))#This line is wrong 

¿Cómo hago esto?

Encontré la documentación relativamente difícil de entender, ya que no parecía ajustarse a la mayoría de los ejemplos, especialmente los ejemplos de Flask-SQLAlchemy.

+0

Qué es exactamente equivocado con la documentación? El [tutorial sqlalchemy] (http://docs.sqlalchemy.org/en/rel_0_7/orm/tutorial.html) cubre todo para tareas comunes. Y conceptos avanzados se encuentran en el resto de la documentación. – schlamar

Respuesta

6

Puede hacer esto con un join en el objeto de consulta, sin necesidad de especificar este atributo directamente. Así que su modelo se vería así:

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

Base = declarative_base() 
engine = create_engine('sqlite:///') 
Session = sessionmaker(bind=engine) 

class User(Base): 
    __tablename__ = 'users' 
    id = Column(Integer, primary_key=True) 
    name = Column(String(50)) 
    addresses = relation("Address", backref="user") 

class Address(Base): 
    __tablename__ = 'addresses' 
    id = Column(Integer, primary_key=True) 
    email = Column(String(50)) 
    user_id = Column(Integer, ForeignKey("users.id")) 


Base.metadata.create_all(engine) 

Una consulta después de direcciones con el filtrado el nombre de usuario se parece a:

>>> session = Session() 
>>> session.add(Address(user=User(name='test'))) 
>>> session.query(Address).join(User).filter(User.name == 'test').first() 
<__main__.Address object at 0x02DB3730> 

Editar: Como se puede acceder directamente al usuario de un objeto de dirección, hay no hay necesidad de referenciar directamente un atributo a la clase Address:

>>> a = session.query(Address).join(User).filter(User.name == 'test').first() 
>>> a.user.name 
'test' 
+0

Aunque es algo correcto, esta no es mi intención, ya que no puedo acceder a la propiedad user_name de 'Address' como describí en mi pregunta. Al escribir esto en PHP personalizado/manuscrito escrito, es un caso simple de generar correctamente el SQL para seleccionar User.name del User .__ table__ y almacenar el atributo, SQLAlchemy debe ser compatible con este escenario. –

+0

@wcdolphin Se ha actualizado la respuesta. – schlamar

+0

Brillante, la clave aquí es la magia que ocurre cuando creó el respaldo, en realidad 'sabe' proporcionar a la dirección un objeto de usuario desde el que consultar el ID. –

1

Si realmente desea que Address tenga una versión de SQL habilitada "User.name" sin la necesidad de unirse explícitamente, necesita usar una subconsulta correlacionada. Esto funcionará en todos los casos, pero tiende a ser ineficiente en el lado de la base de datos (particularmente con MySQL), por lo que posiblemente haya una penalización de rendimiento en el lado de SQL versus el uso de un JOIN regular. Ejecutar algunas pruebas EXPLAIN puede ayudar a analizar la cantidad de un efecto que puede haber.

Otro ejemplo de un column_property correlacionado() está en http://docs.sqlalchemy.org/en/latest/orm/mapped_sql_expr.html#using-column-property.

Para el evento "establecer", una subconsulta correlacionada representa un atributo de solo lectura, pero se puede usar un evento para interceptar los cambios y aplicarlos a la fila del usuario principal. Dos métodos para esto se presentan a continuación, uno usando la mecánica mapa identidad regular, que incurrir en una carga de la fila del usuario si no está ya presente, el otro que emite actualización directa a la fila:

from sqlalchemy import * 
from sqlalchemy.orm import * 
from sqlalchemy.ext.declarative import declarative_base 

Base= declarative_base() 

class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True) 
    name = Column(String(50)) 
    addresses = relation("Address", backref="user") 

class Address(Base): 
    __tablename__ = 'addresses' 

    id = Column(Integer, primary_key=True) 
    user_id = Column(Integer, ForeignKey('users.id')) 
    email = Column(String(50)) 

Address.user_name = column_property(select([User.name]).where(User.id==Address.id)) 

from sqlalchemy import event 
@event.listens_for(Address.user_name, "set") 
def _set_address_user_name(target, value, oldvalue, initiator): 
    # use ORM identity map + flush 
    target.user.name = value 

    # use direct UPDATE 
    #object_session(target).query(User).with_parent(target).update({'name':value}) 

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

s = Session(e) 
s.add_all([ 
    User(name='u1', addresses=[Address(email='e1'), Address(email='e2')]) 
]) 
s.commit() 

a1 = s.query(Address).filter(Address.user_name=="u1").first() 
assert a1.user_name == "u1" 

a1.user_name = 'u2' 
s.commit() 

a1 = s.query(Address).filter(Address.user_name=="u2").first() 
assert a1.user_name == "u2" 
+0

¿Hay algún beneficio con este enfoque?Una propiedad estándar de Python parece más directa (ver mi respuesta). ¿O me falta algo? (Sin embargo, acceder a él como 'address.user.name' es IMO, la versión más pitonica) – schlamar

+0

, así que también puede consultarlo sin llamar a join(). Este patrón fue uno que enfaticé mucho en los primeros días de SQLA, y aún tiene relevancia si necesita algo así como un "conteo agregado". Pero para llegar a un atributo relacionado, sí, no tanto. SQLA realmente quiere que deletree la geometría de las consultas. – zzzeek

Cuestiones relacionadas