Letzte Woche habe ich einen Artikel über die Nutzlosigkeit der Repository-Vorlage für eloquente Entitäten geschrieben , aber ich habe versprochen, zu erklären, wie man sie teilweise mit Nutzen verwendet. Dazu werde ich versuchen zu analysieren, wie diese Vorlage normalerweise in Projekten verwendet wird. Der minimal erforderliche Satz von Methoden für das Repository:
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); }
In realen Projekten werden jedoch häufig Methoden zur Auswahl von Datensätzen hinzugefügt, wenn entschieden wurde, Repositorys zu verwenden:
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Diese Methoden könnten über eloquente Bereiche implementiert werden, aber das Überladen von Entitätsklassen mit Verantwortlichkeiten für das Abrufen selbst ist keine gute Idee, und es erscheint logisch, diese Verantwortung in Repository-Klassen zu übertragen. Ist es so? Ich habe diese Schnittstelle speziell visuell in zwei Teile unterteilt. Der erste Teil der Methoden wird für Schreibvorgänge verwendet.
Standardschreibvorgänge sind:
- Erstellen eines neuen Objekts und Aufrufen von PostRepository :: save
- PostRepository :: getById , Entitätsmanipulation und Aufruf von PostRepository :: save
- Rufen Sie PostRepository :: delete auf
Bei Schreibvorgängen gibt es keine Abtastmethoden. Bei Lesevorgängen werden nur get * -Methoden verwendet. Wenn Sie über das Prinzip der Schnittstellentrennung (Buchstabe I in SOLID ) lesen , wird deutlich, dass unsere Schnittstelle zu groß ist und mindestens zwei verschiedene Aufgaben erfüllt. Es ist Zeit, es in zwei Teile zu teilen. Die Methode getById ist in beiden Fällen erforderlich. Wenn die Anwendung jedoch komplizierter wird, unterscheidet sich ihre Implementierung. Dies werden wir etwas später sehen. Ich habe in einem früheren Artikel über die Nutzlosigkeit des Schreibteils geschrieben, also vergesse ich es einfach.
Lesen Sie den Teil scheint mir nicht so nutzlos, denn selbst für Eloquent kann es mehrere Implementierungen geben. Wie benenne ich eine Klasse? Sie können ReadPostRepository verwenden , es hat jedoch bereits eine geringe Beziehung zur Repository- Vorlage. Sie können einfach PostQueries :
<?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Die Implementierung mit Eloquent ist recht einfach:
<?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit() ->get(); } public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit() ->get(); } public function getUserPosts($userId) { return Post::whereUserId($userId) ->orderBy('created_at', 'desc') ->get(); } }
Die Schnittstelle muss der Implementierung zugeordnet sein, z. B. im AppServiceProvider :
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, EloquentPostQueries::class); } }
Diese Klasse ist bereits nützlich. Er erkennt seine Verantwortung, indem er entweder Controller oder eine Entitätsklasse entlädt. In der Steuerung kann es folgendermaßen verwendet werden:
<?php final class PostsController extends Controller { public function lastPosts(PostQueries $postQueries) { return view('posts.last', [ 'posts' => $postQueries->getLastPosts(), ]); } }
Die PostsController :: lastPosts-Methode fragt sich einfach nach einer Implementierung von PostsQueries und arbeitet damit. Im Provider haben wir PostQueries der EloquentPostQueries- Klasse zugeordnet, und diese Klasse wird in den Controller eingesetzt.
Stellen wir uns vor, unsere Anwendung ist sehr beliebt geworden. Tausende Benutzer pro Minute öffnen die Seite mit den neuesten Veröffentlichungen. Die beliebtesten Veröffentlichungen werden auch sehr oft gelesen. Datenbanken kommen mit solchen Belastungen nicht sehr gut zurecht, daher verwenden sie den Standardlösungs-Cache. Zusätzlich zur Datenbank wird ein bestimmtes Daten-Nugget in einem Repository gespeichert, das für bestimmte Vorgänge optimiert ist - Memcached oder Redis .
Die Caching-Logik ist normalerweise nicht so kompliziert, aber die Implementierung in EloquentPostQueries ist nicht sehr korrekt (zumindest aufgrund des Single-Responsibility-Prinzips ). Es ist viel natürlicher, die Decorator- Vorlage zu verwenden und das Caching als Dekoration für die Hauptaktion zu implementieren:
<?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; private $base; private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); }
Ignorieren Sie die Repository- Schnittstelle im Konstruktor. Aus irgendeinem Grund haben sie beschlossen, die Schnittstelle für das Caching in Laravel aufzurufen.
Die CachedPostQueries- Klasse implementiert nur das Caching. $ this-> cache-> merke dir , ob der angegebene Eintrag im Cache ist. Wenn nicht, ruft er den Rückruf auf und schreibt den zurückgegebenen Wert in den Cache. Es bleibt nur diese Klasse in der Anwendung zu implementieren. Wir benötigen alle Klassen, die in der Anwendung die Implementierung der PostQueries- Schnittstelle anfordern , um eine Instanz der CachedPostQueries- Klasse zu erhalten. CachedPostQueries selbst sollte jedoch die EloquentPostQueries- Klasse als Parameter im Konstruktor erhalten, da dies ohne eine "echte" Implementierung nicht funktionieren kann. AppServiceProvider ändern:
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, CachedPostQueries::class); $this->app->when(CachedPostQueries::class) ->needs(PostQueries::class) ->give(EloquentPostQueries::class); } }
Alle meine Wünsche sind ganz natürlich im Anbieter beschrieben. Daher haben wir das Caching für unsere Anforderungen nur implementiert, indem wir eine Klasse geschrieben und die Containerkonfiguration geändert haben. Der Code für den Rest der Anwendung wurde nicht geändert.
Natürlich ist es für die vollständige Implementierung des Caching weiterhin erforderlich, die Ungültigmachung zu implementieren, damit der gelöschte Artikel nicht länger auf der Site hängt, sondern sofort abläuft (kürzlich wurde ein Artikel über das Caching geschrieben , der im Detail hilfreich sein kann).
Fazit: Wir haben nicht ein, sondern zwei ganze Muster verwendet. Die CQRS- Vorlage (Command Query Responsibility Segregation) bietet eine vollständige Trennung von Lese- und Schreibvorgängen auf Schnittstellenebene. Ich bin durch das Prinzip der Schnittstellentrennung zu ihm gekommen, was bedeutet, dass ich Vorlagen und Prinzipien geschickt manipuliere und als Theorem voneinander ableitet :) Natürlich benötigt nicht jedes Projekt eine solche Abstraktion auf Entitätsbeispielen, aber ich werde den Fokus mit Ihnen teilen. In der Anfangsphase der Anwendungsentwicklung können Sie einfach eine PostQueries- Klasse mit einer regelmäßigen Implementierung über Eloquent erstellen:
<?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); }
Wenn Caching erforderlich ist, können Sie anstelle dieser PostQueries- Klasse problemlos eine Schnittstelle (oder eine abstrakte Klasse) erstellen , deren Implementierung in die EloquentPostQueries- Klasse kopieren und zu dem zuvor beschriebenen Schema wechseln . Der Rest des Anwendungscodes muss nicht geändert werden.
Es bleibt jedoch ein Problem bei der Verwendung derselben Post-Entitäten, die Daten ändern können. Dies ist nicht genau CQRS.

