for - Richtiges Repository Pattern Design in PHP?




sql repository php (4)

Basierend auf meiner Erfahrung, hier sind einige Antworten auf Ihre Fragen:

F: Wie gehen wir damit um, Felder zurückzubringen, die wir nicht brauchen?

A: Aus meiner Erfahrung läuft dies auf den Umgang mit kompletten Entitäten im Vergleich zu Ad-hoc-Abfragen hinaus.

Eine vollständige Entität ist etwas wie ein User . Es hat Eigenschaften und Methoden usw. Es ist ein erstklassiger Bürger in Ihrer Codebasis.

Eine Ad-hoc-Abfrage gibt einige Daten zurück, wir wissen jedoch nichts darüber hinaus. Da die Daten in der Anwendung weitergegeben werden, geschieht dies ohne Kontext. Ist es ein User ? Ein User mit einigen Bestellinformationen angehängt? Wir wissen es nicht wirklich.

Ich arbeite lieber mit vollständigen Entitäten.

Sie haben recht, dass Sie oft Daten zurückholen, die Sie nicht verwenden, aber Sie können dies auf verschiedene Arten angehen:

  1. Speichern Sie die Entitäten aggressiv zwischen, so dass Sie den Lesepreis nur einmal aus der Datenbank bezahlen.
  2. Verbringen Sie mehr Zeit damit, Ihre Entitäten so zu modellieren, dass sie gut zwischen ihnen unterscheiden können. (Ziehen Sie in Betracht, eine große Entität in zwei kleinere Entitäten aufzuteilen, usw.)
  3. Betrachten Sie mehrere Versionen von Entitäten. Sie können einen User für das Back-End und vielleicht einen UserSmall für AJAX-Anrufe haben. Einer könnte 10 Eigenschaften haben und einer hat 3 Eigenschaften.

Die Nachteile der Arbeit mit Ad-hoc-Abfragen:

  1. Sie erhalten im Wesentlichen die gleichen Daten über viele Abfragen hinweg. Zum Beispiel schreiben Sie mit einem User Grunde die selbe select * für viele Anrufe. Ein Anruf erhält 8 von 10 Feldern, eines erhält 5 von 10, eines erhält 7 von 10. Warum nicht alle durch einen Anruf ersetzen, der 10 von 10 bekommt? Der Grund dafür ist, dass es ein Mord ist, den man neu faktorisieren / testen / verspotten kann.
  2. Es wird sehr schwierig, im Laufe der Zeit auf hohem Niveau über Ihren Code nachzudenken. Anstelle von Aussagen wie "Warum ist der User so langsam?" Am Ende werden Sie einmalige Abfragen ausfindig machen und Bugfixes sind daher eher klein und lokalisiert.
  3. Es ist wirklich schwer, die zugrunde liegende Technologie zu ersetzen. Wenn Sie jetzt alles in MySQL speichern und zu MongoDB wechseln möchten, ist es viel schwieriger, 100 Ad-hoc-Aufrufe zu ersetzen als eine Handvoll von Entitäten.

F: Ich habe zu viele Methoden in meinem Repository.

A: Ich habe noch nie einen anderen Weg gesehen, als Anrufe zu konsolidieren. Die Methode ruft Ihr Repository aufrichtig auf Funktionen in Ihrer Anwendung ab. Je mehr Funktionen, desto mehr datenspezifische Anrufe. Sie können Funktionen zurücksetzen und versuchen, ähnliche Anrufe zu einem zusammenzuführen.

Die Komplexität am Ende des Tages muss irgendwo existieren. Mit einem Repository-Muster haben wir es in die Repository-Oberfläche geschoben, anstatt vielleicht eine Menge gespeicherter Prozeduren zu erstellen.

Manchmal muss ich mir sagen: "Nun, es musste irgendwo geben! Es gibt keine Silberkugeln."

Vorwort: Ich versuche, das Repository-Muster in einer MVC-Architektur mit relationalen Datenbanken zu verwenden.

