web-dev-qa-db-ja.com

SQLAlchemy計算列

(新しいSQLAlchemyユーザーアラート)私は3つのテーブルを持っています:人、特定の日付から始まる人の時給、および毎日の時間レポート。私は、その日の時間当たり料金からタイムベースのコストを削減するための正しい方法を探しています。

はい、作成時に値を計算し、それをモデルの一部として含めることができますが、これは、カーテンの背後にあるより複雑なデータを要約する例と考えてください。 Time.costの計算方法を教えてください。それは、hybrid_propery、column_property、またはまったく異なるものですか?

class Person(Base):
    __tablename__ = 'person'
    personID = Column(Integer, primary_key=True)
    name = Column(String(30), unique=True)

class Payrate(Base):
    __tablename__ = 'payrate'
    payrateID = Column(Integer, primary_key=True)
    personID  = Column(Integer, ForeignKey('person.personID'))
    hourly    = Column(Integer)
    starting  = Column(Date)
    __tableargs__ =(UniqueConstraint('personID', 'starting',
                                     name='uc_peron_starting'))

class Time(Base):
    __tablename__ = 'entry'
    entryID  = Column(Integer, primary_key=True)
    personID = Column(Integer, ForeignKey('person.personID'))
    workedon = Column(Date)
    hours    = Column(Integer)

    person = relationship("Person")

    def __repr__(self):
        return "<{date} {hours}hrs ${0.cost:.02f}>".format(self, 
                      date=self.workedon.isoformat(), hours=to_hours(self.hours))

    @property
    def cost(self):
        '''Cost of entry
        '''
        ## This is where I am stuck in propery query creation
        return self.hours * query(Payrate).filter(
                             and_(Payrate.personID==personID,
                                  Payrate.starting<=workedon
                             ).order_by(
                               Payrate.starting.desc())
32
Frustrated

ここにある問題は、可能な限りエレガントに解決するためにvery高度なSQLAlchemyテクニックを使用しているので、あなたは初心者であることがわかりますが、この答えは最後まであなたに示します。ただし、このような問題を解決するには、一度に1ステップずつ歩く必要があり、必要な答えをさまざまな方法で得ることができます。

これをハイブリッド化する方法に入る前に、SQLについて考える必要があります。任意の一連の行に対してTime.costをクエリするにはどうすればよいですか?単純な外部キーがあるので、TimeをPersonにきれいにリンクできます。ただし、TimeをPayrateにリンクするには、この特定のスキーマを使用するのは難しいです。これは、Timeがperson_idだけでなく、workonを介してもPayrateにリンクするためです。SQLでは、 "time.person_id = person.id AND time。 Workon BETWEEN payrate.start_date AND payrate.end_date」です。ただし、ここには「end_date」がありません。つまり、これも導出する必要があります。その派生が最もトリッキーな部分なので、私が思いついたのは次のようになります(列名を小文字にしました)。

_SELECT payrate.person_id, payrate.hourly, payrate.starting, ending.ending
FROM payrate LEFT OUTER JOIN
(SELECT pa1.payrate_id, MIN(pa2.starting) as ending FROM payrate AS pa1
JOIN payrate AS pa2 ON pa1.person_id = pa2.person_id AND pa2.starting > pa1.starting
GROUP BY pa1.payrate_id
) AS ending ON payrate.payrate_id=ending.payrate_id
_

これを取得する他の方法があるかもしれませんが、それは私が思いついたものです-他の方法はほぼ確実に同じような種類のことが起こっているでしょう(すなわち、サブクエリ、結合)。

したがって、給与が開始/終了すると、クエリがどのようになるかを理解できます。 BETWEENを使用して時間エントリを日付範囲に一致させる必要がありますが、最新の給与エントリには「終了」日付のNULLがあるため、回避策の1つは非常に高い日付に対してCOALESCEを使用することです(もう1つは条件文を使用するには):

_SELECT *, entry.hours * payrate_derived.hourly
FROM entry
JOIN
    (SELECT payrate.person_id, payrate.hourly, payrate.starting, ending.ending
    FROM payrate LEFT OUTER JOIN
    (SELECT pa1.payrate_id, MIN(pa2.starting) as ending FROM payrate AS pa1
    JOIN payrate AS pa2 ON pa1.person_id = pa2.person_id AND pa2.starting > pa1.starting
    GROUP BY pa1.payrate_id
    ) AS ending ON payrate.payrate_id=ending.payrate_id) as payrate_derived
ON entry.workedon BETWEEN payrate_derived.starting AND COALESCE(payrate_derived.ending, "9999-12-31")
AND entry.person_id=payrate_derived.person_id
ORDER BY entry.person_id, entry.workedon
_

@hybridがSQLAlchemyでできることは、SQL式レベルで実行した場合、まさに「entry.hours * payrate_deriv.hourly」の部分だけです。 JOINなどはすべて、ハイブリッドの外部に提供する必要があります。

したがって、その大きなサブクエリをこれに固執する必要があります。

_class Time(...):
    @hybrid_property
    def cost(self):
        # ....

    @cost.expression
    def cost(cls):
        return cls.hours * <SOMETHING>.hourly
_

それでは、_<SOMETHING>_が何であるかを理解しましょう。そのSELECTをオブジェクトとして構築します。

_from sqlalchemy.orm import aliased, join, outerjoin
from sqlalchemy import and_, func

pa1 = aliased(Payrate)
pa2 = aliased(Payrate)
ending = select([pa1.payrate_id, func.min(pa2.starting).label('ending')]).\
            select_from(join(pa1, pa2, and_(pa1.person_id == pa2.person_id, pa2.starting > pa1.starting))).\
            group_by(pa1.payrate_id).alias()

payrate_derived = select([Payrate.person_id, Payrate.hourly, Payrate.starting, ending.c.ending]).\
    select_from(outerjoin(Payrate, ending, Payrate.payrate_id == ending.c.payrate_id)).alias()
_

式側のcost()ハイブリッドは、payrate_derivedを参照する必要があります(python側を1分で実行します):

_class Time(...):
    @hybrid_property
    def cost(self):
        # ....

    @cost.expression
    def cost(cls):
        return cls.hours * payrate_derived.c.hourly
_

次に、cost()ハイブリッドを使用するには、その結合を持つクエリのコンテキスト内にある必要があります。ここでは、Pythonの_datetime.date.max_を使用してその最大日付を取得しています(便利です)。

_print session.query(Person.name, Time.workedon, Time.hours, Time.cost).\
                    select_from(Time).\
                    join(Time.person).\
                    join(payrate_derived,
                            and_(
                                payrate_derived.c.person_id == Time.person_id,
                                Time.workedon.between(
                                    payrate_derived.c.starting,
                                    func.coalesce(
                                        payrate_derived.c.ending,
                                        datetime.date.max
                                    )
                                )
                            )
                    ).\
                    all()
_

そのため、結合は大きくて扱いにくいので、頻繁に行う必要があります。言うまでもなく、PythonでPythonを実行するときに、同じコレクションをPythonにロードする必要があります。ハイブリッドです。relationship()を使用してマップできます。つまり、カスタム結合条件を設定する必要がありますが、非プライマリマッパーと呼ばれるあまり知られていない手法を使用して、実際にそのサブクエリにマップする必要があります。非プライマリマッパーを使用すると、行を選択するためだけにクラスを任意のテーブルまたはSELECT構成にマップすることができます。通常、Queryはすでに任意の列とサブクエリをクエリできるため、これを使用する必要はありませんが、 relationship()からそれを取得するには、マッピングが必要です。マッピングには主キーを定義する必要があり、関係は関係のどちら側が「外部」であるかを知る必要もあります。これは最も高度な部分ですこことこの場合、それはこのように機能します:

_from sqlalchemy.orm import mapper, relationship, foreign

payrate_derived_mapping = mapper(Payrate, payrate_derived, non_primary=True,
                                        primary_key=[
                                            payrate_derived.c.person_id,
                                            payrate_derived.c.starting
                                        ])
Time.payrate = relationship(
                    payrate_derived_mapping,
                    viewonly=True,
                    uselist=False,
                    primaryjoin=and_(
                            payrate_derived.c.person_id == foreign(Time.person_id),
                            Time.workedon.between(
                                payrate_derived.c.starting,
                                func.coalesce(
                                    payrate_derived.c.ending,
                                    datetime.date.max
                                )
                            )
                        )
                    )
_

そのため、これがその結合について最後に確認する必要があります。これで、以前のクエリを次のように実行できます。

_print session.query(Person.name, Time.workedon, Time.hours, Time.cost).\
                    select_from(Time).\
                    join(Time.person).\
                    join(Time.payrate).\
                    all()
_

そして最後に、新しいpayrate関係をPythonレベルのハイブリッドにも接続できます。

_class Time(Base):
    # ...

    @hybrid_property
    def cost(self):
        return self.hours * self.payrate.hourly

    @cost.expression
    def cost(cls):
        return cls.hours * payrate_derived.c.hourly
_

ここでの解決策は多くの労力を費やしましたが、少なくとも最も複雑な部分である賃金マッピングは、完全に1か所にあり、もう一度見る必要はありません。

以下は完全に機能する例です。

_from sqlalchemy import create_engine, Column, Integer, ForeignKey, Date, \
                    UniqueConstraint, select, func, and_, String
from sqlalchemy.orm import join, outerjoin, relationship, Session, \
                    aliased, mapper, foreign
from sqlalchemy.ext.declarative import declarative_base
import datetime
from sqlalchemy.ext.hybrid import hybrid_property


Base = declarative_base()

class Person(Base):
    __tablename__ = 'person'
    person_id = Column(Integer, primary_key=True)
    name = Column(String(30), unique=True)

class Payrate(Base):
    __tablename__ = 'payrate'
    payrate_id = Column(Integer, primary_key=True)
    person_id  = Column(Integer, ForeignKey('person.person_id'))
    hourly    = Column(Integer)
    starting  = Column(Date)

    person = relationship("Person")
    __tableargs__ =(UniqueConstraint('person_id', 'starting',
                                     name='uc_peron_starting'))

class Time(Base):
    __tablename__ = 'entry'
    entry_id  = Column(Integer, primary_key=True)
    person_id = Column(Integer, ForeignKey('person.person_id'))
    workedon = Column(Date)
    hours    = Column(Integer)

    person = relationship("Person")

    @hybrid_property
    def cost(self):
        return self.hours * self.payrate.hourly

    @cost.expression
    def cost(cls):
        return cls.hours * payrate_derived.c.hourly

pa1 = aliased(Payrate)
pa2 = aliased(Payrate)
ending = select([pa1.payrate_id, func.min(pa2.starting).label('ending')]).\
            select_from(join(pa1, pa2, and_(
                                        pa1.person_id == pa2.person_id,
                                        pa2.starting > pa1.starting))).\
            group_by(pa1.payrate_id).alias()

payrate_derived = select([Payrate.person_id, Payrate.hourly, Payrate.starting, ending.c.ending]).\
    select_from(outerjoin(Payrate, ending, Payrate.payrate_id == ending.c.payrate_id)).alias()

payrate_derived_mapping = mapper(Payrate, payrate_derived, non_primary=True,
                                        primary_key=[
                                            payrate_derived.c.person_id,
                                            payrate_derived.c.starting
                                        ])
Time.payrate = relationship(
                    payrate_derived_mapping,
                    viewonly=True,
                    uselist=False,
                    primaryjoin=and_(
                            payrate_derived.c.person_id == foreign(Time.person_id),
                            Time.workedon.between(
                                payrate_derived.c.starting,
                                func.coalesce(
                                    payrate_derived.c.ending,
                                    datetime.date.max
                                )
                            )
                        )
                    )



e = create_engine("postgresql://scott:tiger@localhost/test", echo=False)
Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)
p1 = Person(name='p1')
session.add(p1)

