【问题标题】:Lateral Join in django queryset (in order to use jsonb_to_recordset postgresql function)django 查询集中的横向连接(为了使用 jsonb_to_recordset postgresql 函数)
【发布时间】:2020-01-25 10:29:08
【问题描述】:

我有一个模型“myModel”将一些数据保存在名为“json”的(postgresql)jsonField 中, json数据的典型结构是: {key:[{"a":1, "b":2}, {"a":3, "b":4}]}。

我想根据“a”或“b”的值过滤 myModel 查询集。 我可能还想汇总“a”或“b”

因此,非常感谢“取消嵌套” (json -> key) 数组, 但我不知道如何用 django api 做到这一点。

我尝试通过以下 SQL 查询直接在 postgresql 中执行“取消嵌套”。

SELECT * 
FROM "myModel"
join lateral jsonb_to_recordset("myModel"."json" -> 'key') as r("a" int, "b" int) on true
LIMIT 5

我们甚至可以使用横向连接的快捷表示法使其更紧凑

SELECT * 
FROM "myModel", jsonb_to_recordset("myModel"."json" -> 'key') as r("a" int, "b" int)
LIMIT 5

但我不知道如何使用 django API 做一些等效的事情。 我用 annotate 和 RawSQL 尝试了一些东西,但似乎没有一个对“FROM”子句起作用。这是我应该实际添加“jsonb_to_recordset”语句的地方。 我可能可以使用 raw 函数来放置我的原始 SQL,但这意味着我不能使用 django API 在连接的 quesryset 上“过滤”或“聚合”......我必须在 rawSQL 中做所有事情这对我必须做的事情不是很方便。

另一种方法是使用允许在 sql FROM 子句中添加附加表的查询集“额外”功能。 不幸的是,如果我这样做:

qs = myModel.objects.all()
qs = qs.extra(tables = ["""jsonb_to_recordset("myApp_myModel"."json" -> 'key') as r("a" int, "b" int)"""])
qs = qs.values()
print(qs.query)

我得到 django 将执行的查询:

SELECT * 
FROM "myModel", "jsonb_to_recordset("myModel"."json" -> 'key') as r("a" int, "b" int)"

这非常接近我需要的...除了 django 在我提供的额外“表”名称周围添加了额外的引号... 所以这个功能不再起作用了。

知道如何处理这个问题吗?

提前致谢, 逻辑