Ich habe vor kurzem begonnen, TDD in PHP zu lernen, und ich stelle fest, dass meine Datenbank viel zu eng mit dem Rest meiner Anwendung gekoppelt ist. Ich habe über Repositorys gelesen und einen IoC-Container verwendet , um ihn in meine Controller zu "injizieren". Sehr cooles Zeug. Jetzt haben Sie einige praktische Fragen zum Repository-Design. Betrachten Sie das folgende Beispiel.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Problem # 1: Zu viele Felder

Alle diese Suchmethoden verwenden einen Select all fields ( SELECT * ) -Ansatz. In meinen Apps versuche ich jedoch immer die Anzahl der Felder zu begrenzen, da dies oft Overheads verursacht und die Dinge verlangsamt. Wie gehst du mit diesen Mustern um?

Problem # 2: Zu viele Methoden

Während diese Klasse gerade gut aussieht, weiß ich, dass ich in einer echten Welt App viel mehr Methoden brauche. Beispielsweise:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Wie Sie sehen können, könnte es sehr, sehr lange Liste von möglichen Methoden geben. Und wenn Sie das Feldauswahlproblem oben hinzufügen, verschlechtert sich das Problem. In der Vergangenheit habe ich normalerweise diese Logik in meinen Controller eingebaut:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')->byCountry('Canada')->orderBy('name')->rows()

        return View::make('users', array('users' => $users))
    }

}

Mit meinem Repository-Ansatz möchte ich nicht damit enden:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Problem 3: Es ist unmöglich, eine Schnittstelle zu finden

Ich sehe den Vorteil bei der Verwendung von Schnittstellen für Repositories, so dass ich meine Implementierung (für Testzwecke oder andere) austauschen kann. Ich verstehe Schnittstellen so, dass sie einen Vertrag definieren, dem eine Implementierung folgen muss. Das ist großartig, bis Sie beginnen, Ihren Repositories zusätzliche Methoden wie findAllInCountry() . Jetzt muss ich meine Schnittstelle aktualisieren, um auch diese Methode zu haben, andernfalls haben andere Implementierungen es nicht, und das könnte meine Anwendung brechen. Das fühlt sich wahnsinnig an ... ein Fall, in dem der Schwanz mit dem Hund wedelt.

Spezifikationsmuster?

Dies führt zu der Annahme, dass das Repository nur eine feste Anzahl von Methoden haben sollte (wie save() , remove() , find() , findAll() , usw.). Aber wie führe ich dann spezifische Lookups durch? Ich habe von dem Spezifikationsmuster gehört , aber es scheint mir, dass dies nur eine ganze Reihe von Datensätzen reduziert (via IsSatisfiedBy() ), was IsSatisfiedBy() erhebliche Leistungsprobleme hat, wenn Sie aus einer Datenbank ziehen.

Hilfe?

Natürlich muss ich beim Arbeiten mit Repositories etwas überdenken. Kann jemand aufklären, wie dies am besten gehandhabt wird?


Dies sind einige verschiedene Lösungen, die ich gesehen habe. Es gibt Vor-und Nachteile für jeden von ihnen, aber es ist für Sie zu entscheiden.

Problem # 1: Zu viele Felder

Dies ist ein wichtiger Aspekt, insbesondere wenn Sie Index-Only-Scans berücksichtigen. Ich sehe zwei Lösungen zum Umgang mit diesem Problem. Sie können Ihre Funktionen aktualisieren, um einen optionalen Array-Parameter aufzunehmen, der eine Liste der zurückzugebenden Spalten enthält. Wenn dieser Parameter leer ist, würden Sie alle Spalten in der Abfrage zurückgeben. Das kann ein bisschen komisch sein; Basierend auf dem Parameter können Sie ein Objekt oder ein Array abrufen. Sie können auch alle Ihre Funktionen duplizieren, so dass Sie zwei verschiedene Funktionen haben, die dieselbe Abfrage ausführen, aber eine gibt ein Array von Spalten zurück und die andere gibt ein Objekt zurück.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Problem # 2: Zu viele Methoden

