简体   繁体   中英

Using Postgres index operator in SQLAlchemy

Given a table with the following schema:

create table json_data (
    id integer PRIMARY KEY NOT NULL,
    default_object VARCHAR(10) NOT NULL,
    data jsonb NOT NULL
);

For each of entity in the table I want to retrieve value of data['first']['name'] field, or if it's null value of data[json_data.default_object]['name'] , or if the latter is also null then return some default value. In "pure" SQL I can write the following code to satisfy my needs:

insert into
  json_data(
    id,
    default_object,
    data
  )
  values(
    0,
    'default',
    '{"first": {"name": "first_name_1"}, "default": {"name": "default_name_1"}}'
  ),
  (
    1,
    'default',
    '{"first": {}, "default": {"name": "default_name_2"}}'
  );

select
  id,
  coalesce(
    json_data.data -> 'first' ->> 'name',
    json_data.data -> json_data.default_object ->> 'name',
    'default_value'
  ) as value
from
  json_data;

I tried to "translate" the "model" above into an SQLAlchemy entity:

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property


Base = declarative_base()


class JsonObject(Base):
    __tablename__ = 'json_data'

    id = sa.Column(sa.Integer, primary_key=True)
    default_object = sa.Column(sa.String(10), nullable=False)
    data = sa.Column(postgresql.JSONB, nullable=False)

    @hybrid_property
    def name(self) -> str:
        obj = self.data.get('first')
        default_obj = self.data.get(self.default_object)
        return (obj.get('name') if obj else default_obj.get('name')) or default_obj.get('name')

    @name.setter
    def name(self, value: str):
        obj = self.data.setdefault('first', dict())
        obj['name'] = value

    @name.expression
    def name(self):
        return sa.func.coalesce(
            self.data[('first', 'name')].astext,
            self.data[(self.default_object, 'name')].astext,
            'default_value',
        )

But it seems that expression for the name hybrid property doesn't work as I expect. If I query entities by name property, like:

query = session.query(JsonObject).filter(JsonObject.name == 'name')

The query is expanded by SQLAlchemy into a something like this:

SELECT json_data.id AS json_data_id, json_data.default_object AS json_data_default_object, json_data.data AS json_data_data 
FROM json_data 
WHERE coalesce((json_data.data #> %(data_1)s), (json_data.data #> %(data_2)s), %(coalesce_1)s) = %(coalesce_2)s

It uses path operator instead of index operator. What should I do to make SQLAlchemy create an expression such as I wrote in the beginning of the question?

Ok, the solution I found is quite straightforward. As SQLAlchemy documentation tells:

Index operations return an expression object whose type defaults to JSON by default, so that further JSON-oriented instructions may be called upon the result type.

Therefore we can use "chained" python indexing operators. So the following code looks legit to me:

class JsonObject(Base):
    # Almost the same stuff, except for the following:
    @name.expression
    def name(self):
        return sa.func.coalesce(
            self.data['first']['name'].astext,
            self.data[self.default_object]['name'].astext,
            'default_value',
        )

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