Niemand stört sich daran, die Post- Entität von PostQueries abzurufen , zu ändern und die Änderungen mit einem einfachen -> save () zu speichern . Und es wird funktionieren.
Nach einer Weile wechselt das Team zur Master-Slave-Replikation in der Datenbank und PostQueries arbeitet mit Lesereplikaten . Schreibvorgänge für Lesereplikate werden normalerweise blockiert. Der Fehler wird geöffnet, aber Sie müssen hart arbeiten, um alle diese Pfosten zu beheben.
Die naheliegende Lösung besteht darin, die Lese- und Schreibteile vollständig zu trennen. Sie können Eloquent weiterhin verwenden, indem Sie jedoch eine Klasse für schreibgeschützte Modelle erstellen. Beispiel: https://github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.php Alle Datenänderungsvorgänge sind blockiert. Erstellen Sie ein neues Modell, z. B. ReadPost (Sie können Post verlassen, aber in einen anderen Namespace verschieben):
<?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; }
Solche Modelle können schreibgeschützt und sicher zwischengespeichert werden.

Eine weitere Option: Eloquent aufgeben. Dafür kann es mehrere Gründe geben:
- Alle Tabellenfelder werden fast nie benötigt. Für die LastPosts- Anforderung sind normalerweise nur die Felder id , title und beispielsweise shared_at erforderlich. Das Anfordern einiger schwerer Texte von Veröffentlichungen führt nur zu einer unnötigen Belastung der Datenbank oder des Caches. Eloquent kann nur die erforderlichen Felder auswählen, aber all dies ist sehr implizit. PostQueries- Kunden wissen nicht genau, welche Felder ausgewählt werden, ohne in die Implementierung einzusteigen .
- Beim Caching wird standardmäßig die Serialisierung verwendet. Eloquente Klassen nehmen in serialisierter Form zu viel Platz ein. Wenn dies bei einfachen Entitäten nicht besonders auffällt, wird dies bei komplexen Entitäten mit Beziehungen zu einem Problem (wie ziehen Sie sogar Objekte mit einer Megabyte-Größe aus dem Cache?). Bei einem Projekt benötigte eine normale Klasse mit öffentlichen Feldern im Cache zehnmal weniger Speicherplatz als die Option Eloquent (es gab viele kleine Subentitäten). Sie können Attribute nur beim Zwischenspeichern zwischenspeichern, dies erschwert den Vorgang jedoch erheblich.
Ein einfaches Beispiel dafür, wie dies aussehen könnte:
<?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Natürlich scheint dies alles eine wilde Überkomplikation der Logik zu sein. "Nehmen Sie die eloquenten Zielfernrohre und alles wird gut. Warum sich das alles einfallen lassen?" Für einfachere Projekte ist dies richtig. Es ist absolut nicht notwendig, die Bereiche neu zu erfinden. Aber wenn das Projekt groß ist und mehrere Entwickler an der Entwicklung beteiligt sind, die sich oft ändern (sie beenden und neue kommen), werden die Spielregeln etwas anders. Der Code muss geschützt geschrieben werden, damit der neue Entwickler nach einigen Jahren nichts falsch machen kann. Natürlich ist es unmöglich, eine solche Wahrscheinlichkeit vollständig auszuschließen, aber es ist notwendig, ihre Wahrscheinlichkeit zu verringern. Darüber hinaus ist dies eine übliche Systemzerlegung. Sie können alle Caching-Dekoratoren und -Klassen für die Ungültigmachung des Caches in einem bestimmten "Caching-Modul" sammeln und so den Rest der Anwendung von Informationen zum Caching speichern. Ich musste große Anfragen durchsuchen, die von Cache-Aufrufen umgeben waren. Es steht im Weg. Insbesondere wenn die Caching-Logik nicht so einfach ist wie oben beschrieben.