Otimizando paginações com Zend_Paginator

Continuando o artigo Trabalhando com paginação usando Zend_Paginator, no qual abordamos o uso básico de paginações com Zend_Paginator, neste artigo iremos falar sobre as melhores práticas para criar paginações, afinal não precisamos resgatar todos os registros de uma tabela para exibir N itens em cada página.

Confira os princípios de uma páginação otimizada:

  • Buscar apenas os resultados que precisam ser exibidos
  • Limitar registros retornados
  • Ignorar registros já exibidos
  • Obter total de registros afetados

O que tinha de errado na paginação do último artigo?

Basicamente no último artigo para criar a paginação foram resgatados todos os registros da tabela, através de um fetchAll(), dependendo do caso essa tabela poderia ter poucos registros ou milhares e o pior, pode ser que não esteja no mesmo servidor da aplicação, o que iria gerar mais lentidão para trazer todos esses registros.

Além disso, definimos o script e o tipo de paginação na action, o que fica um pouco deselegante, como diria Sandra Annenberg, no exemplo desse artigo iremos configurar no bootstrap da aplicação.

O fetchAll() não é recomendado para criar paginações, porque resgata todos os registros e consequentemente utiliza o adaptador Iterator para controlar os itens de cada página e de preferência defina script e tipo de paginação no bootstrap ou na view.

Solução: Adaptadores DbSelect e DbTableSelect

A solução básica para otimizar paginações de dados vindos do banco de dados com Zend_Paginator é utilizar os adaptadores DbSelect e DbTableSelect, a diferença entre eles é que o DbSelect retorna um array, já o DbTableSelect retorna um objeto rowset.

Como utilizar?

Para utilizar esses adaptadores, devemos passar um objeto da classe Zend_Db_Select ou Zend_Db_Table_Select como parâmetro, seja intanciando um dos adaptadores ou através do método estático factory() da classe Zend_Paginator.

Qual a vantagem desses adaptadores?

A grande vantagem é que o objeto passado como parâmetro é manipulado por esses adaptadores visando a página que será exibida, adicionando limite de registros baseado na configuração de itens por página e ignorando registros já exibidos caso seja necessário.

Mas ao fazer isso nosso resultado é limitado, se nossa paginação estiver configurada para 10 itens por página, será resgatado 10 itens apenas da tabela, logo, como iremos montar o controle de paginação? se só temos base em 10 registros. Agora entra a outra grande sacada, esses adaptadores criam uma query dinâmica apenas para calcular qual seria o total de itens afetados pela query original, ou seja, um count para calcular essa quantidade, com isso temos todos os ingredientes para uma paginação otimizada.

Confira os passos realizados pelos adaptadores:

1. Primeiro precisamos de um objeto Zend_Db_Select ou Zend_Db_Table_Select.

// Basic query
$users = $userModel->select()
                   ->from('user');
// SELECT `user`.* FROM `user`

2. Quando o adaptador recebe esse objeto, ele verifica qual página será exibida, quantos itens por página deve buscar e manipula a query original para limitar a quantidade de registros que serão resgatados.

// Manipulating the query to retrieve only the necessary
SELECT `user`.* FROM `user` LIMIT 20, 10

3. Além disso o adaptador gera uma segunda query dinamicamente, baseada na original, para verificar quantos registros seriam afetados.

// Dynamic counter to basic queries
SELECT COUNT(1) AS zend_paginator_row_count FROM `user`

// Dynamic counter to complex queries
SELECT COUNT(1) AS zend_paginator_row_count
              FROM ( SELECT `user`.* FROM `user` )
Podemos optar por uma contagem personalizada de registros afetados, para isso devemos utilizar o método setRowCount() do adaptador, onde podemos passar uma um objeto select ou um inteiro para forçar um determinado número como quantidade de registros afetados, nesse caso o adaptador não gera a query dinâmica.

Solução alternativa: Adaptador Null

