web-dev-qa-db-de.com

Richtiges Repository-Musterdesign in PHP?

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

Ich habe vor kurzem angefangen, 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 über die Verwendung eines IoC-Container in meine Controller "injiziert". Sehr cooles Zeug. Jetzt haben wir 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)
    {
    }
}

Ausgabe 1: Zu viele Felder

Alle diese Suchmethoden verwenden die Methode select all fields (SELECT *). In meinen Apps versuche ich jedoch immer, die Anzahl der Felder, die ich bekomme, zu begrenzen, da dies häufig zusätzlichen Aufwand verursacht und die Dinge verlangsamt. Wie gehen Sie mit denen um, die dieses Muster verwenden?

Ausgabe 2: Zu viele Methoden

Während diese Klasse im Moment schön aussieht, weiß ich, dass ich in einer realen App viel mehr Methoden brauche. Zum Beispiel:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Usw.

Wie Sie sehen, könnte es eine sehr, sehr lange Liste möglicher Methoden geben. Wenn Sie dann das Feldauswahlproblem oben hinzufügen, verschlechtert sich das Problem. In der Vergangenheit würde ich normalerweise diese ganze Logik in meinen Controller einbauen:

<?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 nicht möglich, eine Schnittstelle zu finden

Ich sehe den Vorteil der Verwendung von Schnittstellen für Repositorys, sodass ich meine Implementierung austauschen kann (zu Testzwecken oder zu anderen Zwecken). Ich verstehe Schnittstellen so, dass sie einen Vertrag definieren, dem eine Implementierung folgen muss. Dies ist großartig, bis Sie mit dem Hinzufügen zusätzlicher Methoden zu Ihren Repositorys wie findAllInCountry() beginnen. Jetzt muss ich meine Schnittstelle aktualisieren, um auch diese Methode verwenden zu können. Andernfalls können andere Implementierungen dies nicht tun, und dies könnte meine Anwendung beschädigen. Dadurch fühlt es sich wahnsinnig an ... ein Fall, in dem der Hund mit dem Schwanz wedelt.

Spezifikationsmuster?

Dies lässt mich glauben, dass das Repository nur eine festgelegte Anzahl von Methoden haben sollte (wie save(), remove(), find(), findAll() usw.). Aber wie führe ich dann bestimmte Suchvorgänge durch? Ich habe von Specification Pattern gehört, aber es scheint mir, dass dadurch nur ein ganzer Satz Datensätze reduziert wird (über IsSatisfiedBy()).

Hilfe?

Klar muss ich die Dinge ein wenig überdenken, wenn ich mit Repositories arbeite. Kann jemand aufklären, wie dies am besten gehandhabt wird?

244
Jonathan

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

Haftungsausschluss: Ich kann nicht immer die richtigen Begriffe verwenden, wenn ich Muster oder Techniken beschreibe. Tut mir leid.

Die Ziele:

  • Erstellen Sie ein vollständiges Beispiel für einen Basiscontroller zum Anzeigen und Bearbeiten von Users.
  • Der gesamte Code muss vollständig testbar und spottbar sein.
  • Der Controller sollte keine Ahnung haben, wo die Daten gespeichert werden (was bedeutet, dass sie geändert werden können).
  • Beispiel zur Darstellung einer SQL-Implementierung (am häufigsten).
  • Für eine maximale Leistung sollten Controller nur die Daten erhalten, die sie benötigen - keine zusätzlichen Felder.
  • Die Implementierung sollte eine Art Data-Mapper zur Erleichterung der Entwicklung nutzen.
  • Die Implementierung sollte die Fähigkeit besitzen, komplexe Datensuchen durchzuführen.

Die Lösung

Ich teile meine permanente Speicherinteraktion (Datenbank) in zwei Kategorien auf: R (Lesen) und CUD (Erstellen, Aktualisieren, Löschen). Ich habe die Erfahrung gemacht, dass Lesen wirklich dazu führt, dass eine Anwendung langsamer wird. Und während die Datenmanipulation (CUD) tatsächlich langsamer ist, geschieht dies viel seltener und ist daher weniger problematisch.

