Python django queryset中的横向连接(为了使用jsonb_to_记录集postgresql函数)

Python django queryset中的横向连接(为了使用jsonb_to_记录集postgresql函数),python,django,postgresql,Python,Django,Postgresql,我有一个模型“myModel”,将一些数据保存在一个名为“json”的(postgresql)json字段中, json数据的典型结构是: {键:[{a:1,b:2},{a:3,b:4}]} 我想根据“a”或“b”的值筛选myModel queryset。 我可能还想合计“a”或“b” 因此,“取消测试”(json->key)数组将非常感谢, 但我不知道如何使用DjangoAPI实现这一点 我试图通过下面的SQL查询直接在postgresql中执行“取消测试” SELECT * FROM "m

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

我想根据“a”或“b”的值筛选myModel queryset。 我可能还想合计“a”或“b”

因此,“取消测试”(json->key)数组将非常感谢, 但我不知道如何使用DjangoAPI实现这一点

我试图通过下面的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
但是我不知道如何使用djangoapi做一些等效的事情。 我尝试过annotate和RawSQL的一些方法,但似乎没有一种是根据“FROM”子句进行操作的。这是我应该实际添加“jsonb_to_recordset”语句的地方。 我可能可以使用raw函数来放置我的原始SQL,但这意味着我不能使用djangoapi对加入的quesryset进行“过滤”或“聚合”。。。。我必须在rawSQL中做所有的事情,这对于我必须做的事情来说不是很方便

另一种方法是使用queryset“extra”函数,该函数允许在SQLFROM子句中添加额外的表。 不幸的是,如果我这样做:

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在我提供的额外“表”名称周围添加了额外的引号。。。 所以这个函数不再起作用了

你知道怎么处理吗

提前感谢,,
Loic

尽管你问起这个问题已经晚了将近6个月,仍试图将我的观点加入你的问题。

最近,为了为Django构建一个功能强大的报告引擎库,我一直在研究使用Django ORM的POSTGRESQL Jsonb函数,我搜索并找到了您的问题陈述。我似乎完全陷入了这个100%相同的问题。
如何使用JSON数组应用聚合/注释功能,以方便向前端报告并显示图形和表格?

经过三天的反复试验,我终于找到了解决这个问题的方法

这可能不是一个理想的方式,但我尽量保持它的理想。还试图避免任何可能的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
        )
说明:

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)
对于
jsonb_to_recordset
函数,它在内部生成一个表,为了得到我们想要的,我们必须将该函数的返回表与Django模型的每个对应行连接起来。要做到这一点,唯一的选择是允许使用
jsonb_To_记录集返回的表进行
横向连接

JsonbFunctionTable
扩展了BaseTable类,该类允许您使用django queryset推送自定义表联接


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

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)
解释

这里我定义了一个ForceF表达式函数,它扩展了django的
F
。这样做的原因是,在查询中,您必须使用动态生成的JSONField的“值路径”选择/order\u by/group\u 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)
脚注:

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)
到目前为止,这是我为满足自己的需要而构建的。以后可能会有不好的后果,但现在它的工作正如我所期望的那样。所以,也许它不能满足您的确切需求,但我非常确定这个实现是可扩展的,可以解决您提到的问题

无论是谁读到这个疯狂的或可能是坏主意,一定不要忘了把你的关心放在我们身上