Ich habe vor einem Jahr kurz mit Propel ORM gearbeitet und das basiert auf dem, was ich aus dieser Erfahrung kenne. Propel hat die Möglichkeit, seine Klassenstruktur basierend auf dem vorhandenen Datenbankschema zu generieren. Es erstellt zwei Objekte für jede Tabelle. Das erste Objekt ist eine lange Liste von Zugriffsfunktionen ähnlich denen, die Sie gerade aufgelistet haben. findByAttribute($attribute_value) . Das nächste Objekt erbt von diesem ersten Objekt. Sie können dieses untergeordnete Objekt aktualisieren, um komplexere Getterfunktionen zu erstellen.

Eine andere Lösung wäre die Verwendung von __call() , um nicht definierte Funktionen auf etwas umsetzbares zu __call() . Ihre __call Methode wäre in der Lage, findById und findByName in verschiedenen Abfragen zu analysieren.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Ich hoffe das hilft zumindest einiges was.


Ich dachte, ich würde einen Sprung machen, um meine eigene Frage zu beantworten. Was folgt, ist nur eine Möglichkeit, die Probleme 1-3 in meiner ursprünglichen Frage zu lösen.

Haftungsausschluss: Ich verwende nicht immer die richtigen Begriffe bei der Beschreibung von Mustern oder Techniken. Das tut mir leid.

Die Ziele:

  • Erstellen Sie ein vollständiges Beispiel eines Basis-Controllers zum Anzeigen und Bearbeiten von Users .
  • Der gesamte Code muss vollständig testbar und anschaulich sein.
  • Der Controller sollte keine Ahnung haben, wo die Daten gespeichert sind (was bedeutet, dass sie geändert werden können).
  • Beispiel zum Anzeigen einer SQL-Implementierung (am häufigsten).
  • Für maximale Leistung sollten Controller nur die Daten erhalten, die sie benötigen - keine zusätzlichen Felder.
  • Die Implementierung sollte eine Art Data Mapper nutzen, um die Entwicklung zu erleichtern.
  • Die Implementierung sollte in der Lage sein, komplexe Datensuchvorgänge durchzuführen.

Die Lösung

Ich spalte meine persistente Speicher (Datenbank) Interaktion in zwei Kategorien: R (Lesen) und CUD (Erstellen, Aktualisieren, Löschen). Meine Erfahrung ist, dass Lesevorgänge wirklich dazu führen, dass eine Anwendung langsamer wird. Und während Datenmanipulation (CUD) tatsächlich langsamer ist, passiert es viel weniger häufig und ist daher viel weniger bedenklich.

CUD (Erstellen, Aktualisieren, Löschen) ist einfach. Dies beinhaltet die Arbeit mit aktuellen models , die dann zur Persistenz an meine Repositories . Beachten Sie, dass meine Repositories immer noch eine Lese-Methode bereitstellen, aber einfach zur Objekt-Erstellung, nicht angezeigt werden. Mehr dazu später.

R (Lesen) ist nicht so einfach. Keine Modelle hier, nur Wertobjekte . Verwenden Sie Arrays, wenn Sie bevorzugen . Diese Objekte können ein einzelnes Modell oder eine Mischung aus vielen Modellen repräsentieren, alles wirklich. Diese sind alleine nicht sehr interessant, aber wie sie erzeugt werden, ist. Ich benutze, was ich Query Objects .

Der Code:

Benutzermodell

Beginnen wir einfach mit unserem grundlegenden Benutzermodell. Beachten Sie, dass es überhaupt keine ORM-Erweiterung oder Datenbank-Daten gibt. Einfach nur Modell Ruhm. Fügen Sie Ihre Getter, Setter, Validierung, was auch immer hinzu.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Repository-Schnittstelle

