Domanda

Ho un database con architettura a stella che voglio rappresentare in SQLAlchemy. Ora ho il problema su come farlo nel miglior modo possibile. In questo momento ho molte proprietà con condizioni di join personalizzate, perché i dati sono memorizzati in tabelle diverse. Sarebbe bello se fosse possibile riutilizzare le dimensioni per diversi tablew di fatti, ma non ho capito come farlo bene.

È stato utile?

Soluzione

Una tipica tabella dei fatti in uno schema a stella contiene riferimenti di chiave esterna a tutte le tabelle di dimensioni, quindi di solito non ci sarebbe alcuna necessità di condizioni di join personalizzate: sono determinate automaticamente da riferimenti di chiave esterna.

Ad esempio uno schema a stella con due tabelle dei fatti sarebbe simile:

Base = declarative_meta()

class Store(Base):
    __tablename__ = 'store'

    id = Column('id', Integer, primary_key=True)
    name = Column('name', String(50), nullable=False)

class Product(Base):
    __tablename__ = 'product'

    id = Column('id', Integer, primary_key=True)
    name = Column('name', String(50), nullable=False)

class FactOne(Base):
    __tablename__ = 'sales_fact_one'

    store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
    product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
    units_sold = Column('units_sold', Integer, nullable=False)

    store = relation(Store)
    product = relation(Product)

class FactTwo(Base):
    __tablename__ = 'sales_fact_two'

    store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
    product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
    units_sold = Column('units_sold', Integer, nullable=False)

    store = relation(Store)
    product = relation(Product)

Ma supponiamo che tu voglia ridurre la piastra della caldaia in ogni caso. Creerei generatori locali per le classi di dimensioni che si configurano su una tabella dei fatti:

class Store(Base):
    __tablename__ = 'store'

    id = Column('id', Integer, primary_key=True)
    name = Column('name', String(50), nullable=False)

    @classmethod
    def add_dimension(cls, target):
        target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
        target.store = relation(cls)

nel qual caso l'utilizzo sarebbe come:

class FactOne(Base):
    ...

Store.add_dimension(FactOne)

Ma c'è un problema. Supponendo che le colonne delle dimensioni che si stanno aggiungendo siano colonne di chiavi primarie, la configurazione del mapper non andrà a buon fine poiché una classe deve avere le sue chiavi primarie impostate prima di impostare il mapping. Quindi supponendo che stiamo usando dichiarative (che vedrai di seguito ha un buon effetto), per far funzionare questo approccio dovremmo usare la funzione instrument_declarative () invece della metaclasse standard:

meta = MetaData()
registry = {}
def register_cls(*cls):
    for c in cls:
        instrument_declarative(c, registry, meta)

Quindi faremmo qualcosa del genere:

class Store(object):
    # ...

class FactOne(object):
    __tablename__ = 'sales_fact_one'

Store.add_dimension(FactOne)

register_cls(Store, FactOne)

Se in realtà hai una buona ragione per le condizioni di join personalizzate, purché ci sia un modo per creare tali condizioni, puoi generarlo con il tuo add_dimension () :

class Store(object):
    ...

    @classmethod
    def add_dimension(cls, target):
        target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
        target.store = relation(cls, primaryjoin=target.store_id==cls.id)

Ma l'ultima cosa interessante se sei su 2.6, è trasformare add_dimension in un decoratore di classe. Ecco un esempio con tutto ripulito:

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

class BaseMeta(type):
    classes = set()
    def __init__(cls, classname, bases, dict_):
        klass = type.__init__(cls, classname, bases, dict_)
        if 'metadata' not in dict_:
            BaseMeta.classes.add(cls)
        return klass

class Base(object):
    __metaclass__ = BaseMeta
    metadata = MetaData()
    def __init__(self, **kw):
        for k in kw:
            setattr(self, k, kw[k])

    @classmethod
    def configure(cls, *klasses):
        registry = {}
        for c in BaseMeta.classes:
            instrument_declarative(c, registry, cls.metadata)

class Store(Base):
    __tablename__ = 'store'

    id = Column('id', Integer, primary_key=True)
    name = Column('name', String(50), nullable=False)

    @classmethod
    def dimension(cls, target):
        target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
        target.store = relation(cls)
        return target

class Product(Base):
    __tablename__ = 'product'

    id = Column('id', Integer, primary_key=True)
    name = Column('name', String(50), nullable=False)

    @classmethod
    def dimension(cls, target):
        target.product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
        target.product = relation(cls)
        return target

@Store.dimension
@Product.dimension
class FactOne(Base):
    __tablename__ = 'sales_fact_one'

    units_sold = Column('units_sold', Integer, nullable=False)

@Store.dimension
@Product.dimension
class FactTwo(Base):
    __tablename__ = 'sales_fact_two'

    units_sold = Column('units_sold', Integer, nullable=False)

Base.configure()

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

    sess = sessionmaker(engine)()

    sess.add(FactOne(store=Store(name='s1'), product=Product(name='p1'), units_sold=27))
    sess.commit()
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top