Paginierung von Datenbankergebnissen
Bei der Entwicklung von Webanwendungen stoßen Sie sehr oft auf die Anforderung, die Anzahl der auf einer Seite aufgelisteten Einträge zu begrenzen. Diese Technik nennt man Paginierung.
Gehen wir von einem Zustand aus, in dem wir alle Daten ohne Paginierung auflisten. Für die Auswahl der Daten aus der Datenbank
haben wir eine Klasse ArticleRepository. Sie enthält neben dem Konstruktor eine Methode
findPublishedArticles, die alle veröffentlichten Artikel absteigend nach dem Veröffentlichungsdatum sortiert
zurückgibt.
namespace App\Model;
use Nette;
class ArticleRepository
{
public function __construct(
private Nette\Database\Connection $database,
) {
}
public function findPublishedArticles(): Nette\Database\ResultSet
{
return $this->database->query('
SELECT * FROM articles
WHERE created_at < ?
ORDER BY created_at DESC',
new \DateTime,
);
}
}
Im Presenter injizieren wir dann diese Modellklasse. In der Render-Methode fordern wir die veröffentlichten Artikel an und übergeben sie an das Template:
namespace App\Presentation\Home;
use Nette;
use App\Model\ArticleRepository;
class HomePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articleRepository,
) {
}
public function renderDefault(): void
{
$this->template->articles = $this->articleRepository->findPublishedArticles();
}
}
Im Template default.latte kümmern wir uns dann um die Ausgabe der Artikel:
{block content}
<h1>Artikel</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
Auf diese Weise können wir alle Artikel auflisten, was jedoch in dem Moment problematisch wird, in dem die Anzahl der Artikel wächst. Dann kommt die Implementierung eines Paginierungsmechanismus gelegen.
Dieser Mechanismus sorgt dafür, dass alle Artikel auf mehrere Seiten verteilt werden und wir nur die Artikel der gerade ausgewählten Seite anzeigen. Die Gesamtzahl der Seiten und die Aufteilung der Artikel berechnet der Paginator selbst, und zwar anhand der Gesamtzahl der Artikel und der gewünschten Anzahl von Artikeln pro Seite.
Im ersten Schritt passen wir die Methode zum Abrufen der Artikel in der Repository-Klasse so an, dass sie nur die Artikel einer Seite zurückgeben kann. Außerdem fügen wir eine Methode hinzu, um die Gesamtzahl der Artikel in der Datenbank zu ermitteln, die wir für die Konfiguration des Paginators brauchen:
namespace App\Model;
use Nette;
class ArticleRepository
{
public function __construct(
private Nette\Database\Connection $database,
) {
}
public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet
{
return $this->database->query('
SELECT * FROM articles
WHERE created_at < ?
ORDER BY created_at DESC
LIMIT ?
OFFSET ?',
new \DateTime, $limit, $offset,
);
}
/**
* Gibt die Gesamtzahl der veröffentlichten Artikel zurück
*/
public function getPublishedArticlesCount(): int
{
return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
}
}
Als Nächstes machen wir uns an die Anpassung des Presenters. An die Methode renderDefault übergeben wir die
Nummer der aktuell angezeigten Seite. Für den Fall, dass diese Nummer nicht Teil der URL ist, setzen wir den Standardwert 1 (die
erste Seite).
Außerdem erweitern wir die Render-Methode, sodass sie eine Paginator-Instanz erzeugt und konfiguriert und die passenden
Artikel für die Anzeige im Template auswählt. Der angepasste HomePresenter sieht dann so aus:
namespace App\Presentation\Home;
use Nette;
use App\Model\ArticleRepository;
class HomePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articleRepository,
) {
}
public function renderDefault(int $page = 1): void
{
// Wir ermitteln die Gesamtzahl der veröffentlichten Artikel
$articlesCount = $this->articleRepository->getPublishedArticlesCount();
// Wir erzeugen eine Paginator-Instanz und konfigurieren sie
$paginator = new Nette\Utils\Paginator;
$paginator->setItemCount($articlesCount); // Gesamtzahl der Einträge
$paginator->setItemsPerPage(10); // Einträge pro Seite
$paginator->setPage($page); // Nummer der aktuellen Seite
// Aus der Datenbank holen wir eine begrenzte Menge von Artikeln nach der Berechnung des Paginators
$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());
// die wir an das Template übergeben
$this->template->articles = $articles;
// und auch den Paginator selbst für die Anzeige der Paginierung
$this->template->paginator = $paginator;
}
}
Das Template iteriert nun nur noch über die Artikel der aktuellen Seite. Wir müssen lediglich die Paginierungslinks hinzufügen:
{block content}
<h1>Artikel</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
<div class="pagination">
{if !$paginator->isFirst()}
<a n:href="default, 1">Erste</a>
|
<a n:href="default, $paginator->getPage() - 1">Vorherige</a>
|
{/if}
Seite {$paginator->getPage()} von {$paginator->getPageCount()}
{if !$paginator->isLast()}
|
<a n:href="default, $paginator->getPage() + 1">Nächste</a>
|
<a n:href="default, $paginator->getPageCount()">Letzte</a>
{/if}
</div>
Damit ist die Paginierung mithilfe des Paginators fertig. Wenn Sie als Datenbankschicht statt Nette Database Core den Nette
Database Explorer verwenden, können Sie die Paginierung sogar ohne direkte Verwendung des Paginators umsetzen. Die Klasse
Nette\Database\Table\Selection enthält nämlich die Methode page(), die die Paginierungslogik
kapselt.
Das Repository sieht bei dieser Art der Implementierung so aus:
namespace App\Model;
use Nette;
class ArticleRepository
{
public function __construct(
private Nette\Database\Explorer $database,
) {
}
public function findPublishedArticles(): Nette\Database\Table\Selection
{
return $this->database->table('articles')
->where('created_at < ', new \DateTime)
->order('created_at DESC');
}
}
Im Presenter müssen wir keine Paginator-Instanz erzeugen. Stattdessen verwenden wir die Methode page() des
Selection-Objekts, das uns das Repository zurückgibt:
namespace App\Presentation\Home;
use Nette;
use App\Model\ArticleRepository;
class HomePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articleRepository,
) {
}
public function renderDefault(int $page = 1): void
{
// Wir holen die veröffentlichten Artikel
$articles = $this->articleRepository->findPublishedArticles();
// und senden an das Template nur den Teil, den die Berechnung der Methode page begrenzt
$lastPage = 0;
$this->template->articles = $articles->page($page, 10, $lastPage);
// und außerdem die nötigen Daten für die Anzeige der Paginierung
$this->template->page = $page;
$this->template->lastPage = $lastPage;
}
}
Da wir dem Template jetzt kein Paginator-Objekt mehr übergeben, passen wir den Teil an, der die Paginierungslinks anzeigt:
{block content}
<h1>Artikel</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
<div class="pagination">
{if $page > 1}
<a n:href="default, 1">Erste</a>
|
<a n:href="default, $page - 1">Vorherige</a>
|
{/if}
Seite {$page} von {$lastPage}
{if $page < $lastPage}
|
<a n:href="default, $page + 1">Nächste</a>
|
<a n:href="default, $lastPage">Letzte</a>
{/if}
</div>
Auf diese Weise haben wir den Paginierungsmechanismus ohne explizite Verwendung des Paginators implementiert.