简体   繁体   English

在 PostgreSQL 上使用 CTE() 的 SQLAlchemy 查询

[英]SQLAlchemy query using CTE() on PostgreSQL

I have the following function (stored procedure) on PostgreSQL that calculates availability and pricing for a small boutique hotel prototype application:我在 PostgreSQL 上有以下函数(存储过程),用于计算小型精品酒店原型应用程序的可用性和定价:

-- Function that emulates Transact-SQL's IIF (if-and-only-if)
CREATE OR REPLACE FUNCTION IIF(BOOLEAN, DATE, DATE) RETURNS DATE
AS $$
    SELECT CASE $1 WHEN True THEN $2 ELSE $3 END
$$
LANGUAGE SQL IMMUTABLE;

-- Function to have together all steps that lead to availability and pricing calculation
CREATE OR REPLACE FUNCTION availability(check_in DATE, check_out DATE, guests INTEGER, room INTEGER[] DEFAULT '{}')
RETURNS TABLE (
    r_id INTEGER,
    r_floor_no INTEGER,
    r_room_no INTEGER,
    r_name VARCHAR,
    r_sgl_beds INTEGER,
    r_dbl_beds INTEGER,
    r_accommodates INTEGER,
    r_code VARCHAR,
    t_nights INTEGER,
    t_price REAL
) AS $$
BEGIN
RETURN QUERY
(
    WITH p AS (
         -- Sum of nights and prices per season (0..N)
         SELECT SUM(IIF($1 > t.date_to, t.date_to, $2) - IIF($1 > t.date_from, $1, t.date_from)) AS nights,
                SUM((IIF($2 > t.date_to, t.date_to, $2) - IIF($1 > t.date_from, $1, t.date_from)) * (t.base_price + t.bed_price * $3)) AS price
           FROM rate AS t
          WHERE (t.date_from, t.date_to) OVERLAPS ($1, $2)
            AND t.published = True
         ),
         a AS (
         -- Room availability
         SELECT r.id AS r_id,
                r.floor_no AS r_floor_no,
                r.room_no AS r_room_no,
                r.name AS r_name,
                r.sgl_beds AS r_sgl_beds,
                r.dbl_beds AS r_dbl_beds,
                r.accommodates AS r_accommodates,
                r.supplement AS r_supplement,
                r.code AS r_code
           FROM room AS r
          WHERE r.id NOT IN (
                SELECT b.id_room
                  FROM booking as b
                 WHERE (b.check_in, b.check_out) OVERLAPS ($1, $2)
                   AND b.cancelled IS NULL
                )
            AND r.accommodates >= $3
            AND CASE WHEN $4 = '{}'::INTEGER[] THEN r.id > 0 ELSE r.id = ANY($4) END
         )
  SELECT a.r_id AS r_id,
         a.r_floor_no AS r_floor_no,
         a.r_room_no AS r_room_no,
         a.r_name AS r_name,
         a.r_sgl_beds AS r_sgl_beds,
         a.r_dbl_beds AS r_dbl_beds,
         a.r_accommodates AS r_accommodates,
         a.r_code AS r_code,
         p.nights::INTEGER AS t_nights,
         (a.r_supplement * p.nights + p.price)::REAL AS t_price
    FROM a, p
ORDER BY t_price ASC, r_accommodates ASC, r_sgl_beds ASC, r_dbl_beds ASC, r_floor_no ASC, r_room_no ASC
);
END
$$ LANGUAGE plpgsql;

I am trying to migrate this code to SQLAlchemy but I cannot seem to be able to handle the double use of CTE (Common Table Expression) in the form of WITH p AS [..] and then a AS [..] on SQLAlchemy.我试图迁移该代码SQLAlchemy的,但我不能似乎能够处理的形式的双重使用CTE(公共表表达式)的WITH p AS [..]a AS [..]上SQLAlchemy的。 Here you are what I have so far:到目前为止,您是我所拥有的:

# Sum of nights and prices per season (0..N)
p = session.query(
    func.sum(Rate.date_to - Rate.date_from).label('nights'),
    (func.sum(
        case(
            [(p.check_in > Rate.date_to, Rate.date_to)],
            else_=p.check_out
        ) -
        case(
            [(p.check_in > Rate.date_from, p.check_in)],
            else_=Rate.date_from
        ) * (Rate.base_price + Rate.bed_price * p.guests)
    ).label('price'))
    ).\
    filter(
        tuple_(Rate.date_from, Rate.date_to).
        op('OVERLAPS')
        (tuple_(p.check_in, p.check_out))
    ).\
    filter(Rate.published.is_(True)).\
    cte(name='p')

# Room availability using a sub-select
subq = session.query(Booking.id_room.label('id')).\
    filter(
        tuple_(Booking.check_in, Booking.check_out).
        op('OVERLAPS')
        (tuple_(p.check_in, p.check_out))
    ).\
    filter(Booking.cancelled.is_(None)).\
    subquery('subq')

