SQLAlchemy で表現したいスター スキーマ アーキテクチャのデータベースがあります。今、これを可能な限り最善の方法で行うにはどうすればよいかという問題があります。現在、データが異なるテーブルに保存されているため、カスタムの結合条件を持つ多くのプロパティがあります。異なるファクトテーブルのディメンションを再利用できればいいのですが、それをうまく行う方法がわかりません。
1 に答える
スター スキーマの一般的なファクト テーブルには、すべてのディメンション テーブルへの外部キー参照が含まれているため、通常、カスタムの結合条件は必要ありません。結合条件は、外部キー参照から自動的に決定されます。
たとえば、2 つのファクト テーブルを持つスター スキーマは次のようになります。
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)
したがって、次のようなことを行います。
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()