Django-tables2: Lignes de tableau dupliquées après tri par en-tête de colonne

Créé le 3 août 2016  ·  32Commentaires  ·  Source: jieter/django-tables2

Je rencontre un problème où certaines lignes sont rendues plusieurs fois à la place d'autres lignes. La table en question rendait parfaitement bien sur les versions antérieures de django et django-tables2. Je les ai mis à niveau tous les deux en même temps vers la dernière version de chacun (django 1.10, django-tables2 1.2.4), puis ce problème a commencé à se produire.

Voir cette image ici pour une démonstration : http://imgur.com/a/pPRT8

Lorsque je charge une table sans choisir un ordre de tri par colonne, je ne vois pas ce problème. Une fois que je clique sur un en-tête de colonne pour modifier l'ordre de tri, les lignes sont dupliquées.

Jusqu'à présent, j'ai identifié que les données transmises à l'objet BoundRows sont correctes, sans éléments dupliqués. Cependant, l'itération dans les lignes liées renvoie des entrées dupliquées. Il semble donc que quelque chose puisse changer l'objet de données pendant l'itération.

Ce problème se produit sur des tables de longueur ~ 150, et je n'ai pas (encore) été en mesure de reproduire ce problème avec un ensemble de données de test plus petit.

Cet extrait montre le code de test que j'ai utilisé pour affiner la cause du problème.

--- a/django_tables2/rows.py
+++ b/django_tables2/rows.py
@@ -8,6 +8,7 @@ from django.utils import six
 from .columns.linkcolumn import BaseLinkColumn
 from .utils import A, AttributeDict, call_with_appropriate, computed_values

+import sys

 class BoundRow(object):
     """
@@ -187,9 +188,17 @@ class BoundRows(object):
     def __init__(self, data, table):
         self.data = data
         self.table = table
+        for d in data:
+            print >>sys.stderr, d
+
+        print >>sys.stderr, "end data"
+

     def __iter__(self):
         for record in self.data:
+            print >>sys.stderr, "__iter__", record
             yield BoundRow(record, table=self.table)
bug

Commentaire le plus utile

wohoo, enfin corrigé !

Oui, je sortirai dans une heure.

Tous les 32 commentaires

La cristallisation du jeu de requêtes via list() arrête le problème :

+++ b/django_tables2/rows.py
@@ -8,6 +8,7 @@ from django.utils import six
 from .columns.linkcolumn import BaseLinkColumn
 from .utils import A, AttributeDict, call_with_appropriate, computed_values

+import sys

 class BoundRow(object):
     """
@@ -187,9 +193,26 @@ class BoundRows(object):
     def __init__(self, data, table):
         self.data = data
         self.table = table


     def __iter__(self):
+        print >>sys.stderr, type(list(self.data.data))

Merci pour le signalement, la conversion vers la liste n'est pas une solution acceptable car cela ne fonctionnera pas avec des ensembles de données plus volumineux.

Pouvez-vous essayer de rétrograder votre version de django-tables2 pour vous assurer que ce n'est pas un problème avec la mise à jour de votre version de Django ?

Oui, ce n'est certainement pas une solution - mais cela peut indiquer la cause. L'autre raison pour laquelle la conversion en liste n'est pas une solution appropriée est qu'elle ne semble pas toujours être un ensemble de requêtes contenant les données de toute façon, auquel cas self.data.data n'existera pas.

@wtfrank , je vois, mais cela aiderait vraiment si vous pouviez fournir un cas de test reproductible. Avez-vous essayé de rétrograder django-tables2 vers la version précédente que vous utilisiez ?

Je pense que je rencontre le même problème : dans ma table, une ligne est manquante et une autre est un doublon... Pour moi, cela a commencé avec la version 1.2.2.
La rétrogradation à 1.2.1 "répare" le problème pour moi... Y a-t-il des informations que je peux fournir pour faciliter la recherche ?

