Magento SearchCriteria ist die saubere ORM-Schicht. Direktes SQL ist schneller und flexibler. Die Kunst liegt darin, zu wissen, wann welcher Ansatz die richtige Wahl ist — und was man riskiert, wenn man das ORM umgeht.
Inhaltsverzeichnis
- 1. Die Architekturentscheidung: ORM vs. SQL
- 2. Die SearchCriteria API
- 3. Repository::getList() korrekt nutzen
- 4. Wann SearchCriteria die richtige Wahl ist
- 5. Direktes SQL mit getConnection()
- 6. Wann direktes SQL besser ist
- 7. Risiken beim ORM-Bypass
- 8. Hybrid-Ansatz: SQL für Reads, Repository für Writes
- 9. Professionelle Unterstützung
- 10. Zusammenfassung
- 11. FAQ
1. Die Architekturentscheidung: ORM vs. SQL
Magento bietet zwei grundlegend verschiedene Wege, auf Datenbankdaten zuzugreifen: die SearchCriteria-API mit Repositories und Service Contracts auf der einen Seite, und direktes SQL über die Datenbankverbindung auf der anderen. Beide Ansätze sind legitim — aber für unterschiedliche Szenarien geeignet.
Die Entscheidung ist keine Frage von "richtig" oder "falsch", sondern von Kontext. SearchCriteria respektiert Plugins, Events und Multi-Store-Logik, ist testbar und API-stabil. Direktes SQL ist schneller, flexibler bei komplexen Joins und unverzichtbar für Bulk-Operationen auf Millionen von Zeilen. Wer den Unterschied nicht kennt, benutzt entweder SearchCriteria dort, wo die Performance einbricht, oder umgeht das ORM dort, wo das Sicherheitsrisiko zu hoch ist.
2. Die SearchCriteria API
Die SearchCriteria API in Magento besteht aus mehreren Bausteinen. Ein Filter ist die atomare Einheit: Feldname, Wert und Kondition (eq, like, gt, in, etc.). FilterGroups fassen mehrere Filter mit OR zusammen. Mehrere FilterGroups in einem SearchCriteria werden mit AND verknüpft. SortOrder definiert die Sortierung, PageSize und CurrentPage steuern die Paginierung.
// PHP: Building a SearchCriteria with FilterGroup and SortOrder
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SortOrderBuilder;
use Magento\Sales\Api\OrderRepositoryInterface;
// Filter 1: status IN ('complete', 'processing')
$statusFilter = $this->filterBuilder
->setField('status')
->setValue(['complete', 'processing'])
->setConditionType('in')
->create();
// Filter 2: created_at >= 2025-01-01
$dateFilter = $this->filterBuilder
->setField('created_at')
->setValue('2025-01-01 00:00:00')
->setConditionType('gteq')
->create();
// SortOrder: newest first
$sortOrder = $this->sortOrderBuilder
->setField('created_at')
->setDescendingDirection()
->create();
$searchCriteria = $this->searchCriteriaBuilder
->addFilters([$statusFilter]) // each addFilters() = one FilterGroup (AND between groups)
->addFilters([$dateFilter])
->addSortOrder($sortOrder)
->setPageSize(50)
->setCurrentPage(1)
->create();
$searchResults = $this->orderRepository->getList($searchCriteria);
$orders = $searchResults->getItems(); // array of OrderInterface
Das Ergebnis von getList() ist ein SearchResultsInterface-Objekt, das neben den Items auch die Gesamtanzahl (getTotalCount()) enthält — nützlich für Paginierungslogik, ohne eine separate Count-Query abzusetzen.
3. Repository::getList() korrekt nutzen
Ein häufiger Fehler ist, alle Datensätze auf einmal zu laden, indem auf setPageSize() verzichtet wird. Das führt bei großen Tabellen zu massivem Memory-Verbrauch. Eine weitere Falle ist die Verwechslung von OR und AND: Innerhalb einer addFilters([])-Gruppe werden alle Filter mit OR verknüpft. Zwischen verschiedenen addFilters()-Aufrufen gilt AND.
// Correct: OR within a FilterGroup (same addFilters() call)
// Finds orders with status = 'complete' OR status = 'processing'
$this->searchCriteriaBuilder
->addFilters([
$this->filterBuilder->setField('status')->setValue('complete')->setConditionType('eq')->create(),
$this->filterBuilder->setField('status')->setValue('processing')->setConditionType('eq')->create(),
]);
// Correct: AND between FilterGroups (separate addFilters() calls)
// Finds orders created today AND with customer_id = 100
$this->searchCriteriaBuilder
->addFilters([
$this->filterBuilder->setField('created_at')->setValue('2025-01-15')->setConditionType('gteq')->create(),
])
->addFilters([
$this->filterBuilder->setField('customer_id')->setValue(100)->setConditionType('eq')->create(),
]);
4. Wann SearchCriteria die richtige Wahl ist
SearchCriteria ist die richtige Wahl in folgenden Szenarien:
- API-exponierte Endpunkte: Jeder REST- oder GraphQL-Endpunkt, der Magento-Entities zurückgibt, soll über Service Contracts gehen, damit Extension Attributes automatisch mitgeliefert werden.
- Plugin-Unterstützung erwartet: Wenn andere Module per Plugin auf Repository-Aufrufe reagieren sollen (z.B. für Multi-Store-Filterung oder Kundenspezifika), muss der Weg über das Repository gehen.
- Testbarkeit: Repository-Interfaces sind einfach zu mocken. Direktes SQL in einem Service ist schwer zu unit-testen.
- Multi-Store-Kontext: Einige Repositories filtern automatisch nach Store-Scope. Direktes SQL muss das explizit selbst implementieren.
5. Direktes SQL mit getConnection()
Magento stellt über ResourceConnection und AdapterInterface Zugriff auf die rohe Datenbankverbindung bereit. Für einfache Queries reicht fetchAll(), für komplexe Abfragen steht der Zend_Db_Select-Builder zur Verfügung.
// PHP: Direct SQL via ResourceConnection
use Magento\Framework\App\ResourceConnection;
// Simple query with fetchAll
$connection = $this->resourceConnection->getConnection();
$tableName = $connection->getTableName('sales_order');
$select = $connection->select()
->from(['o' => $tableName], ['entity_id', 'increment_id', 'grand_total'])
->where('o.status IN (?)', ['complete', 'processing'])
->where('o.created_at >= ?', '2025-01-01 00:00:00')
->order('o.created_at DESC')
->limit(100);
$orders = $connection->fetchAll($select); // array of assoc arrays
// Complex cross-entity report (not possible via SearchCriteria)
$itemTable = $connection->getTableName('sales_order_item');
$select = $connection->select()
->from(['oi' => $itemTable], [
'sku',
'product_name' => 'oi.name',
'qty_sold' => new \Zend_Db_Expr('SUM(oi.qty_ordered)'),
'revenue' => new \Zend_Db_Expr('SUM(oi.row_total)'),
])
->join(['o' => $tableName], 'o.entity_id = oi.order_id', [])
->where('o.status IN (?)', ['complete', 'processing'])
->where('oi.parent_item_id IS NULL')
->group('oi.sku')
->order('revenue DESC')
->limit(10);
$topProducts = $connection->fetchAll($select);
6. Wann direktes SQL besser ist
Direktes SQL ist die bessere Wahl in folgenden Situationen:
- Bulk-Operationen: Ein
UPDATE sales_order SET status = 'complete' WHERE ...mit Zehntausenden von Zeilen ist über direktes SQL um Größenordnungen schneller als eine Schleife mit$repository->save(). - Komplexe Cross-Entity-Reports: JOINs über mehrere Entitäten (order + item + customer + creditmemo) lassen sich über SearchCriteria nicht ausdrücken, weil jedes Repository nur seine eigene Entität kennt.
- Performance-kritische Read-Paths: Hochfrequente Lesezugriffe, bei denen Object-Hydration und Plugin-Overhead messbar ins Gewicht fallen.
- Statistik- und Aggregationsabfragen:
GROUP BY,HAVING, Window Functions und Aggregationen sind mit SearchCriteria schlicht nicht abbildbar.
7. Risiken beim ORM-Bypass
Wer direktes SQL nutzt, muss die Konsequenzen kennen und bewusst akzeptieren:
Keine Plugin-/Event-Ausführung: Ein direktes UPDATE oder INSERT auf eine Magento-Tabelle löst keine before_save-, after_save-Events und keine Plugins aus. Module, die auf diese Events reagieren (z.B. Indexer, Cache-Invalidierung, ERP-Connectoren), werden nicht benachrichtigt.
Kein automatisches Cache-Clearing: Magento's ORM-Layer räumt nach einem repository->save() automatisch relevante Cache-Tags auf. Direktes SQL macht das nicht — manuelles Cache-Flushen ist notwendig.
Schema-Fragilität: Wenn eine Extension eine Tabelle umbenennt oder Spalten ändert, bricht direktes SQL ohne Kompilierfehler. Das ORM abstrahiert über Tabellennamen ($connection->getTableName() sollte immer genutzt werden).
// Always use getTableName() to avoid hardcoded table names
// Wrong: hardcoded table name breaks with table prefix
$connection->query("UPDATE sales_order SET status = 'complete' WHERE ...");
// Correct: resolve table name via ResourceConnection
$tableName = $this->resourceConnection->getTableName('sales_order');
$connection->update(
$tableName,
['status' => 'complete'],
['entity_id IN (?)' => [1, 2, 3]]
);
8. Hybrid-Ansatz: SQL für Reads, Repository für Writes
Der pragmatischste Ansatz für viele Magento-Projekte ist ein Hybrid: Direktes SQL für performante Lesezugriffe und komplexe Reporting-Queries, aber immer Repository für Schreiboperationen, die Events und Plugins brauchen. Dieser Ansatz kombiniert Performance mit Korrektheit.
// Hybrid example: direct SQL for fast read, Repository for write
// READ: find IDs of orders to update (fast, direct SQL)
$connection = $this->resourceConnection->getConnection();
$tableName = $connection->getTableName('sales_order');
$select = $connection->select()
->from($tableName, ['entity_id'])
->where('status = ?', 'processing')
->where('created_at < ?', date('Y-m-d H:i:s', strtotime('-7 days')));
$orderIds = $connection->fetchCol($select); // [101, 205, 318, ...]
// WRITE: update via Repository to fire events and plugins
foreach ($orderIds as $orderId) {
$order = $this->orderRepository->get($orderId);
$order->setStatus('holded');
$this->orderRepository->save($order);
// This fires: sales_order_save_before, sales_order_save_after, etc.
}
Für wirkliche Bulk-Updates — tausende oder zehntausende Zeilen — ist selbst dieser Ansatz zu langsam, weil repository->save() pro Zeile viele Datenbankoperationen auslöst. In solchen Fällen muss direktes SQL für den Write akzeptiert werden, mit anschließend manueller Cache-Invalidierung und Indexer-Ausführung.
Mironsoft
Magento-Datenbankzugriffe sauber und performant architektieren
Wir helfen dabei, die richtige Balance zwischen SearchCriteria und direktem SQL in Magento zu finden — für wartbare Architektur und echte Performance-Gewinne.
Code Review
Repository-Pattern, SearchCriteria-Nutzung und direktes SQL auf Korrektheit prüfen
Performance-Optimierung
Slow Queries identifizieren und ORM-Bottlenecks durch gezieltes direktes SQL ersetzen
Architektur-Beratung
Bulk-Import, Reporting-Layer und API-Integration sauber in Magento integrieren
Magento SearchCriteria vs. direktes SQL — Das Wichtigste auf einen Blick
SearchCriteria wählen wenn
API-Exposition, Plugin-Support, Testbarkeit und Multi-Store-Logik wichtig sind.
Direktes SQL wählen wenn
Bulk-Operationen, komplexe JOINs, Aggregationen oder Performance-kritische Read-Paths nötig sind.
ORM-Bypass Risiko
Keine Events/Plugins, kein automatisches Cache-Clearing, Schema-Fragilität bei Tabellenänderungen.
Hybrid-Ansatz
Direktes SQL für schnelle Reads und Reporting, Repository immer für Writes die Events erfordern.
10. Zusammenfassung
Magento SearchCriteria ist die korrekte ORM-Schicht für API-exponierte Datenzugriffe, testbare Services und alles, was Plugins und Events nutzen soll. Direktes SQL über getConnection() ist die pragmatische Wahl für Bulk-Operationen, komplexe Cross-Entity-Reports und performance-kritische Lesepfade. Die wichtigsten Risiken beim ORM-Bypass sind fehlende Event-/Plugin-Ausführung und kein automatisches Cache-Clearing.
Der Hybrid-Ansatz — direktes SQL für Reads, Repository für Writes — ist in vielen Projekten der beste Kompromiss. Entscheidend ist, die Wahl bewusst zu treffen und die Konsequenzen zu kennen: Wer direktes SQL für Writes einsetzt, muss Cache-Tags manuell invalidieren und Indexer gezielt anstoßen.
11. FAQ: Magento SearchCriteria vs. direktes SQL
1Was ist der Unterschied zwischen SearchCriteria und direktem SQL in Magento?
2Wie verknüpfe ich Filter mit OR in SearchCriteria?
addFilters([...])-Aufruf werden mit OR verknüpft. Separate addFilters()-Aufrufe gelten als AND.3Warum gibt getList() SearchResultsInterface zurück?
getTotalCount() für die Gesamtanzahl — nötig für korrekte Paginierung ohne separate Count-Query.4Welche Risiken entstehen beim direkten SQL in Magento?
getTableName() nutzen.5Wann ist direktes SQL unumgänglich?
6Was ist der Hybrid-Ansatz?
7Warum immer getTableName() nutzen?
getTableName() löst den logischen Namen korrekt auf. Hardcodierte Namen brechen bei Präfix-Setups.8Kann SearchCriteria für Reporting-Queries genutzt werden?
9Wie teste ich Services mit direktem SQL?
10Was passiert mit Magento-Indizes beim direkten SQL-Update?
bin/magento indexer:reindex angestoßen werden.