Подтвердить что ты не робот

Вычисленная колонка SQLAlchemy

(Новое оповещение пользователя SQLAlchemy) У меня есть три таблицы: человек, почасовая ставка лиц, начинающаяся с определенной даты, и ежедневная отчетность. Я ищу правильный способ иметь стоимость для временной базы почасовой ставки лиц в этот день.

Да, я мог бы вычислить значение во время создания и иметь это как часть модели, но подумать об этом как пример суммирования более сложных данных за занавеской. Как вычислить Time.cost? Это гибрид_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())
4b9b3361

Ответ 1

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

Прежде чем вы узнаете, как это гибрид, вам нужно подумать о SQL. Как мы можем запрашивать Time.cost над произвольной серией строк? Мы можем связать время с человеком чисто, потому что у нас есть простой внешний ключ. Но связать Time to Payrate с этой конкретной схемой сложно, потому что Time привязывается к Payrate не только через person_id, но и через workon - в SQL, к которому мы с наибольшей легкостью присоединяемся, используя time.person_id = person.id AND time. doneon BETWEEN payrate.start_date И 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 для "конечной" даты, поэтому один из способов обойти это - использовать COALESCE с очень высокой датой (другой использовать условные обозначения):

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_derived.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 через минуту):

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

Итак, чтобы соединение было большим и klunky, и нам нужно будет делать это часто, не говоря уже о том, что нам нужно будет загрузить ту же коллекцию в Python, когда мы будем использовать наш гибрид in-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

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

Вот полный рабочий пример:

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:[email protected]/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

Ответ 2

Много раз лучший совет, который я могу дать, - просто сделать это по-другому. Столбец с несколькими таблицами, подобный этому, относится к базе данных views. Создайте представление, основанное на таблице времени (или любом другом, что вы хотите), с вашим расчетным столбцом в ней, постройте модель на основе представления и настройте ее. Это, вероятно, будет менее напряженным в базе данных. Это также хороший пример того, почему предельный дизайн для того, что можно выполнить с помощью автоматических миграций, опасен.