Ho un database con schema a stella che voglio rappresentare in SQLAlchemy. Ora ho il problema su come questo può essere fatto nel miglior modo possibile. Al momento ho molte proprietà con condizioni di join personalizzate, perché i dati sono memorizzati in diverse tabelle. Sarebbe bello se fosse possibile riutilizzare le dimensioni per tabelle dei fatti differenti ma non ho capito come si possa fare bene.Schema a stella in SQLAlchemy
risposta
Una tabella dei fatti tipica in uno schema a stella contiene riferimenti a chiavi esterne a tutte le tabelle di dimensioni, quindi di solito non vi è alcuna necessità di condizioni di join personalizzate: vengono determinate automaticamente dai riferimenti di chiavi esterne.
Per esempio uno schema a stella con due tabelle dei fatti sarà 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 si vuole ridurre il boilerplate in ogni caso. Mi piacerebbe creare i generatori locali alle classi di dimensioni che si configurano in 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)
in cui l'utilizzo caso sarebbe come:
class FactOne(Base):
...
Store.add_dimension(FactOne)
Ma, c'è un problema con questo. Supponendo che le colonne delle dimensioni che si stanno aggiungendo siano colonne di chiavi primarie, la configurazione del mapper fallirà poiché una classe deve avere le proprie chiavi primarie impostate prima della configurazione del mapping. Quindi, supponendo che stiamo usando dichiarativa (che vedrete di seguito ha un effetto bello), per fare questo lavoro approccio avremmo dovuto utilizzare la funzione instrument_declarative()
anziché la meta-classe di serie:
meta = MetaData()
registry = {}
def register_cls(*cls):
for c in cls:
instrument_declarative(c, registry, meta)
Allora abbiamo 'd fare qualcosa sulla falsariga di:
class Store(object):
# ...
class FactOne(object):
__tablename__ = 'sales_fact_one'
Store.add_dimension(FactOne)
register_cls(Store, FactOne)
Se effettivamente avete un buon motivo per la personalizzazione si uniscono le condizioni, a patto che ci sia un certo modello di come si creano queste condizioni, è possibile generare che con il vostro 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 siete su 2.6, è trasformare add_dimension
in un decoratore di classe. Ecco un esempio di tutto pulito:
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()
Design molto bello - bello! –
Ispirato da questo, ho finalmente capito come passare la configurazione a dichiarato_attr, in modo che le librerie possano essere rese consapevoli dei modelli di applicazione host: https://gist.github.com/miohtama/844cc78bcf1d317e31ca –