session.add_all([
    Payrate(hourly=10, starting=datetime.date(2013, 5, 17), person=p1),
    Payrate(hourly=15, starting=datetime.date(2013, 5, 25), person=p1),
    Payrate(hourly=20, starting=datetime.date(2013, 6, 10), person=p1),
])

session.add_all([
    Time(person=p1, workedon=datetime.date(2013, 5, 19), hours=10),
    Time(person=p1, workedon=datetime.date(2013, 5, 27), hours=5),
    Time(person=p1, workedon=datetime.date(2013, 5, 30), hours=5),
    Time(person=p1, workedon=datetime.date(2013, 6, 18), hours=12),
])
session.commit()

print session.query(Person.name, Time.workedon, Time.hours, Time.cost).\
                    select_from(Time).\
                    join(Time.person).\
                    join(Time.payrate).\
                    all()

for time in session.query(Time):
    print time.person.name, time.workedon, time.hours, time.payrate.hourly, time.cost
_

出力(最初の行は集約バージョン、残りはオブジェクトごと):

_[(u'p1', datetime.date(2013, 5, 19), 10, 100), (u'p1', datetime.date(2013, 5, 27), 5, 75), (u'p1', datetime.date(2013, 5, 30), 5, 75), (u'p1', datetime.date(2013, 6, 18), 12, 240)]
p1 2013-05-19 10 10 100
p1 2013-05-27 5 15 75
p1 2013-05-30 5 15 75
p1 2013-06-18 12 20 240
_
94
zzzeek

多くの場合、私が与えることができる最良のアドバイスは、それを違うものにすることです。このようなマルチテーブル計算列は、データベース views の目的です。計算された列を含むタイムテーブル(またはその他の必要なもの)に基づいてビューを作成し、ビューに基づいてモデルを作成すると、設定が完了します。これにより、データベースへの負荷も軽減されます。これは、設計を自動化 migrations によって達成できるものに制限することが危険である理由の良い例でもあります。

0
juanitogan