Bevor ich mein Benutzerrepository erstelle, möchte ich meine Repository-Schnittstelle erstellen. Dies definiert den "Vertrag", den Repositories befolgen müssen, um von meinem Controller verwendet zu werden. Denken Sie daran, dass mein Controller nicht weiß, wo die Daten tatsächlich gespeichert sind.

Beachten Sie, dass meine Repositories nur diese drei Methoden enthalten. Die save() -Methode ist verantwortlich für das Erstellen und Aktualisieren von Benutzern, einfach abhängig davon, ob das Benutzerobjekt eine ID gesetzt hat oder nicht.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

SQL-Repository-Implementierung

Jetzt um meine Implementierung der Schnittstelle zu erstellen. Wie gesagt, mein Beispiel würde mit einer SQL-Datenbank sein. Beachten Sie die Verwendung eines Data Mapper , um zu verhindern, dass sich wiederholende SQL-Abfragen geschrieben werden müssen.

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

Abfrageobjektschnittstelle

Jetzt mit CUD (Erstellen, Aktualisieren, Löschen), die von unserem Repository erledigt werden, können wir uns auf das R (Lesen) konzentrieren. Abfrageobjekte sind einfach eine Kapselung eines Typs von Datensuchlogik. Sie sind keine Abfrageersteller. Indem wir es wie unser Repository abstrahieren, können wir seine Implementierung ändern und es einfacher testen. Ein Beispiel für ein Abfrageobjekt könnte eine AllUsersQuery oder AllActiveUsersQuery oder sogar MostCommonUserFirstNames .

Sie denken vielleicht "kann ich nicht einfach Methoden in meinen Repositories für diese Abfragen erstellen?" Ja, aber hier mache ich das nicht:

  • Meine Repositories sind für das Arbeiten mit Modellobjekten gedacht. Warum sollte ich in einer Real-World-App jemals das password aufrufen müssen, wenn ich alle meine Benutzer auflisten möchte?
  • Repositories sind oft modellspezifisch, aber Abfragen beinhalten oft mehr als ein Modell. In welches Repository legen Sie Ihre Methode?
  • Das hält meine Repositories sehr einfach - nicht eine aufgeblähte Klasse von Methoden.
  • Alle Abfragen sind jetzt in eigenen Klassen organisiert.
  • Wirklich, an diesem Punkt existieren Repositories einfach, um meine Datenbankschicht zu abstrahieren.

Für mein Beispiel werde ich ein Abfrageobjekt erstellen, um "AllUsers" zu suchen. Hier ist die Schnittstelle:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Abfrage Objekt Implementierung

Hier können wir wieder einen Data Mapper verwenden, um die Entwicklung zu beschleunigen. Beachten Sie, dass ich eine Optimierung des zurückgegebenen Datasets - der Felder - zulasse. Dies ist ungefähr so ​​weit wie ich mit der Manipulation der durchgeführten Abfrage gehen möchte. Denken Sie daran, dass meine Abfrageobjekte keine Abfrageersteller sind. Sie führen einfach eine spezifische Abfrage durch. Da ich jedoch weiß, dass ich diesen wahrscheinlich in vielen verschiedenen Situationen verwenden werde, gebe ich mir die Möglichkeit, die Felder zu spezifizieren. Ich will nie Felder zurückgeben, die ich nicht brauche!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

Bevor ich mich dem Controller zuwende, möchte ich ein anderes Beispiel zeigen, um zu zeigen, wie mächtig das ist. Vielleicht habe ich eine Reporting-Engine und muss einen Bericht für AllOverdueAccounts . Dies könnte mit meinem Data Mapper schwierig sein, und ich möchte in dieser Situation etwas aktuelles SQL schreiben. Kein Problem, hier könnte das Abfrageobjekt aussehen:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

Dies hält meine ganze Logik für diesen Bericht in einer Klasse und es ist einfach zu testen. Ich kann es nach Herzenslust ausstrahlen oder sogar eine andere Implementierung verwenden.

Der Controller

