GraphQL-Caching pro Kundengruppe in Magento korrekt umsetzen
AI generated
M2
di.xml
Magento 2 · GraphQL · Caching
GraphQL-Caching pro Kundengruppe
wie Cache-Key-Faktoren kundengruppenabhängige Preise korrekt behandeln

Sobald Preise oder Sichtbarkeit von der Kundengruppe abhängen, kann ein naiver Cache dieselbe Antwort an unterschiedliche Kunden ausliefern, mit im schlechtesten Fall falschen Preisen. Dieser Artikel erklärt, wie Magentos eingebauter GraphQL-Response-Cache die Kundengruppe bereits standardmäßig in seinen Cache-Key einrechnet, wo eigene Resolver-Level-Caches trotzdem daneben liegen können, und wie sich das korrekt mit Varnish und einem vorgeschalteten CDN verträgt.

14 Min. Lesezeit CacheIdFactorProviderInterface Varnish · CDN-Edge-Caching

1. Warum kundengruppenabhängige Preise naives Response-Caching brechen

Sobald Preise, Sonderkonditionen oder die Sichtbarkeit einzelner Produkte von der Kundengruppe abhängen, liefert ein und dieselbe GraphQL-Query je nach anfragendem Kunden unterschiedliche, jeweils korrekte Antworten. Ein Cache, der ausschließlich nach URL oder reinem Query-Text schlüsselt, kann diesen Unterschied nicht abbilden und riskiert, die falsche Antwort für den falschen Kunden auszuliefern.

Im schlechtesten Fall bekommt eine Kundengruppe mit ausgehandelten Sonderkonditionen die für eine andere Gruppe berechnete Antwort aus dem Cache ausgeliefert, mit falschen Preisen oder fälschlich sichtbaren beziehungsweise unsichtbaren Produkten. Dieses Fehlerbild wird in Tests mit einem einzigen Testkonto leicht übersehen, weil dort niemals zwei unterschiedliche Kundengruppen gegeneinander verglichen werden.

Besonders tückisch ist, dass ein solcher Fehler in der Regel nicht sofort auffällt: Die Antwort ist syntaktisch vollkommen korrekt, die Query liefert Daten, keine Fehlermeldung deutet auf ein Problem hin. Erst ein gezielter Vergleich zwischen zwei Kundengruppen oder eine Reklamation eines Kunden, der einen falschen Preis gesehen hat, deckt das eigentliche Problem auf, weshalb kundengruppenabhängiges Caching von Anfang an mitgedacht werden sollte, statt es erst nachträglich zu reparieren.

2. Magentos eingebauter Mechanismus: CacheIdCalculator und Kundengruppen-Faktor

Seit dem Modul Magento_GraphQlCache berechnet Magento\GraphQlCache\Model\CacheId\CacheIdCalculator für jede cachefähige Anfrage einen X-Magento-Cache-Id-Wert als Hash über mehrere registrierte Faktoren. Jeder Faktor implementiert CacheIdFactorProviderInterface mit den Methoden getFactorName und getFactorValue.

Die Kundengruppe ist dabei bereits ab Werk enthalten: Magento\CustomerGraphQl\CacheIdFactorProviders\CustomerGroupProvider liefert die Kundengruppen-ID als Faktor CUSTOMER_GROUP, ergänzt um Store, Währung, Steuersatz und Login-Status aus benachbarten Providern. Der eingebaute Response-Cache trennt unterschiedliche Kundengruppen also bereits korrekt.


<?php
declare(strict_types=1);

namespace Magento\GraphQlCache\Model\CacheId;

use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;

/**
 * Vertrag für einen einzelnen Faktor, der in die X-Magento-Cache-Id-Berechnung einfliesst.
 */
interface CacheIdFactorProviderInterface
{
    /**
     * Eindeutiger Name des Faktors innerhalb der Hash-Berechnung.
     *
     * @return string
     */
    public function getFactorName(): string;