a = session.query(Room).\
    filter(Room.deleted.is_(None)).\
    filter(Room.id.notin_(subq)).\
    filter(Room.accommodates >= p.guests)
if p.rooms:
    a = a.filter(Room.id.any(p.rooms))
a = a.cte(name='a')

result = session.query(a.id, a.floor_no, a.room_no, a.number,
                       a.name, a.sgl_beds, a.dbl_beds,
                       a.accommodates, a.code, p.nights,
                       (a.supplement * p.nights + p.price).
                       label('total_price')).\
    order_by('total_price').asc().\
    order_by('accommodates').asc().\
    order_by('sgl_beds').asc().\
    order_by('dbl_beds').asc().\
    order_by('floor_no').asc().\
    order_by('room_no').asc().\
    all()

p.check_in (Date), p.check_out (Date), p.guests (int) and p.rooms (List of ints) are input parameters. p.check_in (日期)、 p.check_out (日期)、 p.guests (整数)和p.rooms (整数列表)是输入参数。

The error I am getting is:我得到的错误是:

AttributeError: 'CTE' object has no attribute 'check_in'

On this line:在这一行:

(tuple_(p.check_in, p.check_out))

which is inside the sub-query block:这是在子查询块内:

# Room availability using a sub-select
subq = session.query(Booking.id_room.label('id')).\
    filter(
        tuple_(Booking.check_in, Booking.check_out).
        op('OVERLAPS')
        (tuple_(p.check_in, p.check_out))
    ).\
    filter(Booking.cancelled.is_(None)).\
    subquery('subq')

I have this feeling that SQLAlchemy expects just one call to cte() but I have not been able to figure it out from the documentation online .我有一种感觉,SQLAlchemy 只希望调用一次cte()但我无法从在线文档中弄清楚。 I've tried to build the big query block by block, then assemble them together, but with no success.我试图逐块构建大查询,然后将它们组合在一起,但没有成功。

To help contextualise, here you are the data inside the room table:为了帮助情境化,这里是room表中的数据:

 id | floor_no | room_no |                           name                           | sgl_beds | dbl_beds | supplement |  code  | deleted 
----+----------+---------+----------------------------------------------------------+----------+----------+------------+--------+---------
  1 |        1 |       1 | Normal bedroom with two single beds                      |        2 |        0 |         20 | pink   | 
  2 |        1 |       2 | Large bedroom with two single and one double beds        |        2 |        1 |         40 | black  | 
  3 |        1 |       3 | Very large bedroom with three single and one double beds |        3 |        1 |         50 | white  | 
  4 |        1 |       4 | Very large bedroom with four single beds                 |        4 |        0 |         40 | purple | 
  5 |        1 |       5 | Large bedroom with three single beds                     |        3 |        0 |         30 | blue   | 
  6 |        1 |       6 | Normal bedroom with one double bed                       |        0 |        1 |         20 | brown  | 

accommodates is now an hybrid property in the Room model class but it used to be a column in the table (and could be reverted to it, updated by a trigger). accommodates现在是Room模型类中的一个混合属性,但它曾经是表中的一个列(并且可以恢复为它,由触发器更新)。

And this is the rate table:这是rate表:

 id | date_from  |  date_to   | base_price | bed_price | published 
----+------------+------------+------------+-----------+-----------
  1 | 2017-03-01 | 2017-04-30 |         10 |        19 | t
  2 | 2017-05-01 | 2017-06-30 |         20 |        29 | t
  3 | 2017-07-01 | 2017-08-31 |         30 |        39 | t
  4 | 2017-09-01 | 2017-10-31 |         20 |        29 | t
  5 | 2018-03-01 | 2018-04-30 |         10 |        21 | t
  6 | 2018-05-01 | 2018-06-30 |         20 |        31 | t
  7 | 2018-07-01 | 2018-08-31 |         30 |        41 | t
  8 | 2018-09-01 | 2018-10-31 |         20 |        31 | t
  9 | 2019-03-01 | 2019-04-30 |         10 |        20 | t
 10 | 2019-05-01 | 2019-06-30 |         20 |        30 | t
 11 | 2019-07-01 | 2019-08-31 |         30 |        40 | t
 12 | 2019-09-01 | 2019-10-31 |         20 |        30 | t

Finally, this is a fragment of the booking table:最后,这是booking表的一个片段:

 id | id_guest | id_room |      reserved       | guests |  check_in  | check_out  | checked_in | checked_out | cancelled | base_price | taxes_percentage | taxes_value | total_price | locator | pin  |  status   |    meal_plan    | additional_services |                 uuid                 | deleted 