Un cas de test minimal serait vraiment bien d'avoir. Nous pouvons ensuite exécuter git bisect pour trouver le mauvais commit.

J'étudie un cas de test en ce moment, mais curieusement, je ne peux pas reproduire le problème sur mon environnement de test ...

Localement (en utilisant ./manage runserver en utilisant sqlite) tout va bien, alors qu'en production (en exécutant uwsgi et mysql) le problème des dupes existe... (les deux environnements fonctionnent sur des versions égales)

Le jeu de requêtes ne contient aucun dupe.... Mais au moment où le tableau.html est rendu, il manque un objet au jeu de requêtes et un autre est en double...

Je vous reviens bientôt avec un cas de test j'espère :)

Le problème se produit également dans la version 1.2.6. Je pense que c'est qc avec #329 ou #330 mais jusqu'à présent, je n'ai pas trouvé de moyen de le réparer.

Ceux-ci ne s'appliquent qu'à la 1.2.6, donc je ne m'attends pas à ce qu'ils soient la source de ce bogue.

Désolé, je ne suis pas en mesure de fournir un cas reproductible approprié, mais je viens d'avoir ce problème (ou, du moins, je crois que c'est ce problème) et je peux dire que cela s'est certainement produit entre 1.2.1 et 1.2.2 .

Chaque fois que je passe à 1.2.1, tout semble parfait. Lorsque je passe à la version 1.2.2 ou supérieure (j'ai commencé avec la dernière version 1.2.6, bien sûr), je perds systématiquement la première ligne (sur les trois que j'ai dans DB) et j'obtiens une copie d'une autre à la place. Par exemple, quand sur 1.2.1 j'ai :

| date | coups | ... |
| --- | --- | --- |
| 2016-10-08 | 123 | ... |
| 2016-10-07 | 321 | ... |
| 2016-10-06 | 0 | ... |

Sur 1.2.2-1.2.6, j'obtiens systématiquement ceci à la place :

| date | coups | ... |
| --- | --- | --- |
| 2016-10-06 | 0 | ... |
| 2016-10-07 | 321 | ... |
| 2016-10-06 | 0 | ... |

J'utilise Django 1.9.9 sur Python 2.7.12, le projet a été lancé en utilisant pydanny/cookiecutter-django (bien que fortement modifié par la suite), et mon code ressemble à ceci :

class DailySummaryTable(tables.Table):
    class Meta:
        model = DailySummary
        attrs = {"class": "paleblue"}
        empty_text = _("No stats yet")
        fields = ("date", "hits", ...long boring list...)
        # order_by = ("-date",)
        # orderable = False

Le modèle Meta n'a que des éléments verbose_name{,_plural} , et la vue est un simple DetailView qui ressemble à ceci :

class SourceDetailView(LoginRequiredMixin, DetailView):
    model = Source
    ...
    def get_context_data(self, **kwargs):
        ctx = super(...)
        ctx["stats"] = DailySummaryTable(DailySummary.objects.filter(source=self.object))
        return ctx

Et il y a trois lignes dans DB (PostgreSQL 9.6), toutes ayant le même source_id (je n'en ai qu'une), donc elles correspondent toutes à la requête. Bizarrement, si je remplace .filter(source=...) par .all() le problème semble disparaître.

C'est tout ce que j'ai pu comprendre. J'espère que ça aide. Je suppose que je vais m'en tenir à 1.2.1 pour l'instant :)

Merci d'avoir montré vos pérégrinations, j'examinerai ce problème plus tard.

Je viens de rencontrer le même problème, cristalliser le jeu de requêtes via list() n'est pas non plus une solution viable de notre côté.

@op-alex-reid pouvez-vous essayer de transformer votre cas d'utilisation en un cas de test reproductible minimal ?

J'ai le même problème (Django==1.9, django-tables==1.2.3) avec le code suivant :

