Skip to main content

Prinzipien der Selector-Schicht

Lernziele

Nachdem Sie diese Lektion abgeschlossen haben, sind Sie in der Lage, die folgenden Aufgaben auszuführen:

  • Erläutern der Vorteile, die die Verwendung der Selektor-Schicht hat
  • Erläutern der Funktionen der Selektor-Schicht
  • Erläutern, wie die Selektor-Schicht in Ihre Anwendungsarchitektur und die Plattform passt

Mit Trail Together einem Dozenten folgen

Möchten Sie bei diesem Schritt einem Experten folgen? Schauen Sie sich dieses Video an, das Teil der Reihe "Trail Together" ist.

(Dieser Clip beginnt bei Minute 36:04, für den Fall, dass Sie zurückspringen und den Anfang des Schritts noch einmal sehen möchten.)

Kennenlernen der Selektor-Schicht

Bei diesem Modul lag einer der Hauptschwerpunkte darauf, eine gute Modularisierung der Anforderungen (Separation of Concerns) sicherzustellen, damit umfangreiche, komplexe Codebasen auf Unternehmensniveau klarer strukturiert, selbstdokumentierend, zuverlässig und durch Refaktorisierung oder funktionelle Weiterentwicklung anpassbar sind. Wir werden diesem Leitgedanken nun weiter folgen.

In dieser Einheit lernen Sie das Selektormuster kennen, eine Code-Schicht zur Kapselung von Logik, die Informationen aus Standard- und benutzerdefinierten Objekte abfragt. Die Selektor-Schicht speist diese Daten in den Code Ihrer Domain- und Service-Schicht ein. Sie können Selektor-Klassen auch von anderen Bereichen aus wiederverwenden, für die Abfragen notwendig sind, wie etwa Batch-Apex und Controller.

Die folgende Abbildung zeigt, wo die Selektor-Klasse in der Hierarchie der Musterklassen anzusiedeln ist. Sie wird hauptsächlich von Klassen innerhalb der Service-Schicht (hinter der Service-Grenze) wie Domain- und den Service-Klassen als solche verwendet. UI-Steuerfelder und Batch-Apex-Klassen können sie aber auch direkt nutzen.

Selektor-Klasse in der Hierarchie

Eine Selektor-Schicht enthält Code für die Abfrage von Datensätzen aus der Datenbank. Sie können zwar SOQL-Abfragen in anderen Schichten platzieren, doch mit zunehmender Komplexität Ihres Codes geschehen jedoch eventuell verschiedene Dinge:

  • Inkonsistenzen bei Abfragen: Wenn dieselben Abfragen für die gleichen Informationen oder Kriterien (bzw. leichten Varianten davon) von verschiedenen Stellen aus durchgeführt werden, kann dies zu Inkonsistenzen in Ihrer Anwendung führen. Eventuell müssen bestimmte Kriterien angewandt werden, gehen aber verloren, wenn Code ständig hin und her kopiert wird. Stellen Sie sich eine Abfrage wie einen Abschnitt mit Logik vor. Sie wissen, dass es keine gute Idee ist, Logikabschnitte hin und her zu kopieren. Und bei Code empfiehlt sich das ebenfalls nicht.
  • Inkonsistenzen bei Abfragedaten: Wenn abgefragte Datensatzdaten (vor allem Objekte) um Ihre Logik herum zu fließen beginnen – hoch und runter und von Seite zu Seite in der Organisation – können Aufrufmethoden, die die Datensätze empfangen, anfällig werden. Vielleicht fordert ein Codeabschnitt, dass ihm ein Account oder eine Opportunity übergeben wird. Er kann jedoch nicht im Vorfeld garantieren, welche Felder abgefragt wurden und welcher Inhalt daher verfügbar ist, wodurch der gefürchtet Laufzeitfehler System.SObjectException: SObject row was retrieved via SOQL without querying the requested field:X.Y verursacht wird. Das kann damit enden, dass Entwickler eine andere Abfrage für dieselbe Datensatzmenge durchführen müssen, nur um die erforderlichen Felder abzufragen. Vielleicht verlaufen die Codepfade auch so, dass von anderen Abfragen abgefragte Account-Datensätze an eine freigegebene Funktion übergeben werden. Beides ist nicht gut.
  • Inkonsistenzen bei der Sicherheit: Bei Salesforce muss der gesamte Apex-Code der Objektsicherheit des aktuellen Benutzers entsprechen. Apex kann auch in derselben Systemschicht ausgeführt werden, sodass der Entwickler die Sicherheit vor der Abfragedurchführung prüfen muss. Glücklicherweise ist dafür nicht viel Apex-Code erforderlich. Diese Aufgabe wird jedoch häufig vergessen und lässt sich auch in Einheitentests nicht leicht prüfen. Die Platzierung in einem Selektor macht Ihnen hier das Leben leichter.

