Django-tables2: Linhas de tabela duplicadas após classificação por cabeçalho de coluna

Criado em 3 ago. 2016  ·  32Comentários  ·  Fonte: jieter/django-tables2

Estou tendo um problema em que algumas linhas são renderizadas várias vezes no lugar de outras linhas. A tabela em questão costumava renderizar perfeitamente bem em versões anteriores do django e django-tables2. Eu atualizei os dois ao mesmo tempo para a versão mais recente de cada um (django 1.10, django-tables2 1.2.4), então esse problema começou a acontecer.

Veja esta imagem aqui para uma demonstração: http://imgur.com/a/pPRT8

Quando carrego uma tabela sem escolher uma ordem de classificação pela coluna, não vejo esse problema. Assim que clico no cabeçalho de uma coluna para alterar a ordem de classificação, as linhas são duplicadas.

Até agora, identifiquei que os dados passados ​​para o objeto BoundRows estão corretos, sem itens duplicados. No entanto, a iteração pelas linhas vinculadas retorna entradas duplicadas. Portanto, parece que algo pode estar mudando o objeto de dados enquanto a iteração está ocorrendo.

Esse problema está acontecendo em tabelas de comprimento ~ 150, e eu (ainda) não consegui reproduzir esse problema com um conjunto de dados de teste menor.

Este trecho mostra o código de teste que usei para restringir a causa do problema.

--- 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

Comentários muito úteis

wohoo, finalmente consertado!

Sim, vou soltar dentro de uma hora.

Todos 32 comentários

Cristalizar o queryset por meio de list () impede que o problema aconteça:

+++ 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))

Obrigado por relatar, lançar para a lista não é aceitável como uma solução, pois isso não funcionará com conjuntos de dados maiores.

Você pode tentar fazer o downgrade de sua versão do django-tables2 para ter certeza de que não é um problema com a atualização da sua versão do Django?

Sim, definitivamente não é uma solução - mas pode ser um indicativo da causa. O outro motivo pelo qual o cast para list não é uma solução adequada é porque nem sempre parece ser um queryset contendo os dados, caso em que self.data.data não existirá.

@wtfrank , entendo, mas ajudaria muito se você pudesse fornecer um caso de teste reproduzível. Você tentou fazer o downgrade do django-tables2 para a versão anterior que você usava?

Acho que estou tendo o mesmo problema: na minha tabela, uma linha está faltando e outra é uma duplicata ... Para mim, isso começou com a versão 1.2.2.
Fazer downgrade para 1.2.1 "fixxes" é o problema para mim ... Há alguma informação que eu possa fornecer para tornar a pesquisa mais fácil?

Seria muito bom ter um caso de teste mínimo. Podemos então executar git bisect para encontrar o commit inválido.

Estou analisando um caso de teste no momento, mas, estranhamente, não consigo reproduzir o problema no meu ambiente de teste ...

Localmente (usando ./manage runserver usando sqlite) está tudo bem, enquanto na produção (executando uwsgi e mysql) existe o problema de ingênuos ... (ambos os ambientes rodam em versões iguais)

O queryset não contém nenhum dupes .... Mas no momento em que o table.html é renderizado, o queryset está sem um objeto e outro está duplicado ...

Voltando para você em breve com um caso de teste, espero :)

O problema ocorre também na versão 1.2.6. Eu acho que é sth com # 329 ou # 330, mas até agora não consigo encontrar uma maneira de consertar.

Eles são aplicáveis ​​apenas ao 1.2.6, então não espero que sejam a fonte desse bug.

Não posso fornecer um caso reproduzível adequado, mas acabei de ter esse problema (ou, pelo menos, acredito que seja esse o problema) e posso dizer que certamente aconteceu entre 1.2.1 e 1.2.2 .

Sempre que faço o downgrade para 1.2.1, tudo parece bem. Quando eu atualizo para 1.2.2 ou superior (eu comecei com o 1.2.6 mais recente, é claro), eu sempre perco a primeira linha (das três que tenho no banco de dados) e obtenho uma cópia de outra em vez dela. Por exemplo, quando em 1.2.1 eu tenho:

| data | acessos | ... |
| --- | --- | --- |
| 08/10/2016 | 123 ... |
| 07/10/2016 | 321 ... |
| 06-10-2016 | 0 | ... |

Em 1.2.2-1.2.6, recebo consistentemente em vez disso:

| data | acessos | ... |
| --- | --- | --- |
| 06-10-2016 | 0 | ... |
| 07/10/2016 | 321 ... |
| 06-10-2016 | 0 | ... |

Estou usando Django 1.9.9 no Python 2.7.12, o projeto foi iniciado usando pydanny / cookiecutter-django (embora muito modificado depois disso) e meu código se parece com este:

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

O Meta tem apenas verbose_name{,_plural} coisas e a visualização é um DetailView simples que se parece com isto:

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

E há três linhas no banco de dados (PostgreSQL 9.6), todas tendo o mesmo source_id (eu tenho apenas uma), então todas correspondem à consulta. Estranhamente, se eu substituir .filter(source=...) por .all() o problema parece desaparecer.

Isso é tudo que eu fui capaz de descobrir. Espero que ajude. Acho que vou ficar com 1.2.1 por enquanto :)

Obrigado por mostrar suas divagações, examinarei esse problema mais tarde.

Acabei de ter o mesmo problema, cristalizar o queryset via list () também não é uma solução viável da nossa parte.

