简体   繁体   中英

Raw SQL to Sqlalchemy hybrid expression

I have this "complex" (complex to convert it to sqla core) postgres query which calculates a score by different factors and tables. I had the score as an aggregated value but I prefer having it stateless, thus I'm trying to define it as a hybrid property and an expression. The hybrid property was easy, did some simple math operations and conditionals with python and voila.

Is there any chance to achieve that without using sqlalchemy core expressions, just raw SQL? If yes that would be great for future operations too as raw, not so dynamic, SQL query is much easier to write than using sqla core functions. If no, I'd appreciate if you could point me in the right direction, just to declare the with clause without the inner subqueries in sqla core language.

I tried with the expression bellow but it throws error:

@score.expression
def score(cls):
    raw = text(calc_score_raw_q) # The sql query bellow
    raw.bindparams(product_id=cls.id)
    return db.session.query(ProductModel).from_statement(raw).scalar()

Also executed the query straight from the engine db.engine.execute(raw) but ended up with the same errors.

The error

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'product_id' 

as it seems, hybrid expression's cls.id is an instrumented attribute, therefore it doesn't provide any value.

The SQL Query:

WITH gdp AS (
    SELECT
        SUM(du + de + sc + 1) AS gdp
            FROM (
                SELECT
                    (
                        CASE WHEN description IS NOT NULL THEN
                            0.5
                        ELSE
                            0
                        END) AS "de",
                    (
                        CASE WHEN duration IS NOT NULL THEN
                            0.5
                        ELSE
                            0
                        END) AS "du",
                    (
                        CASE WHEN shipping_cost IS NOT NULL THEN
                            2.0
                        ELSE
                            0
                        END) AS "sc"
                FROM
                    products
                WHERE
                    id = :product_id) AS a
    ),
td AS (
    SELECT
        COUNT(*) AS td
    FROM
        product_degrees
    WHERE
        product_id = :product_id
)
SELECT
    SUM(
        CASE WHEN td <= 50 THEN
            td + gdp
        WHEN td <= 200 THEN
            gdp + 50 + (td - 50) * 1.5
        ELSE
            gdp + 275 + (td - 200) * 2
        END) AS gd_points
FROM
    td,
    gdp

I managed to solve it by reading throughly SQLA's core documentation. The code is cumbersome but it works. I replicated the exact same raw SQL query as my original post

@score.expression
def score(cls):
    # Case clauses
    desc = case([
        (ProductModel.description != None, 0.5),
    ], else_=0).label('desc')

    ship_cost = case([
        (ProductModel.shipping_cost != None, 2),
    ], else_=0).label('ship_c')

    dur = case([
        (ProductModel.duration != None, 0.5)
    ], else_=0).label('dur')

    # ibp = item body points, SELECT ( CASE ... as a)
    ibp_q = select([desc, ship_cost, dur]) \
        .where(ProductModel.id == cls.id).alias()

    ibp_q = select([func.sum(
        ibp_q.c.desc + ibp_q.c.dur + ibp_q.c.ship_c + 1
    ).label('gdp')]).cte('attr_points')

    # total degrees, SELECT COUNT(*) as td ...
    td_q = select([func.count(product_degrees.c.deal_id).label('total')]).where(product_degrees.c.deal_id == cls.id).cte('total_degrees') # cte creates the aliases inside "with" clause

    # total score logic, SELECT SUM( CASE WHEN td <= 50 ....
    total_q = func.sum(case(
        [
            (td_q.c.total <= 50, td_q.c.total + ibp_q.c.gdp),
            (td_q.c.total <= 200, 50 + ibp_q.c.gdp + (td_q.c.total - 50) * 1.5),
        ], else_=275 + ibp_q.c.gdp + (td_q.c.total - 200) * 2
    )).label('total_points')

    # Construct the whole query. select_from assigns the aliases and renders a "with" clause
    q = select([total_q]).select_from(td_q).select_from(ibp_q).as_scalar()
    return q

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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