Das Selektormuster, das wir hier vorstellen, schafft Abhilfe bei diesen Problemen.

Selektormuster

Nachfolgend finden Sie Martin Fowlers Definition des Mapper-Musters, auf dem das Selektormuster basiert.

Mapper-Muster: – "A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself." (Eine Schicht aus Mappern (473), die Daten zwischen Objekten und einer Datenbank verschieben und dabei dafür sorgen, dass sie voneinander und vom eigentlichen Mapper unabhängig bleiben.) – Fowler, Martin

Bei diesem Muster verwenden wir den Begriff "Selektor" anstelle von "Mapper". Der Begriff "Selektor" unterstreicht einen wichtigen Unterschied bei der Salesforce-Implementierung des Mapper-Musters, den Stephen Willcock bei der Entwicklung der ursprünglichen Apex-Implementierung dieses Musters bemerkte. Die in dieser Einheit vorgestellte fflib_SObjectSelector-Basisklasse basiert darauf.

Der Unterschied besteht darin, dass der Selektor keine Zuordnung eines Datenbankergebnissets zu einem Datenobjekt oder gar einer Domain-Darstellung zuordnet. Seine Hauptaufgabe besteht im Salesforce-Kontext vielmehr darin, SObject-Datensätze bereitzustellen, was dem nativen Verfahren der Plattform entspricht, aus der Datenbank abgefragte Daten auszudrücken. Wie Sie später in dieser Einheit noch sehen werden, kann der Selektor in manchen Fällen andere Apex-Darstellungen von Daten bereitstellen, doch im Grunde genommen wählt er lediglich Daten in dieser Instanz aus, weshalb wir uns auch für die geänderte Bezeichnung dieses Musters entschieden haben.

Im Hinblick auf die Modularisierung der Anforderungen (Separation of Concerns) dient ein Selektor folgenden Zwecken:

  • Sichtbarkeit, Wiederverwendbarkeit und Wartbarkeit: Der Selektor macht es leicht, Datenbankabfragelogik zu finden und zu warten. Das Aktualisieren von Abfragekriterien oder Umsetzen von Schemaänderungen, wie etwa das Hinzufügen von allgemein verwendeter Felder, lassen sich einfacher und mit weniger Risiko für die restliche Codebasis bewerkstelligen. Der Selektor sollte Kompilierzeitverweise auf Feldnamen verwenden, selbst wenn dynamische Abfragen erstellt werden. Dadurch wird beim Löschen von Feldern sichergestellt, dass die Plattform die Löschung verhindert, falls der Code Verweise enthält.
  • Vorhersagbarkeit abgefragter Daten: Der Methodenname muss klar ausdrücken, was der Selektor tut und, was er zurückgibt. Die Rückgabe inkonsistent ausgefüllter Felder in sObjects-Datensätzen ist nicht immer empfehlenswert, da es für den Aufrufer oder denjenigen, an den der Aufrufer die Ergebnisdaten übergibt, nicht klar ist, welche Daten aus Feldern stammen, was zu Laufzeitausnahmen führt. Das Selektormuster stellt sicher, dass für abgefragte SObject-Datensatz eine Mindestmenge an Felder angefragt wird. Entwickler können dies als Teil des "Selektor-Vertrags mit dem Rest der Organisation betrachten."
  • Sicherheit: Bittet dem Aufrufer die Möglichkeit, Sicherheitsprüfungen (für Szenarios auf Systemebene) zu aktivieren bzw. zu deaktivieren, die Freigaben und Berechtigungen für den aktuellen Benutzerkontext durchsetzen.
  • Plattformberücksichtigung: Optimieren Sie Abfragen weitestgehend, indem Sie vor allem Kriterien über Sets ausdrücken und so den Aufrufer ermutigen, beim Aufruf der Selektor-Methoden auf Massenverarbeitung zu setzen. Selektoren sollten zudem in der Lage sein, die Notwendigkeit der Konsistenz abgefragter Felddaten gegenüber optimalen Felddaten auszugleichen, wenn größere Datensets verarbeitet werden und der Heap ein Problem darstellt.

