【问题标题】:Filter/order django rest api on a property=value but property is a value instead of a field在属性 = 值上过滤/排序 django rest api,但属性是值而不是字段
【发布时间】:2020-01-21 14:47:41
【问题描述】:

考虑一个具有多个属性的对象。

class Object(models.Model):
    name= Charfield(max_length=80)

class Property(models.Model):
    object= ForeignKey(Object, related_name='properties')
    name= Charfield(max_length=80)
    value= Charfield(max_length=80)

我想为对象模型提供一个 api 可访问的列表视图,但我无法按属性值对 ObjectList 进行排序,也无法按属性值对 ObjectList 进行排序。

为了使示例更具体,您可以想象 Object 具有 book 类型,并且 book 对象除了其他属性外,还具有名称为 title 的属性和诸如 20,000 Leagues Under the Sea 之类的值。我希望能够指定?order_by=title 并能够过滤?title__contains=League(或至少?title="20,000 Leagues Under the Sea"

我已经尝试使用 title="20,000 Leagues Under the Sea" 注释 Object,并且我能够在 ObjectSerializer 中显示标题,就好像它是一个字段一样,但是按此字段排序不起作用。

【问题讨论】:

  • 您能在此处提供您的视图集示例吗?

标签: django django-models django-rest-framework django-views


【解决方案1】:

考虑您要完成什么以及它如何映射到 SQL 查询(使用 ORM 很方便,但您必须始终牢记,您的所有 ORM 调用都会转换为 SQL)。你有一张桌子books,你有一张桌子properties。让我们说

create table books (
    id integer primary key,
);

create table properties (
    id integer primary key,
    name text,
    value text,
    book_id integer,
)

然后你像这样插入记录

insert into books set (id) values (1);
insert into properties set (id, name, value, book_id) values (1, 'title', 'A Book', 1)

然后你想提取书籍和标题,所以你最终会得到这样的东西

select b.id, p.value as "title"
from books as b
join properties as p on (b.id = p.book_id)
where p.name = 'title'
order by p.value

不幸的是,使用 ORM 构建上述查询并非易事。这样做的原因是,通过将属性名称和值都作为数据引入,而不是将数据写入特定字段,您正在颠覆您引用特定属性的能力。您不能简单地将书名称为特定字段,因为它不是一个字段。如果这本书没有“标题”财产记录怎么办?如果它有多个呢?

你可以使用 ORM API 中的 Subquery() 函数做你想做的事,但我从未亲自使用过它,所以我不确定它的确切语法是什么。大概是这样的

title = Property.objects.filter(name='title', book_id=OuterRef('pk')).values('value')[:1]
books = Object.objects.annotate(title=Subquery(title)).order_by('title')

https://docs.djangoproject.com/en/2.2/ref/models/expressions/#subquery-expressions

也就是说,这很脆弱且难以推理,您可能需要重新考虑您的数据库架构。

【讨论】:

  • 嗨,我确实在寻找解决方案时使用了子查询。我得出了和你类似的结论。你能详细说明它是如何脆弱的吗?
  • 如果您有一个字段,例如标题,并针对它编写查询,您可以非常确定至少该字段存在并包含一个值。如果您的约束允许,该值可能为 null,但它将是一个值。在您的情况下,您不能保证该字段完全存在。您可以设置阻止多个标题的约束,但您不能强制一本书有一个标题。所以现在你必须格外小心你的查询,使用正确的连接类型等。
【解决方案2】:

你所拥有的看起来像一个标准的EAV 模型。

您可能会考虑查看像 django-eav2 这样的包,看看是否有帮助。我不认为它处理排序,这是明智的,因为它实际上很难通过 ORM 完成。

您是否考虑过将模型更改为使用 JSON 列或 HStore(如果您使用的是 postgres)?根据您的数据模型,它们可以完美地工作,并且也很容易被索引、搜索和排序。

如果你想自己做,你需要写一些东西:

  • 帮助函数按属性表中的字段进行过滤和排序
  • 自定义OrderingBackend 实现
  • 自定义FilterField 以处理按属性过滤
  • 自定义DjangoFilterBackend,具体取决于您希望网址的行为方式

如果您可以提供一个更好的示例来说明您希望 URL 的外观,那么提供一些指导会更容易。由于您的属性可以有任何名称,因此您可以做出一些选择:

  1. url参数是属性名吗(例如?title=)
  2. 您是否使用自定义 url 格式,例如?attr=title&value=The Big Bang
  3. 是否需要在Object 级别进行过滤?
  4. 您能否一次过滤/排序不止一件事?
  5. 如何处理不同的数据类型(日期/整数/字符串/等?)

抱歉,这个问题太大了,无法在这里单独回答。尽管如果你足够努力的话是有可能的,但构建 EAV 查询并不容易,而且它们很快。

【讨论】:

  • 您好,您提出的自定义 url 格式是我弄清楚如何在 api 级别解决问题的关键。我能够使用attr=title 来注释查询集,然后我可以在所有现有的 OrderingBackends 和 Filterfields 中使用值。 EAV 的链接也很有用!它帮助我改进并记录了我必须做出的一些设计选择!
【解决方案3】:

假设您有以下型号:

class Object(models.Model):
    name= Charfield(max_length=80)

class Property(models.Model):
    object= ForeignKey(Object, related_name='properties')
    name= Charfield(max_length=80)
    value= Charfield(max_length=80)

那么您的Object 实例只有一个属性,即name='title'

在这种情况下,我会选择类似的东西:

Object.objects.filter(
    properties__name=Value('title'), 
    properties__value__icontains=request.data['title']
    )

用于过滤。

关于排序,您会遇到另一个问题:您尝试按 Many2One 关系的字段(一个对象的许多属性)进行排序,但仅在此关系中的一个相关对象上排序...建议的子查询在this answer 中可能是要走的路,但我个人已经尝试过这样做但没有成功。

This answer 指出你的结构很难处理。实际上,在您的情况下,假设您在某个时候将您的书籍的作者添加为 Object 实例,这些对象将根本没有 title 属性...

如果您不能放弃此数据库架构,我建议您查看JSON fieldsHStore fields。 DjangoORM 提供了根据您尝试实现的内容查询这些字段的方法。但是,请注意,这仍然会比使用传统架构的 RDBMS 慢。

否则,创建一个“真实的”Book 模型,它有一个标题,在这种情况下,一切都会简单得多;)