Uma solução alternativa mas um pouco mais arriscada e complexa é utilizar o adaptador Null, que deixa quase tudo na mão do programador, resgatar os dados corretamente, enviar para view e iterar os dados. O adaptador fica responsável apenas por exibir o controle da paginação, para isso é necessário informar o total de registros afetados pela pesquisa na hora de instanciar o adaptador ou utilizar o método estático factory() da classe Zend_Paginator.

Dicas para melhorar a paginação

O que vimos até agora nada mais é do que o modo correto de criar uma paginação proveniente de banco de dados, que por mais simples que pareça é difícil encontrar exemplos na internet, mas não deixa de ser o uso otimizado de paginações, porque é mais do que pegar todos os dados e exibir determinados registros por páginas.

Para melhorar ainda mais nossas paginações podemos:

  • Utilizar cache nas paginações
  • Criar índices otimizados para as tabelas

Essas dicas ficarão para outro artigo, mas nada impede de você começar a pesquisar sobre o uso otimizado de índices ou de cache. O Zend_Paginator da suporte ao uso de cache de maneira simples, confira na documentação oficial.

Trabalhando com paginações otimizadas

Agora que já temos uma base de como otimizar paginações com Zend_Paginator, vamos criar uma paginação otimizada, para botar em pratica os conhecimentos deste artigo. Baseado na estrutura apresentada no tópico Preparando o ambiente para desenvolvimento com Zend Framework, crie um projeto com nome de example-advanced-paginator.

Estrutura inicial
Estrutura inicial

No exemplo que será visto agora, iremos utilizar uma tabela no banco de dados, será a tabela “user”, com dois campos user_id e name, além disso vamos precisar de alguns registros cadastrados para listar os resultados, para facilitar essa tarefa, acesse o script de criação e inserção de dados, copie este conteúdo, acesse o phpmyadmin, http://localhost/phpmyadmin, crie uma base de dados com o nome de “zf-paginator” e adicione o conteúdo do script para criar a tabela e seus registros, caso você tenha realizado o último exemplo pode pular essa parte.

Configure a aplicação para acessar o banco de dados “zf-paginator”, caso tenha dúvida, visualize o artigo Entendendo modelos no zend framework.

Criando nosso modelo

No artigo anterior criamos um modelo básico, mas dessa vez como queremos enviar um objeto da classe Zend_Db_Table_Select para ser manipulado pelo paginator, iremos criar um método.

User.php

<?php
class User extends Zend_Db_Table_Abstract
{
    /**
    * The default table name
    */
    protected $_name = 'user';

    /**
     * getAll function
     *
     * @desc basic query to retrieve all the data
     * @param boolean $paginate
     */
    public function getAll($paginate = false) {
    	$select = $this->select()
    	               ->from($this->_name);

    	return ($paginate) ? $select : $this->fetchAll($select);
    }
}
?>

Definindo um padrão para as paginações

Nesse momento vamos definir um padrão no bootstrap da aplicação de como deverão ser as paginações, mas nada impede que isso seja alterado em casos específicos, de preferência utilizando o view helper paginationControl() quando necessário.

Bootstrap.php

<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
	/**
    * Init Autoloader
    */
    protected function _initAutoload()
    {
        $loader = Zend_Loader_Autoloader::getInstance();
        $loader->setFallbackAutoloader(true);
    }

    /**
     * Init Paginator
     */
    protected function _initPaginator()
    {
    	Zend_Paginator::setDefaultScrollingStyle('Sliding');
        Zend_View_Helper_PaginationControl::setDefaultViewPartial('pagination.phtml');
    }
}
?>

Criando nosso controller e action

Neste exemplo iremos criar o controlador UserController.php contendo uma action, nomeada de list( listAction ), ou seja, utilizaremos a view list.phtml, o template de paginação que vamos utilizar será do tipo search, disponível na documentação do componente.

Adicione o arquivo “UserController.php” na pasta “application/controllers” com o seguinte conteúdo:

UserController.php