Designüberlegungen

Berücksichtigen Sie beim Schreiben einer Selektor-Klasse folgende Designaspekte:

Verwenden einer Selektor-Klasse

Wie setzen wir diese Designüberlegungen konkret in die Praxis um? Mit dem folgenden einfachen Beispiel wird eine Domain-Klasseninstanz mit Datensätzen initialisiert. Da dieser Selektor nicht-statische Methoden für die Bereitstellung seiner Funktionen verwendet, wird eine Instanz erstellt. Behält der Aufrufer die Selektor-Instanz bei, lässt der Umfang bei Bedarf ein gewisses Maß an Konfiguration und Caching zu.

List<Opportunity> opportunities =
   new OpportunitiesSelector().selectById(opportunityIds));

Der folgende Code aus der Service-Schicht (den Sie eventuell aus der vorhergehenden Einheit wiedererkennen) verwendet jetzt einen Selektor, um Datensätze abzufragen und an eine Domain-Klasse zu übergeben, die Domain-Logik für die abgefragten Datensätze ausführt. Somit wurde die Inline-SOQL-Logik jetzt aus der Service- und Domain-Logik verlagert.

List<Opportunity> opportunityRecords = new 
       OpportunitiesSelector().selectByIdWithProducts(opportunityIds);
Opportunities opportunities = new Opportunities(opportunityRecords);
opportunities.applyDiscount(discountPercentage, uow);

Dieses Beispiel zeigt einen Selektor in einer Batch-Apex-Startmethode, der die bei der Ausführung verarbeiteten Datensätze bereitstellt.

public Database.QueryLocator start(Database.BatchableContext bc) {
    return new InvoicesSelector().queryLocatorInvoicesToProcess();
}

Testen von Selektor-Klassen

Ihr Selektor-Klassencode wird natürlich von Ihren Tests im Zusammenhang mit der Domain- oder Service-Schicht abgedeckt. Wenn Sie jedoch gezieltere Einheitentests durchführen möchten, verhält sich dies bei Selektor-Klassen ähnlich wie bei anderen Klassen: Sie erstellen die Testdaten, rufen die Methoden auf und prüfen die Ergebnisse.

@IsTest
private static void whenQueryOpportuntiesGetProducts() {
    
    // Given
    Set<Id> testRecordIds = setupTestOpportunities();
    // When
    OpportunitiesSelector selector = new OpportunitiesSelector();
    List<Opportunity> opportunities =
      selector.selectByIdWithProducts(testRecordIds);
    // Then
    System.assertEquals(10, opportunities.size());
    System.assertEquals(5, opportunities[0].OpportunityLineItems.size());
}

Wenn Ihr Selektor QueryLocator-Methoden enthält, können Sie die Iterator-Methode aufrufen, um die Datensatzergebnisse abzurufen und dadurch zu den erwarteten Daten in Ihrem Selektor-Test erklären.

Database.QueryLocator queryLocator =
    new ProductsSelector().queryLocatorById(new Set<Id> {product.Id});
Database.QueryLocatorIterator productsIterator = queryLocator.iterator();
Product2 queriedProduct = (Product2) productsIterator.next();

Ressourcen

Lernen Sie weiter kostenlos!
Registrieren Sie sich für einen Account, um fortzufahren.
Was ist für Sie drin?
  • Holen Sie sich personalisierte Empfehlungen für Ihre Karriereplanung
  • Erproben Sie Ihre Fähigkeiten mithilfe praktischer Aufgaben und Quizze
  • Verfolgen Sie Ihre Fortschritte nach und teilen Sie sie mit Arbeitgebern
  • Nutzen Sie Mentoren und Karrierechancen