2
votes

Django JSONField filtering Queryset où la valeur du filtre est la valeur de somme annotée

Comment écrire correctement le code du filtre pour qu'il ne renvoie que les animaux qui ne sont pas épuisés.

J'utilise POSTGRES db, python3.6 et Django 2.1.7 (actuellement il y a v2.2a1, v2 .2b1 pré-versions)

Ma quête est une extension du Filtrage Django JSONField qui filtre sur une valeur codée en dur dans le filtre.

My Case nécessite une valeur annotée dans le filtre.

models.py Je sais que les modèles peuvent être optimisés, mais j'ai déjà énormément de records depuis plus de 3 ans

q = q.filter(data__contains={'count__gt':JSONF('sold_count_sum')})
# err: Object of type 'JSONF' is not JSON serializable

q = q.filter(sold_count_sum__lt=Cast(JSONF('data_count'), IntegerField()))
# err: operator does not exist: text ->> unknown

q = q.filter(sold_count_sum__lt=Cast(JSONF('data__count'), IntegerField()))
# err: 'KeyIntegerTransform' takes exactly 1 argument (0 given)

q = q.filter(sold_count_sum__lt=KeyIntegerTransform('count', 'data'))
# err: operator does not exist: text ->> unknown

q = q.filter(sold_count_sum__lt=F('data__count'))
# err: operator does not exist: text ->> unknown

q = q.filter(sold_count_sum__lt=F('data_count'))
# err: operator does not exist: text ->> unknown

q = q.filter(sold_count_sum__lt=JSONF('data_count'))
# err: operator does not exist: text ->> unknown

q = q.filter(sold_count_sum__lt=JSONF('data__count'))
# err: 'KeyIntegerTransform' takes exactly 1 argument (0 given)

q = q.filter(sold_count_sum__lt=JSONF('data', 'count'))
# err: JSONF.__init__() takes 2 params


dans mon api je souhaite renvoyer uniquement les animaux qui ont encore quelque chose à vendre

from django.db.models.constants import LOOKUP_SEP
from django.db.models import F, Q, Prefetch, Sum
from django.db.models import IntegerField, FloatField, ExpressionWrapper
from django.db.models.functions import Cast
from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.fields.jsonb import KeyTransform, KeyTextTransform

class KeyIntegerTransform(KeyTransform):  # similar to KeyTextTransform
    """ trasnform the data.count to integer """
    operator = '->>'
    nested_operator = '#>>'
    output_field = IntegerField()

class KeyIntTransformFactory:
    """ helper class for the JSONF() """

    def __init__(self, key_name):
        self.key_name = key_name

    def __call__(self, *args, **kwargs):
        return KeyIntegerTransform(self.key_name, *args, **kwargs)


class JSONF(F):
    """ for filtering on JSON Fields """

    def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
        rhs = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
        field_list = self.name.split(LOOKUP_SEP)
        for name in field_list[1:]:
            rhs = KeyIntegerTransform(name)(rhs)
        return rhs


Ce que je veux filtrer doit être similaire à animal.data ['count']> sum (animal.sales_set__count

Animal.objects.annotate(animals_sold=Sum('sales_set__count'))
.filter(data__contains=[{'count__gt': F('animals_sold')}])

avec le code ci-dessus, j'obtiens builtins.TypeError TypeError: L'objet de type 'F' n'est pas sérialisable JSON

si je supprime le F il ne filtrera pas sur la valeur de animals_sold, mais sur le envoyez "animals_sold" et cela n'aide pas.

Animal.objects.annotate(animals_sold=Sum('sales_set__count'))
.filter(data__contains=[{'count__gt': F('animals_sold')}])

Édition 1: Il y a un autre sujet ici qui peut être lié: Postgres: requête de valeurs sur la clé json avec django p >

Modifier 2: voici un code supplémentaire avec des classes de transformation personnalisées comme suggéré dans le ticket django associé

animal = Animal(data={'type':'dog', 'bread':'Husky', 'count':20})

filtrage des ensembles de requêtes que j'ai essayé jusqu'à présent:

from django.db import models
from django.contrib.postgres.fields import JSONField

class Animal(models.Model):
    data = models.JSONField(verbose_name=_('data'), blank=True)

class Sell(models.Model):
    count = models.IntegerField(verbose_name=_('data'), blank=True)
    animal = models.ForeignKey('Animal', 
                               on_delete=models.CASCADE,
                               related_name="sales_set",
                               related_query_name="sold"
   )


0 commentaires

3 Réponses :


1
votes

La classe F ne prend pas en charge un JSONField pour le moment, mais vous pouvez essayer de créer votre propre expression personnalisée comme décrit dans le ticket associé .


5 commentaires

Est-ce que je vous comprends bien? Voulez-vous dire que je ne peux utiliser que la requête SQL pure?


Non, vous pouvez également créer votre propre expression personnalisée comme expliqué dans le commentaire d'alst dans le code du ticket . djangoproject.com/ticket/29769#comment:5


Je ne connais pas assez bien le fonctionnement de cette partie de django, donc je n'arriverai pas à créer ma propre fonction JSONF. J'envisage de refactoriser les modèles et de transférer les données du champ de données vers les champs de modèle. btw, quelle est la valeur du const LOOKUP_SEP ? dans le commentaire là-bas?


githubjango.com/django/blango/ …


J'ai ajouté plus de code dans ma question. il semble qu'un petit changement dans le JSONF devrait faire l'affaire, pouvez-vous s'il vous plaît vérifier ce qui ne va pas?



1
votes

Que diriez-vous de quelque chose comme ceci:

from django.db.models import Sum, F
from django.contrib.postgres.fields.jsonb import KeyTransform

Animal.objects.annotate(animals_sold=Sum('sales_set__count'), data_count=KeyTransform('count', 'data')).filter(data_count__gt=F('animals_sold'))


2 commentaires

J'ai déjà essayé et je viens de le réessayer, mais j'obtiens ProgrammingError : l'opérateur n'existe pas: jsonb> bigint LIGNE 1: ... unt ') HAVING ("my_secret_app_animal". "data" -> 'count')> (SUM ("fi ...


C'est presque la même chose si le data.count est float. pouvez-vous suggérer un Cast fonctionnel?



4
votes
        queryset = Animal.objects.annotate(
            sold_count_sum = Sum('sold__count'),
            sold_times = Count('sold'),
        ).filter(
            Q(sold_times=0) | Q(sold_count_sum__lt=Cast(
                 KeyTextTransform('count', Cast(
                     F('data'), JSONField())), IntegerField()
                 )
            ),
            # keyword filtering here ...
            # client = client
        )

3 commentaires

Oui, j'ai aussi brièvement joué avec et j'ai trouvé à peu près la même chose, ce n'est pas une mauvaise solution dont vous avez toujours besoin pour convertir les valeurs du JSON. Vous n'avez probablement pas besoin d'annoter le json si vous utilisez KeyTextTransform ('count', 'data') .


J'obtiens unhashable type: 'list' si j'utilise KeyTextTransform ('count', 'data') au lieu de 'json' comme dans sold_count_sum__lt = Cast (KeyTextTransform ('count', KeyTextTransform ( 'count', 'data')), IntegerField ()) , mais en utilisant Cast (KeyTextTransform ('count', Cast (F ('data'), JSONField ())), IntegerField () ) fonctionne bien


Je ne sais pas si cela peut être utile pour le billet que vous avez mentionné ici.