1
votes

Django-filter: filtrage par propriété de modèle

J'ai lu sur plusieurs place qu'il n'est pas possible de filtrer les ensembles de requêtes Django à l'aide de propriétés car Django ORM n'aurait aucune idée de comment les convertir en SQL.

Cependant, une fois les données récupérées et chargées en mémoire, il doit être possible de les filtrer en Python en utilisant ces propriétés.

Et ma question: existe-t-il une bibliothèque permettant de filtrer les ensembles de requêtes par propriétés en mémoire? Et sinon, comment les jeux de requêtes doivent-ils être falsifiés pour que cela devienne possible? Et comment y inclure django-filter ?


0 commentaires

4 Réponses :


1
votes

django-filter veut et suppose que vous utilisez des ensembles de requêtes. Une fois que vous prenez un jeu de requêtes et que vous le changez en une liste , tout ce qui est en aval doit pouvoir gérer uniquement une liste ou simplement parcourir la liste, qui n'est plus un ensemble de requêtes.

Si vous avez un django_filters.FilterSet comme:

class MyPropertyFilter(django_filters.Filter):
    def filter(self, qs, value):
        result = [row for row in qs if row.baz == value]
        result.count = len(result)
        return result

alors vous pouvez écrire MyPropertyFilter comme :

class MyPropertyFilter(django_filters.Filter):
    def filter(self, qs, value):
        return [row for row in qs if row.baz == value]

À ce stade, tout ce qui est en aval de MyProperteyFilter aura une liste.

Remarque: je crois l'ordre des champs doit avoir votre filtre personnalisé, MyPropertyFilter en dernier, car il sera toujours traité après les filtres normaux de l'ensemble de requêtes.


Donc, vous venez de casser l'API "queryset", pour certaines valeurs de cassé. À ce stade, vous devrez résoudre les erreurs de tout ce qui se trouve en aval. Si tout ce qui se trouve après le FilterSet nécessite un membre .count , vous pouvez changer MyPropertyFilter comme:

class FooFilterset(django_filters.FilterSet):
    bar = django_filters.Filter('updated', lookup_expr='exact')
    my_property_filter = MyPropertyFilter('property')
    class Meta:
        model = Foo
        fields = ('bar',  'my_property_filter')


4 commentaires

N'est-il pas préférable de renvoyer un QuerySet au lieu d'une liste?


Bien sûr, il est préférable de renvoyer un ensemble de requêtes. Mais vous dites que vous ne pouvez pas. Si vous ne pouvez pas effectuer le filtrage sur la base de données, vous devez effectuer le filtrage par la suite. Un jeu de requêtes est quelque chose qui peut absolument être exécuté sur une base de données. Si vous pouvez modifier votre propriété pour qu'elle soit exécutée d'une manière ou d'une autre sur la base de données, vous évitez complètement cela. Si, cependant, vous devez effectuer un filtrage en python réel, vous avez violé l'essence d'un jeu de requêtes et ce n'est plus un jeu de requêtes.


Désolé, peut-être que je ne me suis pas bien exprimé. Puis-je faire quelque chose comme return QuerySet ([row for row in qs if row.baz == value]) afin qu'il ait toutes les méthodes comme .count , .filter et ainsi de suite? Bien sûr, ils fonctionneraient tous en mémoire et n'atteindraient pas du tout la base de données, ou du moins c'est mon idée. Puis-je faire fonctionner cela d'une manière ou d'une autre?


Il n'y a pas de moyen par défaut de changer la liste en un QuerySet . .count est facile dans la casse de liste, mais qu'en est-il de .filter () ? Django ORM ne sait pas comment faire du filtrage sur une liste , il ne fait du filtrage que sur la base de données. Quoi qu'il en soit, chaque partie de l'API QuerySet , que votre code en aval utilise , devra être implémentée. Si vous commandez les filtres correctement, vous pouvez être en mesure de placer simplement votre filtre QuerySet -> list à la fin de l'ordre des champs .



2
votes

Avez-vous une propriété difficile ou non? Sinon, vous pouvez le réécrire dans un jeu de requêtes comme celui-ci:

from django.db import models

class UserQueryset(models.Manager):

    def get_queryset(self):

        return super().get_queryset().annotate(
            has_profile=models.Exists(Profile.objects.filter(user_id=models.OuterRef('id')))
        )