【问题讨论】:

    标签: python django postgresql


    【解决方案1】:

    尽管你问这个问题已经晚了将近 6 个月,试图将我的贡献添加到你的问题中。

    虽然最近我一直在研究 POSTGRESQL Jsonb 函数以使用 Django ORM 来为 Django 构建强大的报告引擎库,但我搜索并找到了您的问题陈述。似乎我完全陷入了这个 100% 相同的问题。 How could I use JSON array's to apply aggregation/annotation functions for easy reporting to front-end and show graphs and tables?

    经过 3 天的反复试验,我终于在书房的墙壁上摸索,找到了解决问题的方法

    这可能不是一个理想的方式,但我尽量保持理想。还试图避免任何可能的 SQL 注入。让我们不要因为“谈论我的事情”而厌倦我们的时代。


    现在进入下面的实现:

    from django.db.models.constants import LOOKUP_SEP
    from django.db.models.sql.datastructures import BaseTable
    
    
    class JsonbFunction:
        JSONB_TO_RECORDSET = ("jsonb_to_recordset", '#>')
    
    
    class JsonbFunctionTable(BaseTable):
        jsonb_join_type = ("JOIN LATERAL", "ON TRUE")
        function_name = None
        function_alias = None
    
        def __init__(self, table_name, function_name, field_name, columns):
            field_name_seq = field_name.split(LOOKUP_SEP)
            self.model_field = field_name_seq[0]
            self.json_path = field_name_seq[1:]
            alias = self.model_field + 's'
            super(JsonbFunctionTable, self).__init__(table_name=table_name, alias=alias)
            self.function_name = function_name
            column_definitions = list()
            for _c in columns:
                column_definitions.append('{c_name} {c_type}'.format(c_name=_c[0], c_type=_c[1]))
            self.function_alias = '{alias}({column_definitions})'.format(
                alias=alias, column_definitions=','.join(column_definitions))
    
        def as_sql(self, compiler, connection):
            return "{join} {function}({table}.{field} {sign} '{json_path}') {f_alias} {condition}".format(
                join=self.jsonb_join_type[0],
                condition=self.jsonb_join_type[1],
                f_alias=self.function_alias,
                function=self.function_name[0],
                sign=self.function_name[1],
                table=compiler.quote_name_unless_alias(self.table_name),
                field=compiler.quote_name_unless_alias(self.model_field),
                json_path='{' + ",".join(self.json_path) + '}'
            ), []
    
        def relabeled_clone(self, change_map):
            return self.__class__(self.table_name, change_map.get(self.table_alias, self.table_alias))
    
        def equals(self, other, with_filtered_relation):
            return (
                    isinstance(self, other.__class__) and
                    self.table_name == other.table_name and
                    self.table_alias == other.table_alias and
                    self.function_name == other.function_name and
                    self.join_type == other.join_type
            )
    

    说明:

    对于jsonb_to_recordset 函数,它在内部生成一个表,为了得到我们想要的,我们必须将这个函数的返回表与我们的 Django 模型的每个对应行连接起来。为此,唯一的选择是允许 LATERAL JOINjsonb_to_recordset returned table

    JsonbFunctionTable 扩展了 BaseTable 类,允许您使用 django 查询集推送自定义 TABLE JOIN。


    现在让我们跳到下一段代码:

    from django.contrib.postgres.fields import JSONField
    from django.db.models import F, CharField, Expression
    from django.db.models.constants import LOOKUP_SEP
    from django.utils.deconstruct import deconstructible
    
    
    class ForceColumn(Expression):
        """
        Represents the SQL of a column name without the table name.
    
        This variant of Col doesn't include the table name (or an alias) to
        avoid a syntax error in check constraints.
        """
        contains_column_references = True
    
        def __init__(self, target, output_field=None):
            if output_field is None:
                output_field = target
            super().__init__(output_field=output_field)
            self.target = target
    
        def __repr__(self):
            return '{}({})'.format(self.__class__.__name__, self.target)
    
        def as_sql(self, compiler, connection):
            return self.target.db_column, []
    
        def get_group_by_cols(self):
            return [self]
    
        def get_db_converters(self, connection):
            if self.target == self.output_field:
                return self.output_field.get_db_converters(connection)
            return (
                    self.output_field.get_db_converters(connection) +
                    self.target.get_db_converters(connection)
            )
    
    
    @deconstructible
    class ForceF(F):
        model = None
        json_field = None
    
        def __init__(self, model, name, json_field):
            super(ForceF, self).__init__(name)
            self.model = model
            self.json_field = json_field
    
        def resolve_expression(self, query=None, allow_joins=True, reuse=None,
                               summarize=False, for_save=False, simple_col=False):
            _field_ref = self.json_field + 's.' + self.name.replace(LOOKUP_SEP, '.')
            path, final_field, targets, rest = query.names_to_path(
                [self.json_field], query.get_meta(), query.get_initial_alias())
            if not isinstance(final_field, JSONField):
                raise Exception('`ForeF` only available for JSON Fields')
            _dummy_field = CharField(db_column=_field_ref, name=_field_ref)
            _dummy_field.model = self.model
            return ForceColumn(_dummy_field, _dummy_field)
    

    说明

    这里我定义了一个扩展django的F的ForceF表达式函数。这样做的原因是,在 Query 中,您必须使用动态生成的 JSONField 的“您的价值路径”来选择/order_by/group_by。但正如预期的那样,Django 不允许您这样做,因为 Django 将尝试使用您的“动态生成的 JSON 列名称”和常规可用的 Django 字段进行验证。那会给你又一次堵塞的打击。 ForeceF 就是来解决这个问题的。它还通过强制生成表的命名约定规则来处理有意可能的 SQL 注入。


    以下是您最终将如何使用 Django 查询集:

    results = myModel.objects.filter()
    # Adding extra JOIN for JSONb
    join_config = JsonbFunctionTable(
        table_name=myModel._meta.db_table,
        function_name=JsonbFunction.JSONB_TO_RECORDSET,
        field_name='json__key', # Your JSON Field and path to that array
        columns=[('a', 'int'), ('b', 'int')]
    )
    results.query.join(join=join_config)
    # Group by as your need
    results = results.values('group_by_somethings')
    # Now finally force annotate with your dynamically generated JSON Columns
    results = results.annotate(
        jsonb_annotated=ForceF(model=myModel, name='a', json_field='json')
    )
    results = results.values("value_1", "value_2", "jsonb_annotated")
    # Check generated query
    print(results.query)
    # Check generated result
    print(results.query)
    

    脚注:

    到目前为止,这是我为满足我的需要而构建的。以后可能会产生不良后果,但现在它正在按预期工作,以满足我的需要。所以,也许它不能满足你的确切要求,但我很确定这个实现可以扩展来解决你提到的问题。

    而谁读到这个疯狂或可能是坏主意,一定不要忘记向我们表达您的担忧。

    【讨论】:

      猜你喜欢
      • 2019-01-15
      • 2020-08-25
      • 2019-02-05
      • 1970-01-01
      • 2021-02-21
      • 2013-06-26
      • 2020-07-17
      • 2019-08-05
      • 2020-05-16
      相关资源
      最近更新 更多