2009-09-16 4 views
7

У меня есть архитектура с архитектурой звездной схемы, которую я хочу представлять в SQLAlchemy. Теперь у меня есть проблема, как это можно сделать наилучшим образом. Сейчас у меня много свойств с пользовательскими условиями соединения, потому что данные хранятся в разных таблицах. Было бы неплохо, если бы можно было использовать размеры для разных таблиц фактов, но я не понял, как это можно сделать красиво.Звездная схема в SQLAlchemy

ответ

19

Типичная таблица фактов в схеме звезд содержит ссылки на внешние ключи для всех таблиц измерений, поэтому обычно не требуется никаких условий пользовательского соединения - они определяются автоматически из ссылок на внешние ключи.

Например звезда схема с двумя таблицами фактов будет выглядеть так:

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) 

Но предположим, что вы хотите, чтобы уменьшить шаблонного в любом случае. Я бы создать генераторы локальных для классов размеров, которые настоятся на таблице фактов:

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) 

, в которых использование случае было бы как:

class FactOne(Base): 
    ... 

Store.add_dimension(FactOne) 

Но есть проблема с этим. Предполагая, что столбцы измерения, которые вы добавляете, являются столбцами первичного ключа, конфигурация сопоставления будет терпеть неудачу, так как класс должен иметь свои первичные ключи, настроенные до того, как настроено сопоставление. Таким образом, при условии, что мы используем декларативные (который вы увидите ниже, имеет хороший эффект), чтобы сделать этот подход работать, мы должны были бы использовать функцию instrument_declarative() вместо стандартного метакласса:

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

Итак, мы «d сделать что-то вдоль линий:

class Store(object): 
    # ... 

class FactOne(object): 
    __tablename__ = 'sales_fact_one' 

Store.add_dimension(FactOne) 

register_cls(Store, FactOne) 

Если у вас действительно есть хороший повод для пользовательских присоединиться условий, до тех пор, пока какая-то картина того, как создаются эти условия, вы можете создать что с 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) 

Но последняя замечательная вещь, если вы на 2.6, состоит в том, чтобы превратить add_dimension в декоратор класса. Вот пример со всем убираемым:

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() 
+0

Очень красивый дизайн - красивый! –

+0

Вдохновленный этим, я, наконец, понял, как передать конфигурацию объявленному_attr, чтобы библиотеки могли быть ознакомлены с моделями хост-приложений: https://gist.github.com/miohtama/844cc78bcf1d317e31ca –