    /**
     * Aktueller Wert des Faktors für den gegebenen GraphQL-Request-Kontext.
     *
     * @param ContextInterface $context
     * @return string
     */
    public function getFactorValue(ContextInterface $context): string;
}

3. Wo es trotzdem schiefgeht: Resolver-Level-Caching als separate Schicht

Der Response-Cache ist nicht die einzige Cache-Ebene: Magento_GraphQlResolverCache erlaubt das Zwischenspeichern einzelner, teurer Resolver-Ergebnisse über Resolver\IdentityInterface und einen eigenen Cache-Key-Mechanismus, unabhängig davon, ob die komplette Antwort überhaupt response-cachefähig ist.

Dieser Mechanismus kennt Kundengruppe nicht automatisch. Jeder eigene Resolver, der sich am Resolver-Cache beteiligt, muss die relevanten Kontextdimensionen selbst in seinen Cache-Key einbeziehen, sonst entsteht genau auf dieser Ebene die Vermischung, die der Response-Cache eigentlich schon verhindert.

Der Grund für diese Trennung ist nachvollziehbar: Der Response-Cache entscheidet auf Ebene der gesamten Anfrage, ob überhaupt gecached werden darf, während der Resolver-Cache gezielt einzelne, besonders teure Berechnungen innerhalb einer sonst nicht cachefähigen oder personalisierten Antwort beschleunigen soll. Genau diese Flexibilität macht ihn aber auch zur Fehlerquelle, weil er bewusst unabhängig vom Response-Cache arbeitet und dessen bereits korrekte Kundengruppen-Logik nicht automatisch erbt.

4. Praxisbeispiel: Ein manuell gecachter Resolver ohne Kundengruppen-Anteil

Ein naheliegender, in der Praxis häufiger Fehler entsteht, wenn ein Entwickler einen teuren, eigenen Resolver direkt mit CacheInterface cached, etwa das Ergebnis einer aufwendigen ERP-Preisberechnung, und dabei den Cache-Key allein aus der Produkt-SKU bildet.

Die korrigierte Version nimmt die Kundengruppen-ID aus dem GraphQL-Context als zusätzlichen Bestandteil des Cache-Keys auf, wodurch zwei Kundengruppen zwar denselben Resolver-Code, aber garantiert unterschiedliche Cache-Einträge nutzen.


<?php
declare(strict_types=1);

namespace Vendor\ErpPricing\Model\Resolver;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;

/**
 * Beispiel-Resolver, der eine teure ERP-Preisberechnung zwischenspeichert.
 */
class ErpNegotiatedPrice implements ResolverInterface
{
    /**
     * @param CacheInterface $cache
     */
    public function __construct(private readonly CacheInterface $cache)
    {
    }

    /**
     * @param Field $field
     * @param mixed $context
     * @param ResolveInfo $info
     * @param array|null $value
     * @param array|null $args
     * @return float
     */
    public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): float
    {
        $sku = $value['sku'];

        // FALSCH: Der Cache-Key enthält nur die SKU, alle Kundengruppen teilen sich denselben Eintrag.
        // $cacheKey = 'erp_price_' . $sku;

        // KORREKT: Die Kundengruppen-ID aus dem GraphQL-Context ergänzt den Cache-Key,
        // damit zwei Kundengruppen niemals denselben Preis-Cache-Eintrag nutzen.
        $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId();
        $cacheKey = 'erp_price_' . $sku . '_group_' . $customerGroupId;

        $cached = $this->cache->load($cacheKey);
        if ($cached !== false) {
            return (float) $cached;
        }

        $price = $this->fetchNegotiatedPriceFromErp($sku, $customerGroupId);
        $this->cache->save((string) $price, $cacheKey, [], 600);

        return $price;
    }

    /**
     * @param string $sku
     * @param int $customerGroupId
     * @return float
     */
    private function fetchNegotiatedPriceFromErp(string $sku, int $customerGroupId): float
    {
        // ... teurer ERP-Aufruf, hier stark vereinfacht dargestellt
        return 0.0;
    }
}