CUD (Erstellen, Aktualisieren, Löschen) ist einfach. Dies beinhaltet das Arbeiten mit aktuellen Modellen , die dann zur Persistenz an meine Repositories übergeben werden. Beachten Sie, dass meine Repositorys weiterhin eine Read-Methode bereitstellen, jedoch lediglich zur Objekterstellung und nicht zur Anzeige. Dazu später mehr.

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

Der Code:

Benutzermodell

Beginnen wir einfach mit unserem grundlegenden Benutzermodell. Beachten Sie, dass es überhaupt keine ORM-Erweiterung oder Datenbanksachen gibt. Nur reiner Vorbild. Fügen Sie Ihre Getter, Setter, Validierung oder 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. Dadurch wird der "Vertrag" definiert, dem die Repositorys folgen müssen, damit sie von meinem Controller verwendet werden können. Denken Sie daran, dass mein Controller nicht weiß, wo die Daten tatsächlich gespeichert sind.

Beachten Sie, dass meine Repositorys nur jeweils diese drei Methoden enthalten. Die save()-Methode ist für das Erstellen und Aktualisieren von Benutzern verantwortlich. Dies hängt davon ab, ob für das Benutzerobjekt eine ID festgelegt ist.

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

SQL-Repository-Implementierung

Nun erstellen Sie meine Implementierung der Schnittstelle. Wie gesagt, mein Beispiel sollte eine SQL-Datenbank sein. Beachten Sie die Verwendung eines data mapper , um zu verhindern, dass sich wiederholende SQL-Abfragen schreiben 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, da CUD (Erstellen, Aktualisieren, Löschen) von unserem Repository erledigt wird, können wir uns auf das R (Lesen) konzentrieren. Abfrageobjekte sind einfach eine Kapselung irgendeiner Art von Nachschlagelogik. Sie sind nicht Abfrageersteller. Indem wir es wie unser Repository abstrahieren, können wir die Implementierung ändern und einfacher testen. Ein Beispiel für ein Abfrageobjekt kann AllUsersQuery oder AllActiveUsersQuery oder sogar MostCommonUserFirstNames sein.

Möglicherweise denken Sie "Kann ich nicht einfach Methoden für diese Abfragen in meinen Repositorys erstellen?" Ja, aber hier mache ich das nicht:

  • Meine Repositorys sind für die Arbeit mit Modellobjekten gedacht. Warum sollte ich in einer realen App das Feld password benötigen, wenn ich alle Benutzer auflisten möchte?
  • Repositorys sind oft modellspezifisch, jedoch umfassen Abfragen häufig mehr als ein Modell. In welchem ​​Repository setzen Sie Ihre Methode ein?
  • Dadurch bleiben meine Repositorys sehr einfach - keine aufgeblähte Klasse von Methoden.
  • Alle Abfragen sind jetzt in ihren eigenen Klassen organisiert.
  • Zu diesem Zeitpunkt gibt es tatsächlich Repositorys, die lediglich meine Datenbankschicht abstrahieren.Für mein Beispiel werde ich ein Abfrageobjekt erstellen, um nach "AllUsers" zu suchen. Hier ist die Schnittstelle:.

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

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

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..."; } }

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

Die wichtigsten Punkte, die hier zu beachten sind, sind, dass ich beim Ändern (Erstellen, Aktualisieren oder Löschen) von Entitäten mit realen Modellobjekten arbeite und die Beständigkeit durch meine Repositorys durchführe.

Beim Anzeigen (Auswählen von Daten und Senden der Ansichten an die Ansichten) arbeite ich jedoch nicht mit Modellobjekten, sondern eher mit alten Wertobjekten. Ich wähle nur die Felder aus, die ich brauche, und es ist so konzipiert, dass ich meine Daten-Lookup-Leistung maximieren kann.

Meine Repositories bleiben sehr sauber und stattdessen ist dieses "Durcheinander" in meinen Modellabfragen organisiert.

Ich benutze einen Data Mapper, um bei der Entwicklung zu helfen, da es einfach lächerlich ist, sich wiederholendes SQL für allgemeine Aufgaben zu schreiben. Sie können jedoch absolut SQL schreiben, wenn dies erforderlich ist (komplizierte Abfragen, Berichterstellung usw.). Und wenn Sie es tun, ist es schön in einer richtig benannten Klasse versteckt.

Ich würde gerne hören, wie Sie meine Herangehensweise annehmen!.


Aktualisierung Juli 2015:

Ich wurde in den Kommentaren gefragt, wo ich all das habe. Nun, nicht so weit weg. Ehrlich gesagt mag ich Repositories immer noch nicht wirklich. Ich finde sie übertrieben für grundlegende Suchvorgänge (insbesondere wenn Sie bereits einen ORM verwenden) und unübersichtlich, wenn Sie mit komplizierteren Abfragen arbeiten.

Im Allgemeinen arbeite ich mit einem ActiveRecord-Style-ORM. Daher werde ich diese Modelle meistens direkt in meiner Anwendung referenzieren. In Situationen, in denen ich komplexere Abfragen habe, verwende ich jedoch Abfrageobjekte, um diese wiederverwendbar zu machen. Ich sollte auch beachten, dass ich meine Modelle immer in meine Methoden einspreche, so dass sie in meinen Tests leichter zu machen sind.

I generally work with an ActiveRecord style ORM, so most often I'll just reference those models directly throughout my application. However, in situations where I have more complex queries, I'll use query objects to make these more reusable. I should also note that I always inject my models into my methods, making them easier to mock in my tests.

179
Jonathan

Aufgrund meiner Erfahrung finden Sie hier einige Antworten auf Ihre Fragen:

F: Wie gehen wir mit Feldern um, die wir nicht brauchen?

A: Aus meiner Erfahrung läuft es wirklich auf den Umgang mit kompletten Entitäten statt auf Ad-hoc-Anfragen.

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

Eine Ad-hoc-Abfrage gibt einige Daten zurück, aber darüber hinaus wissen wir nichts. Da die Daten um die Anwendung herum übertragen werden, erfolgt dies ohne Kontext. Ist es eine User? Eine User mit einigen Order-Informationen? Wir wissen es nicht wirklich.

Ich arbeite lieber mit ganzen Entitäten. 

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

  1. Zwischenspeichern Sie die Entitäten aggressiv, sodass Sie den Lesepreis nur einmal aus der Datenbank bezahlen.
  2. Verbringen Sie mehr Zeit damit, Ihre Entitäten zu modellieren, damit sie sich gut unterscheiden. (Erwägen Sie, eine große Entität in zwei kleinere Entitäten aufzuteilen usw.).
  3. Erwägen Sie mehrere Versionen von Entitäten. Sie können eine User für das Backend und möglicherweise eine UserSmall für Aufrufe von AJAX haben. Eine könnte 10 Eigenschaften haben und eine hat 3 Eigenschaften.

Die Nachteile der Arbeit mit Ad-hoc-Abfragen:

  1. Bei vielen Abfragen erhalten Sie im Wesentlichen dieselben Daten. Mit User schreiben Sie zum Beispiel für viele Anrufe im Wesentlichen den gleichen select *. Ein Anruf erhält 8 von 10 Feldern, eines erhält 5 von 10, ein Anruf 7 von 10. Der Grund, warum dies schlecht ist, ist, dass es ein Mord ist, einen neuen Faktor/Test/Mock durchzuführen.
  2. Es wird sehr schwierig, im Laufe der Zeit auf hoher Ebene über Ihren Code nachzudenken. Anstelle von Aussagen wie "Warum ist User so langsam?" Am Ende suchen Sie nach einmaligen Abfragen, und daher sind Fehlerbehebungen 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 Entitäten.

F: Ich habe zu viele Methoden in meinem Repository.

A: Ich habe nichts anderes gesehen, als Anrufe zu konsolidieren. Die Methodenaufrufe in Ihrem Repository sind wirklich Funktionen in Ihrer Anwendung zugeordnet. Je mehr Funktionen, desto mehr datenspezifische Anrufe. Sie können auf Funktionen zurückgreifen und versuchen, ähnliche Anrufe in einer zusammenzuführen.

Die Komplexität am Ende des Tages muss irgendwo bestehen. Mit einem Repository-Muster haben wir es in die Repository-Benutzeroberfläche verschoben, anstatt möglicherweise eine Reihe von gespeicherten Prozeduren zu erstellen.

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

43
ryan1234

Ich verwende folgende Schnittstellen: 

  • Repository - lädt, fügt, aktualisiert und löscht Entitäten
  • Selector - Findet Entitäten basierend auf Filtern in einem Repository
  • Filter - kapselt die Filterlogik ein

