Im Shopify-Architektur-Ratgeber besprechen wir die Grundlagen. Heute schauen wir uns an, wie du daten in Shopify modellierst – das Fundament jeder guten Architektur.
Bei smplx. haben wir mit verschiedenen Ansätzen experimentiert. Metafields? Metaobjects? Custom Apps mit separaten Datenbanken? Jede Option hat ihren Platz. Lass uns gemeinsam die richtige Entscheidung treffen.
Das Shopify-Datenmodell
Shopify hat ein starres, aber intelligentes Datenmodell:
Shop
├── Products
│ ├── Variants
│ └── Metafields (seit 2021)
├── Collections
│ └── Metafields
├── Customers
│ └── Metafields
├── Orders
│ └── Metafields
└── Metaobjects (seit 2024)
└── Fields & Relationships
Die Standard-Entitäten
Products sind das Herzstück:
- Title, Description, Vendor
- Published Status
- Collections (viele-zu-viele Beziehung)
- Variants (eins-zu-viele Beziehung)
Variants sind die SKUs:
- Price, SKU, Barcode
- Inventory
- Options (Size, Color, etc.)
Collections sind Kategorien:
- Title, Description
- Produkte (viele-zu-viele)
Aber was machst du, wenn du mehr brauchst? Das ist, wo Metafields einsteigen.
Metafields: Custom Daten anhängen
Was sind Metafields?
Metafields sind Key-Value Paare, die du an jede Standard-Entität anhängen kannst.
Beispiel: Produkt mit zusätzliche Eigenschaften
query GetProduct($id: ID!) {
product(id: $id) {
id
title
metafields(first: 10) {
edges {
node {
id
key
value
type
}
}
}
}
}
Metafield-Typen
Shopify bietet vorgefertigte Typen:
| Typ | Beispiel | Nutzung |
|---|---|---|
single_line_text |
"Baumwolle" | Material |
multi_line_text |
"Langbeschreibung" | Detaillierte Info |
number_integer |
"100" | Menge |
number_decimal |
"19.99" | Preis-Addon |
date |
"2026-02-19" | Verfügbarkeitsdatum |
date_time |
"2026-02-19T10:30" | Zeitstempel |
boolean |
true/false | Ja/Nein Flags |
json |
{...} |
Beliebige Struktur |
list.single_line_text |
["rot", "blau"] | Array von Text |
list.number_integer |
[10, 20] | Array von Numbers |
url |
"https://..." | Links |
file_reference |
File ID | Datei-Zugang |
Metafield-Beispiele
Beispiel 1: Material-Info für Produkt
mutation CreateProductMetafield($id: ID!, $metafield: MetafieldsSetInput!) {
metafieldsSet(ownerId: $id, metafields: $metafield) {
metafields {
id
key
value
}
}
}
Input:
{
"id": "gid://shopify/Product/123",
"metafield": {
"key": "material_composition",
"value": "{\"cotton\": 85, \"polyester\": 15}",
"type": "json"
}
}
Beispiel 2: Lagerbestands-Warnung
{
"key": "low_stock_threshold",
"value": "10",
"type": "number_integer"
}
Metafields im Theme (Liquid)
<!-- Produkt-Material anzeigen -->
<p>
Material: {{ product.metafields.custom.material_composition.value }}
</p>
<!-- Mit Fallback -->
{% if product.metafields.custom.material_composition %}
<p>{{ product.metafields.custom.material_composition.value }}</p>
{% else %}
<p>Material nicht spezifiziert</p>
{% endif %}
<!-- JSON parsen -->
{% assign material = product.metafields.custom.material_composition.value | parse_json %}
Baumwolle: {{ material.cotton }}%
Metaobjects: Strukturierte Custom Data
Was sind Metaobjects?
Metaobjects sind völlig custom Datenmodelle, die du selbst definierst. Sie sind wie "Custom Tables in Shopify".
Metaobjects vs. Metafields
| Aspekt | Metafields | Metaobjects |
|---|---|---|
| Anhängsel | Ja (an Produkt, Order, etc.) | Nein (standalone) |
| Struktur | Key-Value Paare | Vollständiges Datenmodell |
| Relationships | Begrenzt | Ja, bidirektional |
| Komplexität | Einfache Daten | Komplexe Daten |
| Abfragen | Per Entität | GraphQL wie Standard-Entitäten |
Metaobject-Beispiel: Produktbundle
# Metaobject Definition (via Admin)
# Typ: "product_bundle"
# Felder:
# - bundle_name (single_line_text)
# - bundle_description (multi_line_text)
# - bundle_price (number_decimal)
# - products (list.product_reference)
# - discount_percentage (number_integer)
In GraphQL abfragen:
query GetBundle($id: ID!) {
metaobject(id: $id) {
id
handle
fields {
key
value
}
}
}
Resultat:
{
"metaobject": {
"id": "gid://shopify/Metaobject/abc123",
"handle": "summer-bundle",
"fields": [
{
"key": "bundle_name",
"value": "Summer Bundle"
},
{
"key": "bundle_price",
"value": "99.99"
},
{
"key": "products",
"value": ["gid://shopify/Product/123", "gid://shopify/Product/456"]
}
]
}
}
Relationships: Metaobjects verbinden
Das Neue in 2024: Metaobjects können sich gegenseitig referenzieren.
Beispiel: Brand ↔ Product
Metaobject "brand":
Fields:
- brand_name (text)
- brand_logo (file_reference)
- brand_description (text)
- featured_products (list.product_reference) ← Relationship!
Im Liquid:
{% if product.metafields.custom.brand %}
{% assign brand = product.metafields.custom.brand.value %}
<p>Brand: {{ brand.brand_name }}</p>
<img src="{{ brand.brand_logo.url }}" alt="">
{% endif %}
Bidirektionale Relationships
query {
products(first: 5) {
edges {
node {
id
title
# Zurück zum Brand (wenn definiert)
brand: metafield(namespace: "custom", key: "brand") {
value
}
}
}
}
# Umgekehrt
metaobjects(type: "brand", first: 5) {
edges {
node {
id
fields {
value # Diese Products referenzieren mich
}
}
}
}
}
Real-World: Bekateq Case Study
Bekateq brauchte ein komplexes Datenmodell:
- 4400+ Zeilen Code
- 24 verschiedene Collection Templates
- Custom Attribute für jeden Product-Typ
- Null externe Dependencies
Ihre Lösung: Metaobjects + Metafields
Das Datenmodell
Product
├── Metafields
│ ├── technical_specs (json)
│ ├── available_colors (list.text)
│ └── warranty_period (number_integer)
└── Relationships via Metafield
└── Documentation Metaobject
├── manual_pdf
├── installation_guide
└── faq_entries
Brand (Metaobject)
├── brand_name
├── brand_logo
├── products (list.product_reference)
└── certifications (list.text)
Category (Metaobject)
├── category_name
├── parent_category (category_reference)
└── featured_products (list.product_reference)
Warum das funktioniert: Bekateq brauchte keine 5 Custom Apps. Sie brauchten ein sauberes Datenmodell.
Metafields oder Metaobjects? Entscheidungsmatrix
| Szenario | Lösung |
|---|---|
| Zusätzliche Produkt-Info (z.B. Material) | Metafield |
| Viele zusätzliche Felder an Produkt | Metaobject (dann referenzieren) |
| Separate "Tabelle" mit eigner Entität | Metaobject |
| Beziehung zwischen Produkten | Metaobject mit Relationships |
| Kunden-Daten erweitern | Metafield am Customer |
| Komplexe Geschäftslogik-Daten | Externe DB + Custom App |
GraphQL für Custom Data Queries
Metafields abfragen
query GetProductsWithMaterial {
products(first: 10, query: "has_metafield:custom.material_composition") {
edges {
node {
id
title
metafields(namespace: "custom", first: 10) {
edges {
node {
key
value
type
}
}
}
}
}
}
}
Metaobjects mit Filtering
query GetBundles {
metaobjects(type: "product_bundle", first: 10) {
edges {
node {
id
fields {
key
value
}
}
}
}
}
Komplexe Queries: Produkt + Metaobject + Relationships
query {
product(id: "gid://shopify/Product/123") {
id
title
variants(first: 5) {
edges {
node {
id
price
sku
}
}
}
# Metafield-Reference zu Brand
brand: metafield(namespace: "custom", key: "brand") {
reference {
... on Metaobject {
id
fields {
key
value
}
# Back-Reference
# products (würde funktionieren wenn bidirektional definiert)
}
}
}
}
}
Praktische Architektur-Entscheidungen
Szenario 1: Product Bundles
Option A: Metafield auf Produkt
{
"key": "bundle_products",
"value": "[\"gid://shopify/Product/1\", \"gid://shopify/Product/2\"]",
"type": "list.product_reference"
}
Problem: Wenn Bundle-Preis ändert, musst du alle verknüpften Produkte updaten.
Option B: Metaobject "Bundle"
Bundle (Metaobject)
├── bundle_name
├── bundle_price
├── bundled_products (list.product_reference)
└── discount_amount
Vorteil: Bundle existiert unabhängig. Klare Verantwortlichkeit.
Unsere Empfehlung: Option B (Metaobject)
Szenario 2: Technische Spezifikationen
Option A: Viele einzelne Metafields
- power_consumption
- operating_temperature
- noise_level
- warranty_months
Problem: Für jede neue Spec brauchst du ein neues Metafield. Unflexibel.
Option B: Ein JSON Metafield
{
"key": "technical_specs",
"value": "{\"power\": 100, \"temp_min\": -20, \"temp_max\": 50, \"noise\": 65}",
"type": "json"
}
Vorteil: Flexibel. Neue Specs ohne Schema-Änderung.
Unsere Empfehlung: Option B (JSON Metafield)
Limits und Workarounds
Limit 1: Kein Zugriff auf externe Daten-Quellen
Problem: Du brauchst Shopify-Daten + ERP-Daten kombiniert in GraphQL.
Lösung: Custom App als Middleware
// Deine Custom App
app.post('/api/product-with-erp', async (req, res) => {
const { productId } = req.body;
// 1. Shopify Daten
const shopifyProduct = await shopify.graphql(`
query { product(id: "${productId}") { ... } }
`);
// 2. ERP Daten
const erpData = await erp.getProduct(shopifyProduct.sku);
// 3. Kombinieren
res.json({
...shopifyProduct,
erpInventory: erpData.inventory,
erpCost: erpData.cost
});
});
Limit 2: Performance bei vielen Metafields
Problem: Ein Produkt mit 50 Metafields = langsam zu laden
Lösung: Strategisch Metaobjects nutzen
// Statt:
// Product {
// metafield_1, metafield_2, ..., metafield_50
// }
// Besser:
Product {
technical_specs: Metaobject, // 30 Specs gebündelt
marketing_data: Metaobject, // 15 Marketing-Felder
inventory_settings: Metaobject // 5 Inventory-Felder
}
Limit 3: Keine Transaktionen über Entitäten
Problem: Du brauchst atomare Updates über Produkt + Metaobject.
Lösung: Custom App mit Webhook-Handling
// Wenn Webhook "order.create" kommt
// 1. Update Shopify Product Inventory
// 2. Update Custom Metaobject "sales_stats"
// Mit Retry-Logic falls eines fehlschlägt
try {
await updateProductInventory(orderId);
await updateSalesStats(orderId);
} catch (error) {
// Retry oder Rollback
await logError(orderId, error);
}
Best Practices
1. Namespace-Konvention
<!-- ✅ RICHTIG: Namespace für Organisation -->
product.metafields.custom.material_composition
product.metafields.marketing.hero_text
product.metafields.technical.power_rating
<!-- ❌ FALSCH: Alles in einem -->
product.metafields.custom.everything
2. Typen korrekt setzen
// ✅ RICHTIG
{
"key": "quantity",
"value": "100",
"type": "number_integer"
}
// ❌ FALSCH
{
"key": "quantity",
"value": "\"100\"", // String statt Integer
"type": "number_integer"
}
3. Relationships statt komplexe JSONs
// ❌ FALSCH: Stark gekoppelt
{
"key": "related_products",
"value": "[{\"id\": \"123\", \"name\": \"Product\"}, ...]",
"type": "json"
}
// ✅ RICHTIG: Loose Coupling
{
"key": "related_products",
"value": "[\"gid://shopify/Product/123\", ...]",
"type": "list.product_reference"
}
4. Versionierung bei Changes
// Wenn du Metafield-Struktur änderst
// v1: { material: "cotton" }
// v2: { material: "cotton", origin_country: "India" }
// Versioniere!
product.metafields.custom.material_data_v2
// Alte Clients nutzen v1, neue v2
Checkliste: Datenmodell-Design
- Habe ich die Standard-Felder (Products, Collections, etc.) genutzt?
- Sind Metafields das richtige für meine Custom Daten?
- Sollte ich stattdessen Metaobjects nutzen?
- Sind Relationships definiert?
- Habe ich Typen korrekt gewählt?
- Gibt es Limits, die ich überschreite?
- Brauchst du externe Daten (dann: Custom App)?
- Sind Namespace-Konventionen konsistent?
- Habe ich Performance-Implikationen erwogen?
Über den Autor
Claudio Gerlich ist Technical Architect bei smplx. und seit 2020 spezialisiert auf Shopify-Datenmodellierung. Mit Bekateq haben wir ein komplexes Datenmodell mit Metaobjects elegant gelöst – Zero External Dependencies, Pure Shopify.
Datenmodellierung ist das Fundament. Gute Modelle = einfache Features. Schlechte Modelle = Chaos.
smplx. ist Shopify Technical Partner (seit 2020) mit Sitz in Coesfeld, NRW.