Jetzt kommt der spaßige Teil - bringt alle Teile zusammen. Beachten Sie, dass ich die Abhängigkeitsinjektion verwende. Normalerweise werden Abhängigkeiten in den Konstruktor eingefügt, aber ich bevorzuge es, sie direkt in meine Controller-Methoden (Routen) zu injizieren. Dadurch wird der Objektgraph des Controllers minimiert und ich finde ihn besser lesbar. Wenn Sie diesen Ansatz nicht mögen, verwenden Sie einfach die traditionelle Konstruktormethode.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

Abschließende Gedanken:

Die wichtigen Dinge, die hier zu beachten sind, sind, dass ich beim Modifizieren (Erstellen, Aktualisieren oder Löschen) von Entitäten mit realen Modellobjekten arbeite und die Persistenz durch meine Repositories durchführe.

Wenn ich Daten anzeigen und an die Ansichten senden möchte, arbeite ich jedoch nicht mit Modellobjekten, sondern mit einfachen alten Wertobjekten. Ich wähle nur die Felder aus, die ich brauche, und es ist so konzipiert, dass ich meine Datensuchleistung maximieren kann.

Meine Repositories bleiben sehr sauber und stattdessen wird diese "Unordnung" in meinen Modellabfragen organisiert.

Ich benutze einen Data Mapper, um bei der Entwicklung zu helfen, da es einfach lächerlich ist, repetitives SQL für allgemeine Aufgaben zu schreiben. Sie können jedoch absolut SQL schreiben, wo es benötigt wird (komplizierte Abfragen, Berichte usw.). Und wenn du es tust, ist es schön versteckt in einer richtig benannten Klasse.

Ich würde gerne hören, wie ich dich anspreche!

Juli 2015 Aktualisierung:

Ich wurde in den Kommentaren gefragt, wo ich das alles gefunden habe. Nun, nicht so weit weg. Ehrlich gesagt, mag ich Repositories immer noch nicht wirklich. Ich finde sie Overkill für grundlegende Nachfragen (vor allem, wenn Sie bereits ein ORM verwenden) und unordentlich, wenn Sie mit komplizierteren Abfragen arbeiten.

Ich arbeite generell mit einem ActiveRecord-ORM-Stil, daher referenziere ich diese Modelle meistens direkt in meiner Anwendung. In Situationen, in denen ich komplexere Abfragen habe, verwende ich jedoch Abfrageobjekte, um diese wiederverwendbar zu machen. Ich sollte auch darauf achten, dass ich meine Modelle immer in meine Methoden einfüge, so dass sie in meinen Tests leichter zu verspotten sind.


Ich füge ein wenig hinzu, da ich gerade versuche, das alles selbst zu erfassen.

# 1 und 2

Dies ist ein perfekter Ort für Ihr ORM, um schwer zu heben. Wenn Sie ein Modell verwenden, das eine Art von ORM implementiert, können Sie einfach seine Methoden verwenden, um sich um diese Dinge zu kümmern. Machen Sie Ihre eigene Bestellung durch Funktionen, die die Eloquent-Methoden implementieren, wenn Sie müssen. Mit Eloquent zum Beispiel:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

Was Sie suchen, ist ein ORM. Kein Grund, warum Ihr Repository nicht auf einem basieren kann. Dies würde erfordern, dass der Benutzer eloquent erweitert, aber ich persönlich sehe das nicht als ein Problem.

Wenn Sie jedoch ein ORM vermeiden möchten, müssten Sie dann "selbst rollen", um das zu erhalten, wonach Sie suchen.

#3

Schnittstellen sollen keine harten und schnellen Anforderungen sein. Etwas kann eine Schnittstelle implementieren und hinzufügen. Was es nicht tun kann, ist es, eine erforderliche Funktion dieser Schnittstelle nicht zu implementieren. Sie können auch Schnittstellen wie Klassen erweitern, um DRY zu vermeiden.

Das heißt, ich fange gerade erst an zu greifen, aber diese Erkenntnisse haben mir geholfen.







repository-pattern