5. Einen eigenen CacheIdFactorProviderInterface für zusätzlichen Kontext schreiben

Reicht die Kundengruppe allein nicht aus, etwa weil B2B-Firmen innerhalb derselben Kundengruppe unterschiedliche, ausgehandelte Preise über einen Shared Catalog erhalten, lässt sich ein zusätzlicher Faktor analog zum Kern-Muster ergänzen.

Die Registrierung erfolgt als weiteres Array-Item am Konstruktor-Argument idFactorProviders von CacheIdCalculator, exakt nach demselben Muster, mit dem der Kern bereits Store-, Währungs- und Kundengruppen-Provider verdrahtet.


<?php
declare(strict_types=1);

namespace Vendor\B2bCaching\Model\CacheIdFactorProviders;

use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\GraphQlCache\Model\CacheId\CacheIdFactorProviderInterface;

/**
 * Ergänzt die B2B-Firmen-ID als eigenen Faktor in der Cache-Id-Berechnung,
 * für Fälle, in denen dieselbe Kundengruppe unterschiedliche Firmenpreise hat.
 */
class CompanyIdProvider implements CacheIdFactorProviderInterface
{
    /**
     * @return string
     */
    public function getFactorName(): string
    {
        return 'COMPANY_ID';
    }

    /**
     * @param ContextInterface $context
     * @return string
     */
    public function getFactorValue(ContextInterface $context): string
    {
        return (string) ($context->getExtensionAttributes()->getCompanyId() ?? '0');
    }
}

6. Zusammenspiel mit Varnish: Der Header wandert in den VCL-Hash

Das mitgelieferte Varnish-VCL übernimmt den von Magento berechneten X-Magento-Cache-Id-Header direkt in die eigene Hash-Berechnung, ergänzt um Store- und Content-Currency-Header sowie einen Zusatz für authentifizierte Anfragen mit Bearer-Token.

Varnish muss dafür selbst nichts über Kundengruppen wissen, es verlässt sich vollständig darauf, dass Magento die Berechnung korrekt vornimmt. Ein Response wird zusätzlich nur dann überhaupt gecached, wenn der im Request gesendete Cache-Id-Wert mit dem in der Antwort übereinstimmt.


# Auszug aus dem mitgelieferten varnish6.vcl, vereinfacht
sub process_graphql_headers {
    if (req.http.X-Magento-Cache-Id) {
        hash_data(req.http.X-Magento-Cache-Id);
        hash_data(req.http.Store);
        hash_data(req.http.Content-Currency);
        if (req.http.Authorization) {
            hash_data("Authorized");
        }
    }
}

7. CDN-Edge-Caching vor Varnish: Das eigentliche Risiko

Ein zusätzliches CDN vor Varnish bringt eigene Caching-Regeln mit, die sich nicht zwangsläufig an Magentos Header halten. Cached ein CDN GraphQL-Antworten allein nach URL, etwa bei GET-Anfragen mit Persisted Queries, kann es trotz korrektem Magento- und Varnish-Verhalten gruppenübergreifend falsche Antworten ausliefern.

Die Lösung besteht entweder darin, das CDN so zu konfigurieren, dass die relevanten Magento-Header explizit in dessen eigenen Cache-Schlüssel einfließen, oder GraphQL-Anfragen am CDN grundsätzlich vom Caching auszunehmen und diese Aufgabe vollständig Varnish zu überlassen.

8. Abgrenzung zum allgemeinen Varnish-GraphQL-Artikel

Ein allgemeiner Artikel zur Varnish-Konfiguration für GraphQL behandelt typischerweise Grundlagen wie VCL-Aufbau, Cache-Tags für Invalidierung und Timeouts. Dieser Artikel setzt genau dort an, wo die Kundengruppe konkret in die Cache-Key-Berechnung einfließt, sowohl auf Magento- als auch auf CDN-Ebene.

Wer beide Themen kombiniert, sollte zuerst die grundsätzliche Varnish-Anbindung sicherstellen und erst danach die hier beschriebenen, kundengruppenspezifischen Faktoren und die Resolver-Cache-Fallstricke prüfen, weil eine fehlerhafte Grundkonfiguration jede weitere Optimierung wertlos macht.

9. Testing und Verifikation gegen Preis-Leckage zwischen Kundengruppen

Der zuverlässigste Test führt dieselbe Query mit zwei unterschiedlichen, authentifizierten Kundengruppen aus und vergleicht sowohl die gelieferten Preise als auch den zurückgegebenen X-Magento-Cache-Id-Wert, beide Werte müssen sich zwischen den Gruppen unterscheiden.

Ergänzend lohnt sich ein automatisierter Regressionstest genau für diesen Vergleich, damit ein künftiger, unbedacht hinzugefügter manueller Resolver-Cache nicht unbemerkt wieder genau die Lücke öffnet, die der eingebaute Mechanismus eigentlich schon geschlossen hat.

Sinnvoll ist außerdem ein regelmäßiger, automatisierter Check direkt in Produktion, der stichprobenartig denselben Vergleich mit zwei echten Testkonten unterschiedlicher Kundengruppen durchführt und bei einer Abweichung sofort Alarm schlägt, statt sich allein auf einmalige Tests zum Zeitpunkt eines Deployments zu verlassen. Gerade Änderungen an Drittmodulen, die eigene Resolver oder eigene Caching-Logik mitbringen, können diese Garantie später wieder unbemerkt aushebeln, weshalb ein solcher Produktionscheck fester Bestandteil des laufenden Monitorings bleiben sollte statt einer einmaligen Abnahmeprüfung.

Bei einem Audit lohnt sich zusätzlich ein bewusster Blick auf jeden Drittanbieter-Resolver im Schema, mit der einfachen Leitfrage, ob und wie er cached, und ob die Kundengruppe dabei tatsächlich Teil des verwendeten Cache-Keys ist. Diese kurze, aber gezielte Prüfung deckt erfahrungsgemäß deutlich mehr potenzielle Preis-Leckagen auf als ein rein oberflächlicher Blick auf die Gesamtarchitektur des Caching.

Schicht Mechanismus Kundengruppe automatisch enthalten? Zuständig für
GraphQL-Response-Cache CacheIdCalculator plus CacheIdFactorProviderInterface Ja, über CustomerGroupProvider Kompletten Response cachen und wiederverwenden
Resolver- oder Feld-Cache Magento_GraphQlResolverCache, Resolver\IdentityInterface Nein, muss pro Resolver ergänzt werden Einzelne teure Feldergebnisse cachen
Eigener manueller Resolver-Cache Direkter Aufruf von CacheInterface im Resolver-Code Nein, nur wenn Entwickler es einbaut Ad-hoc-Caching in Custom-Resolvern
Varnish VCL-Hash über X-Magento-Cache-Id, Store, Content-Currency Ja, indirekt über den übernommenen Header Edge-Caching kompletter HTTP-Responses
Externes CDN vor Varnish Abhängig von der CDN-Konfiguration Nur wenn Header explizit respektiert werden Globales Edge-Caching

Mironsoft

Magento-Entwicklung, Modul-Beratung und Systemarchitektur

Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?

Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.

Architektur-Beratung

Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.

Custom-Modul-Entwicklung

Individuelle Magento-Module nach Best Practices sauber umsetzen.

Code-Review & Audit

Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.

10. Zusammenfassung

GraphQL-Caching pro Kundengruppe in Magento: Das Wichtigste auf einen Blick

Guter Startpunkt