class UserPlayerTable(tables.Table):
    actions = tables.LinkColumn('player:my_players_detail', args=[A('slug')],
                                text=_('View / Edit'),
                                verbose_name=_('View / Edit'), empty_values=())
    embed = tables.LinkColumn('player:my_players_embed', args=[A('slug')],
                                text=_('View / Embed now'),
                                verbose_name=_('View / Embed now'), empty_values=())

    class Meta:
        template = 'tables/table.html'
        model = Player
        fields = ('created', 'slug', 'actions', 'embed')
        attrs = {"class": "changeset"}
        order_by = ['-created']
        orderable = False

et

UserPlayerTable(Player.objects.filter(user=context['impersonate_user']))

Fait intéressant, lorsque je définis orderable = True , le problème ne se produit pas.

Dans mon cas, cristalliser le jeu de requêtes via list() _est_ une option (et le correctif pour l'instant), car il n'y a que 5 lignes au maximum dans la table.

Merci pour l'exemple !

J'ai essayé de reproduire ceci en utilisant ce code:

class Player(models.Model):
    person = models.ForeignKey(Person)
    created = models.DateTimeField(auto_now_add=True)
    score = models.PositiveIntegerField()

def test_issue_361(per_page=5):

    bob = Person.objects.create(first_name='Bob', last_name='Builder')
    eve = Person.objects.create(first_name='Eve', last_name='Dropper')

    for i in range(10):
        Player.objects.create(person=bob, score=randint(0, 200))
        Player.objects.create(person=eve, score=randint(200, 400))
        Player.objects.create(person=bob, score=5)
        Player.objects.create(person=eve, score=5)

    class UserPlayerTable(tables.Table):
        class Meta:
            model = Player
            fields = ('person.name', 'score',)
            order_by = ['-score']
            orderable = False

    queryset = Player.objects.filter(score=5)

    table = UserPlayerTable(queryset)
    RequestConfig(request, paginate={'per_page': per_page}).configure(table)
    html = table.as_html(request)

    # count the number of rows, subtract one for the header
    assert (html.count('<tr') - 1) == per_page

mais cela ne donne pas de lignes dupliquées...

J'ai rencontré le même problème aujourd'hui. J'utilise django-tables2==1.2.9 dans un projet Django==1.10, où j'utilise également django-filters==1.0.1.

Chose intéressante, le doublement des lignes de ma table ne se produit que si

  1. il y a beaucoup d'entrées à afficher
    (donc si j'active la pagination avec 25 enregistrements par page, tout va bien, mais si j'affiche l'ensemble de données complet, j'obtiens plusieurs entrées identiques)

  2. le tableau est trié par une colonne qui contient des non-chaînes
    (tout va bien si je trie par une colonne qui contient par exemple des noms, mais le tri par colonnes avec des dates, des entiers ou des bools entraîne des problèmes)

La rétrogradation à 1.2.1 résout le problème, mais ce n'est pas vraiment une option.
De plus, les dupes semblent être affichés à la place et non en plus des entrées réelles, car le nombre {{filter.qs.count}} est comme il se doit.

J'espère que cela vous aidera un peu à aller au fond du problème, car à part cela, j'aime vraiment travailler avec django-tables2.

Merci pour tout votre travail!

@n0ctua merci !

Pouvez-vous consulter les requêtes exactes exécutées et peut-être les partager ici ?

Je viens d'avoir ce problème aussi et je comprends pourquoi cela se produit pour moi, alors j'ai pensé poster car je soupçonne que d'autres l'ont pour la même raison. _(Bien que je n'aie pas essayé avec d'autres versions de django-tables2, donc je peux me tromper car je ne comprends pas pourquoi cela empêcherait cela de se produire... Bien que... en y pensant, je suppose que cela est parce que dans les anciennes versions, les tables non paginées exécutaient une seule requête sur la base de données au lieu d'une requête pour chaque ligne - plus de détails à ce sujet ci-dessous...)_

C'est parce que _SQL order by sur des champs non uniques n'est pas déterministe_ donc lorsqu'il est combiné par top n vous obtenez les mêmes n premières lignes encore et encore. (De plus, pour une raison quelconque, lorsque je crée un Table sans pagination, il génère une requête pour _every_ row. IE top 1 ).

Je ne sais pas quelle est la meilleure solution ici? Je pense que la seule façon de garantir que cela fonctionnera est de toujours avoir le dernier order by sur un champ unique. Idéalement la clé primaire ? En outre, cela pourrait-il valoir la peine d'ajouter des notes à la documentation mettant en garde à ce sujet ?

En tant que problème distinct, mais qui réduira vraiment la fréquence à laquelle cela se produit (et accélérera les requêtes pour les tables non paginées), au lieu d'exécuter une requête top 1 pour chaque ligne de la table, il suffirait d'exécuter un seul requête qui récupère tous les résultats.

@intiocean ce nombre de requêtes pour les tables non paginées est assez étrange, je ne sais pas pourquoi cela se produit, et cela ne devrait pas se produire. Ce cas de test montre le problème :

def test_single_query_for_non_paginated_table(settings):
    '''
    A non-paginated table should not generate a query for each row, but only
    one query to count the rows and one to fetch the rows.
    '''
    from django.db import connection
    settings.DEBUG = True

    for i in range(10):
        Person.objects.create(first_name='Bob %d' % randint(0, 200), last_name='Builder')

    num_queries_before = len(connection.queries)

    class PersonTable(tables.Table):
        class Meta:
            model = Person
            fields = ('first_name', 'last_name')

    table = PersonTable(Person.objects.all())
    request = build_request('/')
    RequestConfig(request, paginate=False).configure(table)
    table.as_html(request)

    # print '\n'.join(q['sql'] for q in connection.queries)
    assert len(connection.queries) - num_queries_before == 2

J'ai essayé, sans succès, de créer un test pour le cas paginé qui échoue. Je ne comprends pas pourquoi mais tout semble fonctionner normalement dans le cas paginé... Je pensais que j'obtiendrais des lignes qui apparaîtraient comme si elles étaient dupliquées sur plusieurs pages si elles étaient triées par un champ non unique.

@intiocean je pense que je l'ai corrigé avec 10f5e968bc10552be0ad02ee566513b5d274d5c5

@jieter Super ! Je peux voir pourquoi cela résoudrait le problème avec les tableaux non paginés, mais je pense toujours que le classement par une colonne non unique lorsque les données sont réparties sur différentes pages pourrait entraîner l'apparition de la même ligne sur plusieurs pages en raison du top n ... limit x offset y

Qu'est-ce que tu penses?

Similaire à : http://stackoverflow.com/questions/31736700/duplicate-elements-in-django-paginate-after-order-by-call

@wtfrank , @intiocean @n0ctua @op-alex-reid @fliphess Je viens de pousser quelques commits au master qui pourrait résoudre ce problème. Pouvez-vous vérifier?

@intiocean Si c'est la source de ce problème, je pense que nous ne pouvons résoudre ce problème que partiellement en documentant ce comportement.

Je peux confirmer que cela corrige les tableaux non paginés @jieter. Cela améliore également les performances des tables non paginées car il n'y a désormais qu'une seule requête au lieu d'une pour chaque ligne.

Vérifier:
Avec 962f502 et tri par la colonne 'Notes' -> duplication
image

Avec f853078 et tri par la colonne 'Notes' -> pas de duplication 🎉
image

Avez-vous une chance de faire une sortie @jieter ?

wohoo, enfin corrigé !

Oui, je sortirai dans une heure.

publié 1.4.0

Génial, merci @jieter !

@jieter et @intiocean merci beaucoup à tous les deux d'avoir résolu ce problème ! Fonctionne parfaitement maintenant.

Bonjour, je viens de rencontrer ce problème sur la version 1.21.2 et Django==2.0.6. Grâce au commentaire de intiocean , cela a été corrigé en définissant un ordre supplémentaire par 'pk'.

Cette page vous a été utile?
0 / 5 - 0 notes