----+----------+---------+---------------------+--------+------------+------------+------------+-------------+-----------+------------+------------------+-------------+-------------+---------+------+-----------+-----------------+---------------------+--------------------------------------+---------
  1 |        1 |       1 | 2016-12-25 17:00:04 |      2 | 2017-05-05 | 2017-05-09 |            |             |           |        200 |               10 |          20 |         220 | AAAAA   | 1234 | Confirmed | BedAndBreakfast | "PoolKit"=>"1"      | 4df783c9-9375-47d6-8a9d-3309aa2c0a10 | 
  2 |        2 |       2 | 2016-12-26 09:03:54 |      3 | 2017-04-01 | 2017-04-11 |            |             |           |        500 |               10 |          50 |         550 | AAAAB   | 1234 | Confirmed | BedAndBreakfast | "PoolKit"=>"1"      | 0428692a-267a-46e7-871f-a7a20c8e9406 | 
  3 |        3 |       3 | 2016-01-25 14:43:00 |      3 | 2017-06-02 | 2017-06-12 |            |             |           |        500 |               10 |          50 |         550 | AAAAC   | 1234 | Confirmed | BedAndBreakfast | "PoolKit"=>"1"      | 12deeb14-1568-4b70-9247-5df2df433359 | 
  4 |        4 |       4 | 2016-01-25 14:43:00 |      3 | 2017-06-01 | 2017-06-10 |            |             |           |        500 |               10 |          50 |         550 | AAAAD   | 1234 | Confirmed | BedAndBreakfast | "PoolKit"=>"1"      | b3453b07-5ec7-4c15-be72-998e451998c6 | 
  5 |        5 |       5 | 2016-01-25 14:43:00 |      3 | 2017-06-08 | 2017-06-18 |            |             |           |        500 |               10 |          50 |         550 | AAAAE   | 1234 | Confirmed | BedAndBreakfast | "PoolKit"=>"1"      | 02a5c8f8-1d4c-45d6-9698-50bfa6d47b42 |

I am using the latest versions of both SQLAlchemy and PostgreSQL, so no restrictions there.我正在使用 SQLAlchemy 和 PostgreSQL 的最新版本,因此没有任何限制。

Everything you see here does not necessarily have to make all the sense in the world since this is just a prototype to test a number of features of a combination of technologies.您在此处看到的所有内容不一定在世界上都有意义,因为这只是测试技术组合的许多功能的原型。

Thanks in advance.提前致谢。

So after Ilja figuring out the variable naming conflict, I kept on working on the query and this is the final, working result:因此,在 Ilja 找出变量命名冲突后,我继续处理查询,这是最终的工作结果:

from sqlalchemy import func, tuple_, case, cast
from sqlalchemy import Integer as sqlInteger
from sqlalchemy import Float as sqlFloat
from sqlalchemy import Date as sqlDate

p = session.query(
    func.SUM(
        case(
            [(check_out > Rate.date_to, Rate.date_to)],
            else_=check_out
        ) -
        case(
            [(check_in > Rate.date_from, check_in)],
            else_=Rate.date_from
        )
    ).label('nights'),
    (func.SUM((
        case(
            [(check_out > Rate.date_to, Rate.date_to)],
            else_=check_out
        ) -
        case(
            [(check_in > Rate.date_from, check_in)],
            else_=Rate.date_from
        )) * (Rate.base_price + Rate.bed_price * guests)
    ).label('price'))
    ).\
    filter(
        tuple_(Rate.date_from, Rate.date_to).
        op('OVERLAPS')
        (tuple_(cast(check_in, sqlDate), cast(check_out, sqlDate)))
    ).\
    filter(Rate.published.is_(True)).\
    cte(name='p')

# Room availability using a sub-select
subq = session.query(Booking.id_room.label('id')).\
    filter(
        tuple_(Booking.check_in, Booking.check_out).
        op('OVERLAPS')
        (tuple_(cast(check_in, sqlDate), cast(check_out, sqlDate)))
    ).\
    filter(Booking.cancelled.is_(None)).\
    subquery('subq')

a = session.query(Room.id, Room.floor_no, Room.room_no, Room.name,
                  Room.sgl_beds, Room.dbl_beds,  Room.supplement,
                  Room.code, Room.number, Room.accommodates).\
    filter(Room.deleted.is_(None)).\
    filter(Room.id.notin_(subq)).\
    filter(Room.accommodates >= guests)
if rooms:
    a = a.filter(Room.id.any(rooms))
a = a.cte(name='a')

result = session.query(
    a.c.id, a.c.floor_no, a.c.room_no, a.c.name, a.c.sgl_beds,
    a.c.dbl_beds, a.c.code, a.c.number, a.c.accommodates,
    cast(p.c.nights, sqlInteger).label('nights'),
    cast(a.c.supplement * p.c.nights + p.c.price, sqlFloat).
    label('total_price')).\
    order_by('total_price ASC').\
    order_by(a.c.accommodates.asc()).\
    order_by(a.c.sgl_beds.asc()).\
    order_by(a.c.dbl_beds.asc()).\
    order_by(a.c.floor_no.asc()).\
    order_by(a.c.room_no.asc()).\
    all()

Please note that now input parameters are located inside the check_in , check_out , guests and rooms variables.请注意,现在输入参数位于check_incheck_outguestsrooms变量内。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM