2013-07-17 16 views
12

Does sqlalchemy ha qualcosa come GenericForeignKey di django? Ed è giusto usare campi estranei generici.sqlalchemy chiave esterna generica (come in django ORM)

Il mio problema è: ho diversi modelli (ad esempio, post, progetto, posto vacante, niente di speciale lì) e voglio aggiungere commenti a ciascuno di essi. E voglio usare solo un modello di commento. Ne vale la pena? O dovrei usare PostComment, ProjectComment ecc? Pro/contro di entrambi i modi?

Grazie!

risposta

13

Il modello più semplice che utilizzo più spesso è che in realtà si dispone di tabelle di commenti separate per ogni relazione. Ciò può sembrare spaventoso all'inizio, ma non comporta alcun codice aggiuntivo rispetto a qualsiasi altro approccio: le tabelle vengono create automaticamente e i modelli vengono riferiti utilizzando il modello Post.Comment, Project.Comment, ecc. La definizione di Commento viene mantenuta in un posto. Questo approccio da un punto di vista referenziale è il più semplice ed efficiente, nonché il più amichevole DBA, poiché i diversi tipi di commenti sono conservati nelle proprie tabelle che possono essere ridimensionate individualmente.

Un altro schema da utilizzare è una singola tabella di commenti, ma tabelle di associazione distinte. Questo modello offre il caso d'uso che si potrebbe desiderare un commento collegato a più di un tipo di oggetto alla volta (come un post e un progetto allo stesso tempo). Questo modello è ancora ragionevolmente efficiente.

In terzo luogo, c'è la tabella di associazione polimorfica. Questo modello utilizza un numero fisso di tabelle per rappresentare le raccolte e la classe correlata senza sacrificare l'integrità referenziale. Questo modello cerca di avvicinarsi alla "chiave estranea generica" ​​in stile Django pur mantenendo l'integrità referenziale, sebbene non sia così semplice come i precedenti due approcci.

È anche possibile imitare il modello utilizzato da ROR/Django, in cui non sono presenti chiavi esterne reali e le righe sono abbinate utilizzando la logica dell'applicazione.

I primi tre modelli sono illustrati in forma moderna nella distribuzione SQLAlchemy in esempi/generic_associations /.

Il pattern ROR/Django, dal momento che viene chiesto spesso, aggiungerò anche agli esempi di SQLAlchemy, anche se non mi piace molto. L'approccio che sto utilizzando non è esattamente lo stesso di quello che fa Django poiché sembra che utilizzino una tabella "contenttypes" per tenere traccia dei tipi, che a me sembra un po 'superfluo, ma l'idea generale di una colonna intera che punta a un numero qualsiasi di tabelle basate su una colonna discriminatore presente. Eccolo:

from sqlalchemy.ext.declarative import declarative_base, declared_attr 
from sqlalchemy import create_engine, Integer, Column, \ 
        String, and_ 
from sqlalchemy.orm import Session, relationship, foreign, remote, backref 
from sqlalchemy import event 


class Base(object): 
    """Base class which provides automated table name 
    and surrogate primary key column. 

    """ 
    @declared_attr 
    def __tablename__(cls): 
     return cls.__name__.lower() 
    id = Column(Integer, primary_key=True) 
Base = declarative_base(cls=Base) 

class Address(Base): 
    """The Address class. 

    This represents all address records in a 
    single table. 

    """ 
    street = Column(String) 
    city = Column(String) 
    zip = Column(String) 

    discriminator = Column(String) 
    """Refers to the type of parent.""" 

    parent_id = Column(Integer) 
    """Refers to the primary key of the parent. 

    This could refer to any table. 
    """ 

    @property 
    def parent(self): 
     """Provides in-Python access to the "parent" by choosing 
     the appropriate relationship. 

     """ 
     return getattr(self, "parent_%s" % self.discriminator) 

    def __repr__(self): 
     return "%s(street=%r, city=%r, zip=%r)" % \ 
      (self.__class__.__name__, self.street, 
      self.city, self.zip) 

class HasAddresses(object): 
    """HasAddresses mixin, creates a relationship to 
    the address_association table for each parent. 

    """ 

@event.listens_for(HasAddresses, "mapper_configured", propagate=True) 
def setup_listener(mapper, class_): 
    name = class_.__name__ 
    discriminator = name.lower() 
    class_.addresses = relationship(Address, 
         primaryjoin=and_(
             class_.id == foreign(remote(Address.parent_id)), 
             Address.discriminator == discriminator 
            ), 
         backref=backref(
           "parent_%s" % discriminator, 
           primaryjoin=remote(class_.id) == foreign(Address.parent_id) 
           ) 
         ) 
    @event.listens_for(class_.addresses, "append") 
    def append_address(target, value, initiator): 
     value.discriminator = discriminator 

class Customer(HasAddresses, Base): 
    name = Column(String) 

class Supplier(HasAddresses, Base): 
    company_name = Column(String) 

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

session = Session(engine) 

session.add_all([ 
    Customer(
     name='customer 1', 
     addresses=[ 
      Address(
        street='123 anywhere street', 
        city="New York", 
        zip="10110"), 
      Address(
        street='40 main street', 
        city="San Francisco", 
        zip="95732") 
     ] 
    ), 
    Supplier(
     company_name="Ace Hammers", 
     addresses=[ 
      Address(
        street='2569 west elm', 
        city="Detroit", 
        zip="56785") 
     ] 
    ), 
]) 

session.commit() 

for customer in session.query(Customer): 
    for address in customer.addresses: 
     print(address) 
     print(address.parent) 
+0

Grazie! Non mi piace il primo schema - penso, non è SECCO. Se voglio aggiungere qualche informazione ad ogni commento (può essere, 'edited' flag) dovrei farlo con tutti i modelli/tabelle. Stavo pensando al secondo modello per il mio modello "Tag". Può essere collegato a 'Project' e' Post' allo stesso tempo. E terzo sembra essere quello di cui ho bisogno per il mio 'Comment'. ROR/Django sembra non essere così semplice, quindi lo "studio". – krasulya

+1

è totalmente ASCIUTTO. ASCIUTO significa "non ripeterti". Se osservi come funziona il modello, non ti stai ripetendo affatto. Solo perché ci sono molte tabelle simili nel DB non significa che ti stai ripetendo; la loro creazione è automatizzata, così come l'aggiunta di alcune nuove colonne come "modificato" (usando uno strumento come Alembic). È l'approccio più efficiente in termini di spazio/tempo e DBA-friendly (poiché l'archiviazione per tabelle diverse può essere configurata in modo indipendente). Peccato che ho così tanti problemi a convincere ex-djangoers di questo. – zzzeek

+0

Mi piace il concetto della tua prima soluzione, ma non sono chiaro su come lo realizzi effettivamente. Potresti fare un breve esempio? – aquavitae

0

So che questo è probabilmente un modo terribile di farlo, ma è stata una soluzione rapida per me.

class GenericRelation(object): 
def __init__(self, object_id, object_type): 
    self.object_id = object_id 
    self.object_type = object_type 

def __composite_values__(self): 
    return (self.object_id, self.object_type) 


class Permission(AbstractBase): 

#__abstract__ = True 

_object = None 

_generic = composite(
    GenericRelation, 
    sql.Column('object_id', data_types.UUID, nullable=False), 
    sql.Column('object_type', sql.String, nullable=False), 
) 

permission_type = sql.Column(sql.Integer) 

@property 
def object(self): 
    session = object_session(self) 
    if self._object or not session: 
     return self._object 
    else: 
     object_class = eval(self.object_type) 
     self._object = session.query(object_class).filter(object_class.id == self.object_id).first() 
     return self._object 

@object.setter 
def object(self, value): 
    self._object = value 
    self.object_type = value.__class__.__name__ 
    self.object_id = value.id 
Problemi correlati