Der eingebaute Response-Cache berücksichtigt Kundengruppe bereits standardmäßig über CustomerGroupProvider.

Echtes Risiko

Eigene, manuell gecachte Resolver-Ergebnisse ohne Kundengruppen-Anteil im Cache-Key.

Erweiterung

Zusätzliche Kontext-Dimensionen wie B2B-Firmen-ID über einen eigenen CacheIdFactorProviderInterface ergänzen.

Edge-Ebene

Varnish übernimmt den von Magento berechneten Header, ein CDN davor muss ihn explizit respektieren.

11. FAQ: GraphQL-Caching pro Kundengruppe in Magento: Das Wichtigste auf einen Blick

1Cached Magentos GraphQL-Response-Cache automatisch pro Kundengruppe getrennt?
Ja, seit Magento_GraphQlCache ist die Kundengruppe über den mitgelieferten CustomerGroupProvider standardmäßig Teil der X-Magento-Cache-Id-Berechnung.
2Wo entsteht trotzdem das Risiko falscher Preise aus dem Cache?
Vor allem bei eigenen Resolvern, die zusätzlich manuell mit CacheInterface cachen und dabei die Kundengruppe im selbstgebauten Cache-Key vergessen, unabhängig vom korrekten Response-Cache.
3Was ist der Unterschied zwischen CacheIdFactorProviderInterface und Resolver\IdentityInterface?
Ersteres beeinflusst den Cache-Schlüssel des gesamten Response-Caches, Letzteres liefert Tags für die gezielte Invalidierung einzelner Ressourcen, beide Mechanismen ergänzen sich, ersetzen sich aber nicht.
4Muss ich für Kundengruppen selbst einen Factor-Provider schreiben?
Nein, für die reine Kundengruppe nicht, der Kern liefert CustomerGroupProvider bereits mit, ein eigener Provider lohnt sich erst für zusätzliche Dimensionen wie eine B2B-Firmen-ID.
5Wie schreibe ich einen eigenen Cache-Id-Factor?
Über eine Klasse, die CacheIdFactorProviderInterface implementiert und per di.xml als zusätzliches Array-Item bei CacheIdCalculator registriert wird, analog zur Kern-Registrierung von Store- und Currency-Providern.
6Reicht der Magento-Mechanismus für Varnish automatisch aus?
Ja, Varnish übernimmt den von Magento berechneten X-Magento-Cache-Id-Header direkt in den eigenen VCL-Hash, zusätzlich zu Store- und Content-Currency-Headern, ohne dass Varnish selbst etwas über Kundengruppen wissen muss.
7Was passiert, wenn ein CDN vor Varnish sitzt?
Wenn das CDN GraphQL-Antworten eigenständig allein nach URL cached und die relevanten Header ignoriert, kann es trotz korrektem Magento- und Varnish-Verhalten zu falschen, gruppenübergreifend ausgelieferten Antworten kommen.
8Wie behebe ich ein CDN, das GraphQL falsch cached?
Entweder das CDN so konfigurieren, dass es die von Magento gesetzten Header in seinen eigenen Cache-Schlüssel übernimmt, oder GraphQL-POST-Anfragen am CDN grundsätzlich vom Caching ausnehmen und das Caching allein Varnish überlassen.
9Worin unterscheidet sich dieser Artikel vom allgemeinen Varnish-GraphQL-Artikel?
Dort geht es um die grundsätzliche Varnish-Konfiguration für GraphQL, hier konkret um die Cache-Key-Faktoren und wie kundengruppenabhängige Daten korrekt in diese Faktoren einfließen.
10Wie teste ich, ob wirklich keine Preise zwischen Kundengruppen durchsickern?
Denselben Query mit zwei unterschiedlichen, authentifizierten Kundengruppen ausführen, dabei sowohl die gelieferten Preise als auch den zurückgegebenen X-Magento-Cache-Id-Wert vergleichen, beide müssen sich unterscheiden.