<?php
class UserController extends Zend_Controller_Action
{
    public function listAction()
    {
        $page = $this->_getParam('page', 1);

        $userModel = new User();
        // Returns an instance of the class Zend_Db_Table_Select
        $users = $userModel->getAll(true);

        // Returns a rowset
        // $users = $userModel->getAll();

        // First option to use Zend_Paginator_Adapter_DbTableSelect
        $adapter = new Zend_Paginator_Adapter_DbTableSelect($users);
        // $adapter->setRowCount($customCount);
        $paginator = new Zend_Paginator($adapter);

        // Second option to use Zend_Paginator_Adapter_DbTableSelect
        // $paginator = new Zend_Paginator($users);
        // Note: You cannot customize the count in this option

        $paginator->setCurrentPageNumber($page)
                  ->setItemCountPerPage(10);

        $this->view->assign('paginator', $paginator);
    }
}
?>

Agora vamos preparar nossa view, crie uma pasta nomeada de “user” em “application/views/scripts” e depois adicione o arquivo list.phtml com o seguinte conteúdo:

list.phtml

<ul>
<?php if(sizeof($this->paginator)): ?>
    <?php foreach($this->paginator as $user): ?>
        <li><?php echo $this->escape($user->name); ?></li>
    <?php endforeach; ?>
<?php else: ?>
    <li>Nenhum usuário encontrado.</li>
<?php endif; ?>
</ul>
<?php echo $this->paginator; ?>

Só falta nosso template de paginação, adicione o arquivo pagination.phtml em “application/views/scripts” com o seguinte conteúdo:

pagination.phtml

<?php if ($this->pageCount): ?>
<div>
<!-- Previous page link -->
<?php if (isset($this->previous)): ?>
    <a href="<?php echo $this->url(array('page' => $this->previous)); ?>">
        &lt; Previous
    </a> |
<?php else: ?>
    <span>&lt; Previous</span> |
<?php endif; ?>

<!-- Numbered page links -->
<?php foreach ($this->pagesInRange as $page): ?>
    <?php if ($page != $this->current): ?>
        <a href="<?php echo $this->url(array('page' => $page)); ?>">
            <?php echo $page; ?>
        </a> |
    <?php else: ?>
        <?php echo $page; ?> |
    <?php endif; ?>
<?php endforeach; ?>

<!-- Next page link -->
<?php if (isset($this->next)): ?>
    <a href="<?php echo $this->url(array('page' => $this->next)); ?>">
        Next &gt;
    </a>
<?php else: ?>
    <span>Next &gt;</span>
<?php endif; ?>
</div>
<?php endif; ?>

Estrutura final do nosso exemplo:

Estrutura final
Estrutura final

Resultado

Ao acessar nossa action list do controller user, teremos o seguinte resultado:

Resultado Paginator, página 1
Resultado Paginator, página 1

Caso seja clicado no link para página 2 ou em next, iremos obter outra lista de resultado, confira:

Resultado Paginator, página 2
Resultado Paginator, página 2

Visualizar ou efetuar download do exemplo, lembrando que no repositório desse projeto no github não consta os arquivos do framework.

Aparentemente nada mudou, mas em questão de desempenho, nossa aplicação agradece, isso pode ser calculado verificando o tempo de execução dos scripts, não esqueça de adicionar bastantes registros antes de fazer os cálculos para ver a diferença.

Referência(s):

http://framework.zend.com/manual/en/zend.paginator.html