class User(models.Model):
    objects = UserQueryset


class Profile(models.Model):
    user = models.OneToOneField(User, related_name='profile')


# When you want to filter by has profile just use it like has field has profile

user_with_profiles = User.objects.filter(has_profile=True)

Ce n'est peut-être pas ce que vous voulez, mais cela peut vous aider dans certains cas


3 commentaires

J'aimerais avoir une API générale pour toutes sortes de propriétés, donc oui, j'en aurais peut-être des plus difficiles.


@karloss, je suggérerais que l'API générique est Django ORM lui-même. Presque chaque fois que vous poussez un filtre vers la base de données via Django ORM, cela va être plus rapide que de charger ces données puis de les filtrer vous-même en python. Non pas parce que Python est trop lent, mais parce que vous extrayez plus de données de la base de données au lieu de les filtrer dans la base de données. Ecrire un django_filters.Filter personnalisé pour des propriétés inférées impaires n'est pas si mal.


@karloss, vous pouvez également générer des annotations et des filtres à la volée. Créez de nouveaux filtres, puis un ensemble de filtres à l'aide de Métaclasses . J'ai fait cela avec succès avec Django Rest Framework afin de permettre au client de réduire le nombre de champs qui reviennent à lui-même depuis le serveur principal.



1
votes

Étant donné que le filtrage par attributs hors champ tels que property convertit inévitablement le QuerySet en liste (ou similaire), j'aime le reporter et effectuez le filtrage sur object_list dans la méthode get_context_data . Pour conserver la logique de filtrage dans la classe filterset , j'utilise une astuce simple. J'ai défini un decorator

class MyFiltersetClass(django_filters.FilterSet):
    is_static = django_filters.BooleanFilter(
        method='attr_filter_is_static',
    )

    class Meta:
        model = MyModel
        fields = [...]

    @attr_filter
    def attr_filter_is_static(self, queryset, name, value):
        return [instance for instance in queryset if instance.is_static]

qui est utilisé sur les méthodes de filtrage sans champ django-filter . Grâce à ce décorateur, le filtrage ne fait fondamentalement rien (ou ignore) les méthodes de filtrage sans champ (à cause de la valeur par défaut de force = False ).

Ensuite, j'ai défini un Mixin à utiliser dans la classe view .

class MyFilterView(FilterByAttrsMixin, django_filters.views.FilterView):
    ...
    filterset_class = MyFiltersetClass
    ...

Il revient simplement à votre filterset code > et exécute toutes les méthodes appelées attr_filter_ , cette fois avec force=True.

En résumé, vous devez:

  • Héritez de la classe FilterByAttrsMixin dans votre classe view
  • appelez votre méthode de filtrage attr_filter_
  • utilisez le décorateur attr_filter sur la méthode de filtrage

Exemple simple (étant donné que j'ai model appelé MyModel avec la propriété appelée is_static que je souhaite filtrer par:

modèle:

class MyModel(models.Model):
    ...

@property
def is_static(self):
    ...

vue:

    class FilterByAttrsMixin:
    
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            filtered_list = self.filter_qs_by_attributes(self.object_list, self.filterset)
            context.update({
                'object_list': filtered_list,
            })
            return context
    
        def filter_qs_by_attributes(self, queryset, filterset_instance):
            if hasattr(filterset_instance.form, 'cleaned_data'):
                for field_name in filter_instance.filters:
                    method_name = f'attr_filter_{field_name}'
                    if hasattr(filterset_instance, method_name):
                        value = filterset_instance.form.cleaned_data[field_name]
                        if value:
                            queryset = getattr(filterset_instance, filter_method_name)(queryset, field_name, value, force=True)
            return queryset


0 commentaires

0
votes

Jetez un œil au package django-property-filter . Il s'agit d'une extension de django-filter et fournit des fonctionnalités pour filtrer les ensembles de requêtes par propriétés de classe. Petit exemple de la documentation:

from django_property_filter import PropertyNumberFilter, PropertyFilterSet

class BookFilterSet(PropertyFilterSet):
    prop_number = PropertyNumberFilter(field_name='discounted_price', lookup_expr='gte')

    class Meta:
        model = NumberClass
        fields = ['prop_number']


0 commentaires