Question

J'ai une base de données d'architecture en schéma en étoile que je veux représenter dans SQLAlchemy. Maintenant, j'ai le problème de savoir comment cela peut être fait de la meilleure façon possible. À l'heure actuelle, j'ai beaucoup de propriétés avec des conditions de jointure personnalisées, car les données sont stockées dans des tables différentes. Ce serait bien s'il serait possible de réutiliser les dimensions pour différentes tables de faits, mais je n'ai pas compris comment cela peut être fait de manière satisfaisante.

Était-ce utile?

La solution

Une table de faits typique dans un schéma en étoile contient des références de clé étrangère à toutes les tables de dimension. Par conséquent, les conditions de jointure personnalisées ne sont généralement pas nécessaires. Elles sont automatiquement déterminées à partir de références de clé étrangère.

Par exemple, un schéma en étoile avec deux tables de faits ressemblerait à ceci:

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)

Mais supposons que vous souhaitiez réduire le passe-partout dans tous les cas. Je créerais des générateurs locaux pour les classes de dimension qui se configurent sur une table de faits:

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)

auquel cas l'utilisation serait comme:

class FactOne(Base):
    ...

Store.add_dimension(FactOne)

Mais cela pose un problème. En supposant que les colonnes de dimension que vous ajoutez soient des colonnes de clé primaire, la configuration du mappeur va échouer car une classe doit avoir ses clés primaires configurées avant le mappage. Donc, en supposant que nous utilisions déclarative (ce que vous verrez ci-dessous a un effet intéressant), pour que cette approche fonctionne, il faudrait utiliser la fonction instrument_declarative () à la place de la métaclasse standard:

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

Nous ferions alors quelque chose dans le sens de:

class Store(object):
    # ...

class FactOne(object):
    __tablename__ = 'sales_fact_one'

Store.add_dimension(FactOne)

register_cls(Store, FactOne)

Si vous avez réellement une bonne raison de créer des conditions de jointure personnalisées, vous pouvez générer cela avec votre 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)

Mais la dernière chose intéressante si vous utilisez la version 2.6 est de transformer add_dimension en un décorateur de classe. Voici un exemple avec tout nettoyé:

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()
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top