2010-05-19 18 views
5

Estoy tratando de crear una estructura db en la que tengo muchos tipos de entidades de contenido, de las cuales una, un Comentario, se puede adjuntar a cualquier otra.Creando tablas autorreferenciales con polimorfismo en SQLALchemy

considerar lo siguiente:

from datetime import datetime 
from sqlalchemy import create_engine 
from sqlalchemy import Column, ForeignKey 
from sqlalchemy import Unicode, Integer, DateTime 
from sqlalchemy.orm import relation, backref 
from sqlalchemy.ext.declarative import declarative_base 

Base = declarative_base() 

class Entity(Base): 
    __tablename__ = 'entities' 
    id = Column(Integer, primary_key=True) 
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 
    edited_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) 
    type = Column(Unicode(20), nullable=False) 
    __mapper_args__ = {'polymorphic_on': type} 

# <...insert some models based on Entity...> 

class Comment(Entity): 
    __tablename__ = 'comments' 
    __mapper_args__ = {'polymorphic_identity': u'comment'} 
    id = Column(None, ForeignKey('entities.id'), primary_key=True) 
    _idref = relation(Entity, foreign_keys=id, primaryjoin=id == Entity.id) 
    attached_to_id = Column(Integer, ForeignKey('entities.id'), nullable=False) 
    #attached_to = relation(Entity, remote_side=[Entity.id]) 
    attached_to = relation(Entity, foreign_keys=attached_to_id, 
          primaryjoin=attached_to_id == Entity.id, 
          backref=backref('comments', cascade="all, delete-orphan")) 

    text = Column(Unicode(255), nullable=False) 

engine = create_engine('sqlite://', echo=True) 
Base.metadata.bind = engine 
Base.metadata.create_all(engine) 

Esto parece correcto, excepto SQLAlchemy no le gusta tener dos claves externas que apuntan a la misma matriz. Dice ArgumentError: Can't determine join between 'entities' and 'comments'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.

¿Cómo especifico onclause?

Respuesta

10

Trata de complementar su Comment.__mapper_args__ a:

__mapper_args__ = { 
    'polymorphic_identity': 'comment', 
    'inherit_condition': (id == Entity.id), 
} 

P. S. No entiendo para qué necesitas una relación _idref? Si desea utilizar un comentario en algún lugar donde se espera Entity, puede pasar la instancia Comment tal como está.

+0

El _idref fue un intento de aclarar a SQLAlchemy qué relación se refiere a qué. No sirve de nada en mi código. –

+0

Así que lo intenté y solucionó el problema, pero introduje uno nuevo: al eliminar cualquier otra entidad (que tiene un respaldo de este modelo, pero ninguna instancia) falla con 'TypeError: id() toma exactamente un argumento (0 dado) ' –

+0

No he entendido completamente tu comentario, pero parece que estás en un lío con la función incorporada de' 'id()' de Python. ¿Olvidaste anexar la identificación con 'self'? ¿O movió '__mapper_args__' en la declaración de clase * después de la * declaración del campo' id'? – nkrkv

3

Para complementar la respuesta de @ nailxx: Repetir todo ese texto repetitivo es tedioso si tiene muchas tablas dependientes. Solución: mover todo a una metaclase.

class EntityMeta(type(Entity)): 
    def __init__(cls, name, bases, dct): 
     ident = dct.get('_identity',None) 
     if '__abstract__' not in dct: 
      xid = Column(None, ForeignKey(Entity.id), primary_key=True) 
      setattr(cls,'id',xid) 
      setattr(cls,'__mapper_args__', { 'polymorphic_identity': dct['_identity'], 'inherit_condition': (xid == Entity.id,), 'primary_key':(xid,) }) 
      setattr(cls,'__tablename__',name.lower()) 
     super(EntityMeta, cls).__init__(name, bases, dct) 

class EntityRef(Entity): 
    __abstract__ = True 
    __metaclass__ = EntityMeta 

class Comment(EntityRef): 
    _identity = 'comment' 
    [...] 

variaciones, como un ejercicio para el lector: Se puede omitir la declaración _identity=… y utilizar el nombre de clase, como en la línea setattr(cls,'__tablename__',…). O no sobrescribir un atributo existente __tablename__ o __mapper_args__.

Asegúrese de probar dct y no cls por existencia: este último encontrará atributos en la clase principal, que obviamente no desea en este momento.

Cuestiones relacionadas