Meine Repository ist datenbankunabhängig. in der Tat gibt es keine Beharrlichkeit an; Es könnte alles sein: SQL-Datenbank, XML-Datei, Remote-Service, ein Alien aus dem Weltraum usw. Für die Suchfunktionen erstellt die Repository eine Selector, die gefiltert werden kann, LIMIT-, sortiert und gezählt wird. Am Ende ruft der Selector eine oder mehrere Entities von der Persistenz ab.

Hier ist ein Beispielcode:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

Dann eine Implementierung:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

Die Idee ist, dass das generische SelectorFilter verwendet, die Implementierung SqlSelector jedoch SqlFilter; SqlSelectorFilterAdapter passt eine generische Filter an eine konkrete SqlFilter an. 

Der Clientcode erstellt Filter-Objekte (dh generische Filter). In der konkreten Implementierung des Selektors werden diese Filter jedoch in SQL-Filter umgewandelt.

Andere Selektorimplementierungen, wie InMemorySelector, transformieren von Filter in InMemoryFilter unter Verwendung ihrer spezifischen InMemorySelectorFilterAdapter; Jede Selector-Implementierung wird mit einem eigenen Filteradapter geliefert.

Bei Verwendung dieser Strategie kümmert sich mein Client-Code (in der Busines-Schicht) nicht um ein bestimmtes Repository oder eine Auswahlimplementierung.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

P.S. Dies ist eine Vereinfachung meines echten Codes

14

Ich werde ein wenig dazu hinzufügen, da ich gerade versuche, das alles selbst zu erfassen. 

# 1 und 2

Dies ist der perfekte Ort für Ihren ORM, um das schwere Heben durchzuführen. Wenn Sie ein Modell verwenden, das eine Art ORM implementiert, können Sie einfach die entsprechenden Methoden verwenden, um sich um diese Dinge zu kümmern. Erstellen Sie Ihre eigenen orderBy-Funktionen, die bei Bedarf die Eloquent-Methoden implementieren. Verwenden Sie zum Beispiel Eloquent:

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 basiert sein kann. Dies würde erfordern, dass der Benutzer eloquent erweitert, aber ich persönlich sehe das nicht als Problem an.

Wenn Sie jedoch einen ORM vermeiden möchten, müssen Sie dann "eigene Rollen" lassen, um das Gesuchte zu erhalten.

#3

Schnittstellen sind keine harten und schnellen Anforderungen. Etwas kann eine Schnittstelle implementieren und ergänzen. Was es nicht kann, ist die Implementierung einer erforderlichen Funktion dieser Schnittstelle. Sie können Schnittstellen wie Klassen auch erweitern, um die Dinge trocken zu halten.

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

4
Will Meldon

Ich kann nur sagen, wie wir (in meiner Firma) damit umgehen. Zuallererst ist Leistung für uns kein großes Problem, aber sauberen/richtigen Code zu haben, ist.

Zunächst definieren wir Modelle wie UserModel, die ORM verwenden, um UserEntity-Objekte zu erstellen. Wenn ein UserEntity aus einem Modell geladen wird, werden alle Felder geladen. Für Felder, die auf fremde Entitäten verweisen, verwenden wir das entsprechende fremde Modell, um die entsprechenden Entitäten zu erstellen. Für diese Entitäten werden die Daten auf Anforderung geladen. Nun könnte Ihre erste Reaktion sein ... ??? ... !!! Ich möchte Ihnen ein Beispiel geben:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

In unserem Fall ist $db ein ORM, der Entitäten laden kann. Das Modell weist den ORM an, eine Gruppe von Entitäten eines bestimmten Typs zu laden. Der ORM enthält eine Zuordnung und verwendet diese, um alle Felder für diese Entität in die Entität einzufügen. Bei Fremdfeldern werden jedoch nur die IDs dieser Objekte geladen. In diesem Fall erstellt OrderModelOrderEntitys nur mit den IDs der referenzierten Aufträge. Wenn PersistentEntity::getField von OrderEntity aufgerufen wird, weist die Entität ihr Modell an, alle Felder faul in die OrderEntitys zu laden. Alle OrderEntitys, die einer UserEntity zugeordnet sind, werden als eine Ergebnismenge behandelt und sofort geladen.

