Paginierung von Datenbankergebnissen

Bei der Erstellung von Webanwendungen stoßen Sie sehr oft auf die Anforderung, die Anzahl der angezeigten Elemente pro Seite zu begrenzen.

Wir gehen von einem Zustand aus, in dem wir alle Daten ohne Paginierung auflisten. Für die Auswahl der Daten aus der Datenbank haben wir die Klasse ArticleRepository, die neben dem Konstruktor die Methode findPublishedArticles enthält, die alle veröffentlichten Artikel absteigend nach 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 die Modellklasse und in der Render-Methode fordern wir die veröffentlichten Artikel an, die wir an die Vorlage übergeben:

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();
	}
}

In der Vorlage default.latte kümmern wir uns dann um die Auflistung 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 Probleme verursacht, sobald die Anzahl der Artikel steigt. In diesem Moment ist die Implementierung eines Paginierungsmechanismus sinnvoll.

Dieser sorgt dafür, dass alle Artikel auf mehrere Seiten aufgeteilt werden und wir nur die Artikel der aktuellen Seite anzeigen. Die Gesamtzahl der Seiten und die Aufteilung der Artikel berechnet Paginator selbst, basierend darauf, wie viele Artikel wir insgesamt haben und wie viele Artikel wir pro Seite anzeigen möchten.

Im ersten Schritt passen wir die Methode zum Abrufen der Artikel in der Repository-Klasse so an, dass sie uns nur Artikel für eine Seite zurückgeben kann. Wir fügen auch eine Methode hinzu, um die Gesamtzahl der Artikel in der Datenbank zu ermitteln, die wir zum Einrichten des Paginators benötigen:

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);
	}
}

Anschließend widmen wir uns den Anpassungen des Presenters. An die Render-Methode übergeben wir die Nummer der aktuell angezeigten Seite. Für den Fall, dass diese Nummer nicht Teil der URL ist, legen wir den Standardwert auf die erste Seite fest.

Weiterhin erweitern wir die Render-Methode um das Abrufen der Paginator-Instanz, deren Einrichtung und die Auswahl der richtigen Artikel zur Anzeige in der Vorlage. Der HomePresenter sieht nach den Anpassungen wie folgt 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 erstellen eine Instanz des Paginators und richten sie ein
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // Gesamtzahl der Artikel
		$paginator->setItemsPerPage(10); // Anzahl der Elemente pro Seite
		$paginator->setPage($page); // Nummer der aktuellen Seite

		// Aus der Datenbank ziehen wir eine begrenzte Menge von Artikeln gemäß der Berechnung des Paginators
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// die wir an die Vorlage übergeben
		$this->template->articles = $articles;
		// und auch den Paginator selbst zur Anzeige der Paginierungsoptionen
		$this->template->paginator = $paginator;
	}
}

Die Vorlage iteriert nun bereits nur über die Artikel einer 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>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->page-1">Vorherige</a>
		&nbsp;|&nbsp;
	{/if}

	Seite {$paginator->getPage()} von {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Nächste</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Letzte</a>
	{/if}
</div>

So haben wir die Seite um die Möglichkeit der Paginierung mit dem Paginator ergänzt. Falls wir anstelle von Nette Database Core als Datenbankschicht Nette Database Explorer verwenden, können wir die Paginierung auch ohne Paginator implementieren. Die Klasse Nette\Database\Table\Selection enthält nämlich die Methode page mit der vom Paginator übernommenen Paginierungslogik.

Das Repository sieht bei dieser Implementierungsmethode wie folgt 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 keinen Paginator erstellen, wir verwenden stattdessen die Methode der Selection-Klasse, die 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 ziehen die veröffentlichten Artikel heraus
		$articles = $this->articleRepository->findPublishedArticles();

		// und senden nur einen Teil davon an die Vorlage, begrenzt durch die Berechnung der page-Methode
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// und auch die notwendigen Daten zur Anzeige der Paginierungsoptionen
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Da wir nun keinen Paginator an die Vorlage senden, 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>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Vorherige</a>
		&nbsp;|&nbsp;
	{/if}

	Seite {$page} von {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Nächste</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Letzte</a>
	{/if}
</div>

Auf diese Weise haben wir den Paginierungsmechanismus ohne Verwendung des Paginators implementiert.