@ op-alex-reid você pode tentar transformar seu caso de uso em um caso de teste reproduzível mínimo?

Estou tendo o mesmo problema (Django == 1.9, django-tables == 1.2.3) com o seguinte código:

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

e

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

Curiosamente, quando defino orderable = True , o problema não ocorre.

No meu caso, cristalizar o queryset via list () _é_ uma opção (e a correção por enquanto), uma vez que existem apenas no máximo 5 linhas na tabela.

Obrigado pelo exemplo!

Tentei reproduzir isso usando este código:

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

mas isso não produz linhas duplicadas ...

Eu encontrei o mesmo problema hoje. Estou usando django-tables2 == 1.2.9 em um projeto Django == 1.10, onde também utilizo django-filters == 1.0.1.

Curiosamente, a duplicação das linhas na minha tabela só ocorre se

  1. existem muitas entradas para mostrar
    (então, se eu ativar a paginação com 25 registros por página, está tudo bem, mas se eu exibir o conjunto de dados completo, obtenho várias entradas idênticas)

  2. a tabela é classificada por uma coluna que contém não strings
    (está tudo bem se eu classificar por uma coluna que contém, por exemplo, nomes, mas classificar por colunas com datas, ints ou bools resulta em problemas)

O downgrade para 1.2.1 resolve o problema, mas isso não é realmente uma opção.
Além disso, os ingênuos parecem ser mostrados em vez de e não em adição às entradas reais, porque a contagem {{filter.qs.count}} é como deveria ser.

Espero que isso ajude um pouco a chegar ao fundo do problema, porque além disso, eu realmente gosto de trabalhar com django-tables2.

Obrigado por todo o seu trabalho!

@ n0ctua, obrigado!

Você pode dar uma olhada nas consultas exatas executadas e talvez compartilhá-las aqui?

Também acabei de ter esse problema e entendo por que está acontecendo comigo, então pensei em postar, pois suspeito que outros têm pelo mesmo motivo. _ (Embora eu não tenha tentado com outras versões de django-tables2, então posso estar errado, pois não entendo por que isso iria impedir que isso acontecesse ... Embora ... realmente pensando sobre isso, eu colocaria a hipótese de que isso é porque em versões anteriores as tabelas não paginadas executavam uma única consulta no banco de dados em vez de uma consulta para cada linha - mais detalhes sobre isso a seguir ...) _

É porque _SQL order by em campos não únicos não é determinístico_ então, quando combinado por top n você obtém as mesmas n linhas principais repetidamente. (Além disso, por alguma razão, quando eu crio um Table sem paginação, ele gera uma consulta para _cada_ linha. IE top 1 ).

Não tenho certeza de qual é a melhor solução aqui? Acho que a única maneira de garantir que isso funcionará é sempre ter o order by final em um campo único. Idealmente, a chave primária? Além disso, pode valer a pena adicionar algumas notas aos documentos de aviso sobre isso?

Como um problema separado, mas que realmente reduzirá a frequência com que isso acontece (e acelerará as consultas para tabelas não paginadas), em vez de executar uma consulta top 1 para cada linha da tabela, seria apenas executar uma única consulta que busca todos os resultados.

@intiocean este número de consultas para tabelas não paginadas é bastante estranho, não sei por que isso acontece, e não deveria acontecer. Este caso de teste mostra o problema:

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

Tentei, sem sucesso, criar um teste para o caso paginado que falha. Não entendo por que, mas tudo parece funcionar normalmente no caso paginado ... Pensei em obter linhas que aparecem como se duplicadas em várias páginas, se classificar por um campo não exclusivo.

@intiocean Acho que consertei com 10f5e968bc10552be0ad02ee566513b5d274d5c5

@jieter Ótimo! Posso ver por que isso resolveria o problema com tabelas não paginadas, mas ainda acho que ordenar por uma coluna não exclusiva quando os dados são espalhados por páginas diferentes pode resultar na mesma linha aparecendo em várias páginas devido ao top n ... limit x offset y

O que você acha?

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

@wtfrank , @intiocean @ n0ctua @ op-alex-reid @fliphess Acabei de

@intiocean Se essa é a origem do problema, acho que só podemos corrigi-lo parcialmente documentando esse comportamento.

Posso confirmar que isso corrige tabelas não paginadas @jieter. Isso também melhora o desempenho de tabelas não paginadas, pois agora há apenas uma consulta em vez de uma para cada linha.

Verificar:
Com 962f502 e classificação pela coluna 'Notas' -> duplicação
image

Com f853078 e classificação pela coluna 'Notas' -> sem duplicação 🎉
image

Alguma chance de você fazer um lançamento @jieter?

wohoo, finalmente consertado!

Sim, vou soltar dentro de uma hora.

lançado 1.4.0

Incrível, obrigado @jieter!

@jieter e @intiocean muito obrigado por consertar isso! Funciona perfeitamente agora.

Olá, acabei de enfrentar esse problema na versão 1.21.2 e no Django == 2.0.6. Graças a intiocean 's comentário , fixa-lo com a criação de encomenda adicional por 'pk'.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

mpasternak picture mpasternak  ·  9Comentários

brianmay picture brianmay  ·  6Comentários

blite picture blite  ·  3Comentários

applegrew picture applegrew  ·  17Comentários

foldedpaper picture foldedpaper  ·  6Comentários