Object Sets Laden und Iterieren von Objekten

In Konquadrat kann man Objekte mit Hilfe des ObjectFinder aus der Datenbank zu laden.

Um speziell einzelne Objekte zu laden anhand der id, des Namens oder der UUID gibt es folgende Methoden:

com\setasign\SetaSite\Block\BlockRenderException - Error in block "DocBlock Load SingleObject" of type "com\setasign\SetaSite\CustomModules\ApiDoc\Blocks\DocBlock\DocBlock"

Falls man nach einem oder mehreren Objekten suchen möchte mit oder ohne bestimmte Filter gibt es die Methode "ObjectFinder::get(Selector $selector): AbstractObject|Undefined|ObjectSet".  Beispiel:

PHP
/**
 * @var \com\setasign\Konquadrat\Object\ObjectFinder $objectFinder
 */
$klaus = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Participant')
    ->where('FirstName', '=', 'Klaus')
    ->limit(1)
);


$participants = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Participant')
);

Wenn bei ObjectFinder::get() der Selektor ein "limit(1)" definiert hat ist der Rückgabetyp immer "AbstractObject|Undefined". $klaus wird der "erste" Teilnehmer sein mit dem Vornamen "Klaus". Falls es mehrere Teilnehmer mit diesem Vornamen gibt ist es undefiniert, welcher davon zurück kommt (falls dies nicht gewünscht ist, muss noch eine Sortierung per "orderBy" mitgegeben werden). Falls kein Teilnehmer mit diesem Vornamen gefunden werden kann, wird ein Objekt vom Typ Undefined zurück gegeben. 

Ansonsten ohne Limit oder mit einem höherem Limit als 1 wird immer ein ObjectSet zurückgegeben.

Seit Konquadrat v1.18.2.0 ist es möglich in einem ObjectSelector auch Magic-Felder als Filter oder Sortierung zu verwenden.

Das Auflösen von Referenzen ist im ObjectSelector momentan nicht möglich.

ObjectSet

 Ein ObjectSet ist einfach gesagt eine Sammlung von Objekten. Zu diesen Objekten muss es noch keine Objekt-Instanz geben im InstancePool. Ein ObjectSet implementiert \Iterator und \Countable. Für den Zugriff auf  die Objekte in dem ObjectSet gibt es unter anderem folgende Methoden:

getActualScope()

Returns the object ids of the objects in the actual scope.

getById()

Return the AbstractObject with the id $id.

hasObject()

Checks whether the object is inside the objectSet.

setActualScope()

toArray()

Returns the actual scope as array.

N+1 Problem

PHP
$llamas = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Llama')
);

foreach ($llamas as $llama) {
    echo $llama->id . '-' . $llama->Hat->Color . '<br/>';
}

Dieser Code würde dafür sorgen, dass für jedes Lama ein Query ausgeführt werden würde um das Referenzfeld "Hat" aufzulösen. Das kann dafür sorgen, dass bei 5000 Lamas mit 5000 verschiedenen Hüten 5000 Queries für das Laden der Hüte und 1 Query für das Laden der Lamas ausgeführt werden (daher der Name "N+1" Problem).

Um dieses Problem zu umgehen gibt es mehrere Varianten, jedoch basieren alle auf sogenanntem "Eager Loading".

Das ObjectSet versucht zunächst die benötigten Objekte aus dem InstancePool zu holen, man könnte also alle Hüte vorher als Instanz vorhalten: 

PHP
$llamas = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Llamas')
);

$hats = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Hat')
);
// creates an instance of all hats
$hats->toArray(true);

foreach ($llamas as $llama) {...}

Diese Variante kann verwendet werden, wenn man weiß dass es nicht viele verschiedene Objekt-Typen gibt oder man sowieso alle Objekte laden wird die es gibt. Falls es jedoch ähnlich viele Hüte wie Lamas gibt oder sobald zum Beispiel eine Pagination über $llamas läuft macht es selten Sinn alle Objekte vom Typ "Hat" zu laden.

