【问题标题】:Django - multiple joins on same table - incorrect result?Django - 同一张表上的多个连接 - 结果不正确?
【发布时间】:2016-04-05 07:39:25
【问题描述】:

我有以下型号:

class Document(models.Model):
    ...

class DocumentAttributes(models.Model):
    document = models.ForeignKey(Document)
    key = models.TextField()
    value = models.TextField()

我想根据属性查询文档。指定的键必须与其中一个值匹配。

最好举个例子:

    self.d1 = document_factory(attributes={'a': '1', 'b': '1'})
    self.d2 = document_factory(attributes={'a': '2', 'b': '2'})
    self.d3 = document_factory(attributes={'a': '2', 'b': '1'})
    self.d4 = document_factory(attributes={'a': '3', 'b': '4'})
    self.d5 = document_factory(attributes={'a': '3', 'b': '2'})
    self.d6 = document_factory(attributes={'a': '1', 'b': '4'})
    self.d7 = document_factory(attributes={'a': '2', 'b': '4'})

 docs = whitelist_keyvalue_in({'a': ['1', '3'], 'b': ['1', '4']}, doc_qs).all()

文档现在应该包含 d1、d4、d6。

这是我的实现:

def whitelist_keyvalue_in(json_obj, doc_qs):
    qs = doc_qs
    for key in json_obj:
        values = [json_obj[key]] if isinstance(json_obj[key], basestring) else json_obj[key]
        q_values = Q()
        for v in values:
            q_values |= Q(value=v)
        qs = qs.filter(attributes=DocumentAttributes.objects.filter(key=key).filter(q_values))
    print(qs.query)
    return qs

由于某种原因,这只返回 d1?并且生成的查询并不完全漂亮。

你能发现任何错误吗?有没有更好的写法?

SELECT ... FROM "document_document"
INNER JOIN "document_documentattributes" ON ("document_document"."id" = "document_documentattributes"."document_id")
INNER JOIN "document_documentattributes" T3 ON ("document_document"."id" = T3."document_id")
WHERE
("document_documentattributes"."id" = ( SELECT U0."id"
                       FROM "document_documentattributes" U0
                       WHERE (U0."key" = 'a' AND (U0."value" = '1' OR U0."value" = '3')))
                       AND T3."id" = ( SELECT U0."id"
                                FROM "document_documentattributes" U0
                                WHERE (U0."key" = 'b' AND (U0."value" = '1' OR U0."value" = '4'))))

如果我自己使用原始查询,一切正常:

def whitelist_keyvalue_in(json_obj, doc_qs):
    names = {key: 'da{}'.format(k_index) for k_index, key in enumerate(json_obj)}
    raw_sql = "SELECT da0.document_id as id FROM document_documentattributes as da0 "
    for key in json_obj:
        if names[key] == 'da0':
            continue
        raw_sql += ("JOIN document_documentattributes as {0} ON {0}.document_id = da0.document_id "
                    "".format(names[key]))
    for key in json_obj:
        where_and = 'WHERE' if names[key] == 'da0' else ' AND'
        values = [json_obj[key]] if isinstance(json_obj[key], basestring) else json_obj[key]
        values_opts = ' OR '.join("{}.value = '{}'".format(names[key], value) for value in values)
        raw_sql += "{} {}.key = '{}' AND ({})".format(where_and, names[key], key, values_opts)
    return doc_qs.filter(id__in=(d.id for d in doc_qs.raw(raw_sql)))

给出:

SELECT da0.document_id as id 
FROM document_documentattributes as da0 
JOIN document_documentattributes as da1 ON da1.document_id = da0.document_id 
WHERE da0.key = 'a' AND (da0.value = '1' OR da0.value = '3') 
  AND da1.key = 'b' AND (da1.value = '1' OR da1.value = '4')

SELECT ... FROM "document_document" WHERE "document_document"."id" IN (1, 4, 6)

我宁愿避免使用 id__in,但不知道如何从原始查询集转换为常规查询集。

如果我必须为此使用原始 sql,有没有办法可以避免两个选择返回正常的查询集?

【问题讨论】:

  • 为什么要避免使用`id__in`过滤器?
  • @MevinBabu - 我已经有了我关心的文件列表。 id__in 执行第二个不必要的查询,这也可能是一个慢查询,具体取决于有多少文档。
  • 你的意思是说你已经有一个查询返回一个文档列表,然后你想再次过滤这个列表以返回与属性匹配的文档?因此你不想做两个不同的查询?对吗?
  • 但是您的 whitelist_keyvalue_in 函数无论如何都会向数据库发起新查询?
  • @MevinBabu - 不完全是。 QuerySets 存储它执行 SQL 查询所需的信息——它不会访问数据库,直到它必须这样做。当我使用原始 sql 查询时,它会立即访问数据库,并给出一个 PK:s 列表。但是,我想返回一个 QuerySet(便于后续操作)。在给定 PK 列表的情况下,我可以返回 QuerySet 的唯一方法是执行 id__in。现在我总是会击中分贝两次。我希望有某种方法可以将原始 sql 提供给 PK 并将其转换为 QuerySet 而无需访问 db 来获取 PK(即 Django 会将其用作 sql 子查询)。希望澄清。

标签: sql django django-orm


【解决方案1】:

你的

qs = qs.filter(attributes=DocumentAttributes.objects.filter(key=key).filter(q_values)) 在 for 循环内,每次应用过滤器时,它的行为都类似于 and 条件。因此,最终的查询将是使用a is 1 or 3 and b is 1 or 4 获取文档。这里 d1 匹配条件并返回,因为它有 a = 1b = 1

在每个键周围使用Q()| 应该可以解决这个问题。

这行qs = qs.filter(attributes=DocumentAttributes.objects.filter(key=key).filter(q_values)) 将触发两个查询,因为DocumentAttributes.objects.filter(key=key).filter(q_values) 将被评估以用于qs 查询

【讨论】:

  • 您的分析不正确。注意: qs.filter 返回一个新的过滤器,它是迭代地建立一个查询集。您的建议也不正确 - 每当查询多个属性时,它都会返回空集。 (请注意,“if key in filter_dic:”永远不会如此。)
  • 这部分DocumentAttributes.objects.filter(key=key).filter(q_values) 将被评估以用于qs.filter 查询
  • 是的,你的权利。我把事情搞混了。我已经编辑了答案。
  • 仍然不正确。 “它的行为就像一个和条件”,这正是我想要的。 “a 是 1 或 3,b 是 1 或 4”也正是我想要的,但请注意,d1 并不是唯一符合此标准的。它确实应该返回:d1、d4、d6。
  • "将触发两个查询,因为"不,它不会。查询集的好处(如我之前所述)是您可以处理抽象查询而无需 将 sql 语句触发到数据库,直到您需要它们为止。例如:qs = Documents.objects.all()。不会执行任何查询。如果我然后执行 qs2 = qs.filter(something=something) - 它仍然不会执行任何查询。只有当我实际获取数据时才会发送查询。例如 doc = qs2.all()[0].
【解决方案2】:

OK 终于找到了正确的方法。生成的原始查询甚至很好看。

def whitelist_keyvalue_in(attributes, doc_qs):
    qs = doc_qs
    for key, values in attributes.iteritems():
        values = [values] if isinstance(values, basestring) else values
        qs = qs.filter(attributes__key=key, attributes__value__in=values)
    return qs

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-05-18
    • 2015-07-24
    • 1970-01-01
    • 2014-08-17
    • 1970-01-01
    • 2016-03-07
    • 2011-10-18
    相关资源
    最近更新 更多