http://www.slideshare.net/norm2782/20090828-php-benelux-bbq-advanced-usage-of-zend-paginator

  • José Hélio

    19/12/2011 às 17:39

    Muito interessante isso sobre o Zend_Paginator!

    Gostaria de saber se você conhece algum método que possibilite ordenar o resultado dessas paginações…

  • Douglas Lira

    20/12/2011 às 01:35

    Boa garoto, mandou bem!!

  • Paulo

    28/12/2011 às 10:34

    mas que post elegante

  • Carlos Filho

    06/01/2012 às 18:15

    Caro Diogo, tenho trabalhado com paginações há algum tempo e percebi que o Zend não faz as coisas como deveria.

    Internamente, se você fizer alguns debugs, vai perceber que no fundo ele faz a consulta na tabela inteira e apenas vai eliminando os registros desnecessários. Ao meu ver, ele não é feliz nesta implementação, o que, a grosso modo, não significa que ela não funcione bem.

    Apenas é ruim trabalhar com tabelas muito grandes sem nenhum limit, claro, porque com o tempo fica praticamente inviável fazer uma listagem.

    Tive de implementar tudo com Adapter Null, que te dá muita liberdade, mas nenhuma flexibilidade de cara. É preciso avaliar como deixar o método genérico pro seu sistema e estender uma classe pra isso, ou um helper, tanto faz.

    Experimente fazer um debug mais profundo e conferir o que digo aqui. No mais, seu post é bem claro e explicativo. Parabéns!

  • Diogo Matheus

    07/01/2012 às 05:26

    Fala Carlos,

    Muito legal seu comentário, ainda não parei para ver o quanto realmente otimiza comparando com outras soluções, quando eu tiver algum tempo tentarei verificar os pontos que você colocou.

    Quanto ao adapter null é um ótimo caminho para quem realmente sabe o que precisa ser feito para otimizar as paginações.

    Como falei no artigo, essa otimização mostrada nada mais é do que o uso verdadeiro do zend_paginator com registros resgatados do banco de dados, mas não quer dizer que é o mais otimizado possível.

    Abraço

  • Diogo Matheus

    07/01/2012 às 05:33


    José Hélio:

    Olha não creio em um método para isso, mas não acho que seja difícil realizar uma ordenação, caso seja padrão, basta ordenar pelo select antes de enviar para o adaptador, caso seja escolhido pelo usuário, basta implementar em forma de filtro, caso ele clique em uma coluna por exemplo, para organizar digamos uma listagem de notícias pelo título, basta passar isso como parâmetro e ter uma verificação na hora de montar o select de consulta.

    Caso eu saiba de algo diferente sobre eu posto aqui.

    Abraço e boa sorte

  • Mauro

    24/07/2012 às 16:33

    Fiz a paginção com resultado que vem de um formulário, a primeira página carrega normalmente, mas quando mudo de página perde as informalções como posso manter as informações em todas as páginas.

  • Diogo Matheus

    28/08/2012 às 21:42

    Olá Mauro,

    Que tipo de informações está perdendo? Já vi casos de locais que não estavam trabalhando com urls absolutas e acontecia da página não renderizar corretamente.

    Abraço e boa sorte.

  • Jean Gomes

    29/01/2014 às 15:19

    Muito bom o artigo, parabéns pelo site, e obrigado seus artigos sempre me ajudaram muito.

    Sei que tem muito tempo o artigo, já temos o zend 2, mas estou em um projeto com Zend 1 e utilizei essa otimização numa listagem aqui, precisava realizar um inner join se puder me dar uma ideia ficarei muito grato.

  • Romero

    27/01/2015 às 11:24

    Prezado Diogo, seu post foi muito esclarecedor a respeito do uso do Zend_Paginator. Obrigado!

    Estou prestando manutenção a uma aplicação com ZF 1.12 e ExtJS 4.1. Realmente, ajudou-me muito!

    Em tempo: o colega acima disse ter realizado debugs, onde constatou que o Zend_Paginator não utilizou LIMIT e OFFSET. Realizei um debug e pude perceber que o Zend_Paginator_Adapter_DbTableSelect utiliza sim, o limit.
    O que me vem a cabeça seria um bug em alguma versão anterior à 1.12, onde esta não executava o limit satisfatoriamente.

    Abraço.

  • Diogo Matheus

    08/02/2015 às 17:32

    Olá Jean,
    Confira o artigo sobre joins:
    http://www.diogomatheus.com.br/blog/zend-framework/realizando-joins-no-zend-framework/

    Romero,
    Obrigado por compartilhar sua experiência.

Deixe uma resposta

O seu endereço de e-mail não será publicado.. Campos obrigatórios são marcados com *