Das ObjectSet bietet auch passende Methoden an um ein ObjectSet per "Eager Loading" vorzubereiten: 

PHP
$llamas = $objectFinder->get(
    $objectFinder->createSelector()
    ->setType('Llama')
    ->limit($limit, $offset)
);

$llamas->withReferences(['Llamas' => ['Hat']]);

foreach ($llamas as $llama) {...}

Bei diesem Code werden zunächst alle Lamas geladen und anschließend werden für alle geladenen Lamas die dazugehörigen Hüte geladen. Falls es z.B. keine Lamas gibt, passiert in withReferences() auch nichts.

Falls man anschließend noch von allen Hüten zum Beispiel die Childs laden möchte geht dies wie folgt: 

PHP
$hats = $llamas->withReferences(['Llamas' => ['Hat']])->get();
$hats->withChilds();

Falls aus dem ObjectSet weitere "Unter-ObjectSets" erstellt werden über z.B. "getChilds" ist dieses mit dem ursprünglichem ObjectSet verbunden und beide ObjectSets teilen sie die selben Daten (nicht den Scope, also welche Objekte aktuell iteriert werden).

Das ObjectSet bietet insgesamt folgende Methoden für das "Eager Loading" an:

resolveMagic()

resolveMagicWithParam()

withAllChilds()

This method will load all objects below the actual scope via the path.

withChilds()

withParents()

withReferences()

withReferencingObjects()

SmartObjectSet

Das normale "Eager Loading" hat den Nachteil, dass man manuell als Programmier darauf achten muss, dass die Objekte richtig vorgeladen werden. Stark inspiriert von diesem Blog-Eintrag gibt es seit Konquadrat v1.18.2.0 das sogenannte SmartObjectSet. Bei diesem muss man nicht mehr manuell vor dem Iterieren alle References, ReferencingObjects, Childs und Magic-Felder auflösen, stattdessen wird dies automatisch aufgelöst.

Das Ganze hat jedoch einen entscheidenden Nachteil, weswegen das SmartObjectSet nicht das Standardverhalten ist: das SmartObjectSet verzichtet KOMPLETT auf die Verwendung vom InstancePool. Das bedeutet dass es von einem Objekt mehrere Instanzen geben kann, was normalerweise durch den InstancePool verhindert wird. 

Das SmartObjectSet ist nicht für jeden Anwendungsfall geeignet!

Code-Beispiel:

PHP
$llamas = $objectFinder->getSmartSet(
    $objectFinder->createSelector()
    ->setType('Llama')
);

foreach ($llamas as $llama) {
    echo $llama->id . '-' . $llama->Hat->Color . '<br/>';
}

Funktionsweise vom SmartObjectSet

Das SmartObjectSet funktioniert auf einem relativ einfachen Gedanken: Jedes Objekt aus dem ObjectSet und jedes Unter-ObjectSet kennt den kompletten Pfad woher das Objekt kommt. Wenn nun wie in dem Beispiel oben das Referenzfeld "Llama::Hat" aufgerufen wird, wird der komplette ObjectSet-Pfad aufgebaut woher das "Llama"-Objekt kommt - in diesem Fall aus dem Root-ObjectSet mit allen Llamas. 

Danach wird der komplette Pfad nach unter wieder verfolgt, jedoch ohne auf ein einzelnes Objekt herunter zu brechen (beim iterieren brechen wir das ObjectSet auf ein einzelnes Objekt herunter). Sprich es wird von allen "Llama"-Objekten im ObjectSet die Hüte geladen. Das funktioniert selbstverständlich auch über mehrere Ebenen und auch Magic-Felder werden automatisch auf die gleiche Art aufgelöst.

Notiz: Selbst wenn vom selben "Llama"-Objekt zweimal das "Hat"-Feld abfragt wird, kommt jedes mal eine neue Instanz zurück.

3 caught errors Unexpected output x