编辑:您也可以直接在 SQL 中编写查询,可以直接从cursor 执行(请参阅here)或从模型的查询集(请参阅here)执行。我在各种情况下都使用了这两种方法,您必须小心处理用户输入的方式,但它应该可以工作 =)

【讨论】:

  • “书本”模型可能是一个不错的例子,但选择其他架构还有其他原因。属性比字段复杂一些,因为我们希望允许用户自己设计属性。 JSON 和 HStore 也不理想,因为我需要将字段作为单独的对象,并带有指向元字段和多个测试的链接。替代架构更难做到这一点。
  • 根据我对您的用例的了解,我个人会选择在 SQL 中手动进行过滤/排序。我为自己的东西求助于这个,我在几个字段上使用全文搜索,具体取决于用户是否提供了在字段中搜索的输入......如果你不害怕 SQL,那可能是最直接的-正向解决方案
【解决方案4】:

您可以重新定义方法get_queryset(),该方法准备对数据库的查询,并可以按您提供的字段(-s)为您进行排序。
看看我提供的例子。不确定它是否开箱即用,但希望您理解其中的逻辑。但如果您有任何问题 - 请添加评论。

class ObjectViewSet(ModelViewSet):

    def get_queryset(self):
        queryset = super(ObjectViewSet, self).get_queryset()

        order_by = self.request.query_params.get('order_by', '')
        if order_by:
            order_by_name = order_by.split(' ')[1]
            order_by_sign = order_by.split(' ')[0]
            order_by_sign = '' if order_by_sign == 'asc' else '-'
            queryset = queryset.order_by(order_by_sign + order_by_name)

        return queryset

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2011-09-21
  • 2022-08-20
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-07
  • 1970-01-01
相关资源
最近更新 更多