Die Magie dabei ist, dass unser Modell und unser ORM alle Daten in die Entitäten einfügen und dass Entitäten lediglich Wrapper-Funktionen für die generische getField-Methode bereitstellen, die von PersistentEntity bereitgestellt wird. Zusammenfassend laden wir immer alle Felder, aber Felder, die auf eine Fremdeinheit verweisen, werden bei Bedarf geladen. Das Laden einer Reihe von Feldern ist nicht wirklich ein Leistungsproblem. Laden Sie alle möglichen fremden Entitäten, wäre dies jedoch ein RIESIGER Leistungsabfall.

Nun zum Laden einer bestimmten Gruppe von Benutzern, basierend auf einer where-Klausel. Wir bieten ein objektorientiertes Klassenpaket an, mit dem Sie einfache Ausdrücke angeben können, die miteinander verklebt werden können. Im Beispielcode habe ich es GetOptions genannt. Es ist ein Wrapper für alle möglichen Optionen für eine Auswahlabfrage. Es enthält eine Sammlung von Where-Klauseln, eine Group-by-Klausel und alles andere. Unsere Where-Klauseln sind ziemlich kompliziert, aber Sie könnten natürlich eine einfachere Version erstellen.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Eine einfachste Version dieses Systems besteht darin, den WHERE-Teil der Abfrage als Zeichenfolge direkt an das Modell zu übergeben.

Es tut mir leid für diese ziemlich komplizierte Antwort. Ich habe versucht, unser Framework so schnell und klar wie möglich zusammenzufassen. Wenn Sie weitere Fragen haben, können Sie sie gerne stellen und ich werde meine Antwort aktualisieren.

BEARBEITEN: Wenn Sie einige Felder wirklich nicht sofort laden möchten, können Sie in Ihrem ORM-Mapping eine Option zum verzögerten Laden angeben. Da alle Felder schließlich über die getField-Methode geladen werden, können Sie einige Felder in letzter Minute laden, wenn diese Methode aufgerufen wird. Dies ist kein sehr großes Problem in PHP, aber ich würde es nicht für andere Systeme empfehlen.

3
TFennis

Dies sind einige verschiedene Lösungen, die ich gesehen habe. Jeder von ihnen hat Vor- und Nachteile, aber Sie müssen entscheiden.

Problem 1: Zu viele Felder

Dies ist ein wichtiger Aspekt, insbesondere wenn Sie Nur-Index-Scans berücksichtigen. Ich sehe zwei Lösungen, um mit diesem Problem umzugehen. 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, geben Sie alle Spalten in der Abfrage zurück. Das kann etwas komisch sein; basierend auf dem Parameter können Sie ein Objekt oder ein Array abrufen. Sie können auch alle Ihre Funktionen duplizieren, sodass Sie zwei unterschiedliche Funktionen haben, die dieselbe Abfrage ausführen, wobei eine ein Array von Spalten und die andere ein Objekt zurückgibt.

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 dies basiert auf dem, woran ich mich aus dieser Erfahrung erinnern kann. Propel hat die Option, seine Klassenstruktur basierend auf dem vorhandenen Datenbankschema zu generieren. Es werden zwei Objekte für jede Tabelle erstellt. Das erste Objekt ist eine lange Liste von Zugriffsfunktionen, die Ihrer aktuellen Liste ähneln. findByAttribute($attribute_value). Das nächste Objekt erbt von diesem ersten Objekt. Sie können dieses untergeordnete Objekt aktualisieren, um Ihre komplexeren Getter-Funktionen einzubauen.

Eine andere Lösung wäre die Verwendung von __call(), um nicht definierte Funktionen auf etwas umsetzbares abzubilden. Ihre __call -Methode könnte findById und findByName in verschiedene Abfragen zerlegen.

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 wenigstens ein wenig was.

3
Logan Bailey

Ich empfehle https://packagist.org/packages/prettus/l5-repository als Hersteller, um Repositorys/Kriterien usw. in Laravel5: D zu implementieren

0
abenevaut

Ich stimme @ ryan1234 zu, dass Sie vollständige Objekte innerhalb des Codes übergeben und generische Abfragemethoden verwenden sollten, um diese Objekte abzurufen. 

Model::where(['attr1' => 'val1'])->get();

Für den externen/Endpoint-Einsatz mag ich die GraphQL-Methode sehr.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}
0
AVProgrammer