En la Guia de arquitectura Shopify cubrimos los fundamentos. Hoy analizamos como modelar datos en Shopify -- la base de toda buena arquitectura.
En smplx., hemos experimentado con diferentes enfoques. Metafields? Metaobjects? Custom apps con bases de datos separadas? Cada opcion tiene su lugar. Tomemos juntos la decision correcta.
El modelo de datos de Shopify
Shopify tiene un modelo de datos rigido pero inteligente:
Shop
|-- Products
| |-- Variants
| +-- Metafields (desde 2021)
|-- Collections
| +-- Metafields
|-- Customers
| +-- Metafields
|-- Orders
| +-- Metafields
+-- Metaobjects (desde 2024)
+-- Fields & Relationships
Las entidades estandar
Products son la pieza central:
- Title, Description, Vendor
- Published Status
- Collections (relacion muchos-a-muchos)
- Variants (relacion uno-a-muchos)
Variants son los SKUs:
- Price, SKU, Barcode
- Inventory
- Options (Size, Color, etc.)
Collections son categorias:
- Title, Description
- Products (muchos-a-muchos)
Pero que haces cuando necesitas mas? Ahi es donde entran los metafields.
Metafields: Adjuntar datos personalizados
Que son los Metafields?
Los metafields son pares clave-valor que puedes adjuntar a cualquier entidad estandar.
Ejemplo: Producto con propiedades adicionales
query GetProduct($id: ID!) {
product(id: $id) {
id
title
metafields(first: 10) {
edges {
node {
id
key
value
type
}
}
}
}
}
Tipos de metafields
Shopify ofrece tipos predefinidos:
| Tipo | Ejemplo | Uso |
|---|---|---|
single_line_text |
"Algodon" | Material |
multi_line_text |
"Descripcion larga" | Info detallada |
number_integer |
"100" | Cantidad |
number_decimal |
"19.99" | Precio adicional |
date |
"2026-02-19" | Fecha de disponibilidad |
date_time |
"2026-02-19T10:30" | Marca de tiempo |
boolean |
true/false | Flags Si/No |
json |
{...} |
Estructura arbitraria |
list.single_line_text |
["rojo", "azul"] | Array de texto |
list.number_integer |
[10, 20] | Array de numeros |
url |
"https://..." | Enlaces |
file_reference |
File ID | Acceso a archivos |
Ejemplos de metafields
Ejemplo 1: Info de material para producto
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"
}
}
Ejemplo 2: Alerta de stock bajo
{
"key": "low_stock_threshold",
"value": "10",
"type": "number_integer"
}
Metafields en el theme (Liquid)
<!-- Mostrar material del producto -->
<p>
Material: {{ product.metafields.custom.material_composition.value }}
</p>
<!-- Con fallback -->
{% if product.metafields.custom.material_composition %}
<p>{{ product.metafields.custom.material_composition.value }}</p>
{% else %}
<p>Material no especificado</p>
{% endif %}
<!-- Parsear JSON -->
{% assign material = product.metafields.custom.material_composition.value | parse_json %}
Algodon: {{ material.cotton }}%
Metaobjects: Datos personalizados estructurados
Que son los Metaobjects?
Los metaobjects son modelos de datos completamente personalizados que tu mismo defines. Son como "tablas personalizadas en Shopify."
Metaobjects vs. Metafields
| Aspecto | Metafields | Metaobjects |
|---|---|---|
| Adjunto | Si (a Product, Order, etc.) | No (independiente) |
| Estructura | Pares clave-valor | Modelo de datos completo |
| Relationships | Limitados | Si, bidireccionales |
| Complejidad | Datos simples | Datos complejos |
| Consultas | Por entidad | GraphQL como entidades estandar |
Ejemplo de Metaobject: Product Bundle
# Definicion de Metaobject (via Admin)
# Tipo: "product_bundle"
# Campos:
# - bundle_name (single_line_text)
# - bundle_description (multi_line_text)
# - bundle_price (number_decimal)
# - products (list.product_reference)
# - discount_percentage (number_integer)
Consulta en GraphQL:
query GetBundle($id: ID!) {
metaobject(id: $id) {
id
handle
fields {
key
value
}
}
}
Resultado:
{
"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: Conectar Metaobjects
Novedad en 2024: Los metaobjects pueden referenciarse mutuamente.
Ejemplo: Brand <-> Product
Metaobject "brand":
Campos:
- brand_name (text)
- brand_logo (file_reference)
- brand_description (text)
- featured_products (list.product_reference) <- Relationship!
En Liquid:
{% if product.metafields.custom.brand %}
{% assign brand = product.metafields.custom.brand.value %}
<p>Marca: {{ brand.brand_name }}</p>
<img src="{{ brand.brand_logo.url }}" alt="">
{% endif %}
Relationships bidireccionales
query {
products(first: 5) {
edges {
node {
id
title
# De vuelta a la Brand (si esta definida)
brand: metafield(namespace: "custom", key: "brand") {
value
}
}
}
}
# Inverso
metaobjects(type: "brand", first: 5) {
edges {
node {
id
fields {
value # Estos Products me referencian
}
}
}
}
}
Mundo real: Caso de estudio Bekateq
Bekateq necesitaba un modelo de datos complejo:
- Mas de 4.400 lineas de codigo
- 24 plantillas de collection diferentes
- Atributos personalizados para cada tipo de producto
- Cero dependencias externas
Su solucion: Metaobjects + Metafields
El modelo de datos
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)
Por que funciona: Bekateq no necesitaba 5 custom apps. Necesitaban un modelo de datos limpio.
Metafields o Metaobjects? Matriz de decision
| Escenario | Solucion |
|---|---|
| Info adicional de producto (p. ej., material) | Metafield |
| Muchos campos adicionales en producto | Metaobject (luego referenciar) |
| "Tabla" separada con entidad propia | Metaobject |
| Relacion entre productos | Metaobject con relationships |
| Ampliar datos de cliente | Metafield en Customer |
| Datos de logica de negocio compleja | DB externa + Custom App |
GraphQL para consultas de Custom Data
Consultar Metafields
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 con filtrado
query GetBundles {
metaobjects(type: "product_bundle", first: 10) {
edges {
node {
id
fields {
key
value
}
}
}
}
}
Consultas complejas: Product + Metaobject + Relationships
query {
product(id: "gid://shopify/Product/123") {
id
title
variants(first: 5) {
edges {
node {
id
price
sku
}
}
}
# Referencia Metafield a Brand
brand: metafield(namespace: "custom", key: "brand") {
reference {
... on Metaobject {
id
fields {
key
value
}
# Back-reference
# products (funcionaria si se define bidireccionalmente)
}
}
}
}
}
Decisiones practicas de arquitectura
Escenario 1: Product Bundles
Opcion A: Metafield en producto
{
"key": "bundle_products",
"value": "[\"gid://shopify/Product/1\", \"gid://shopify/Product/2\"]",
"type": "list.product_reference"
}
Problema: Si el precio del bundle cambia, tienes que actualizar todos los productos vinculados.
Opcion B: Metaobject "Bundle"
Bundle (Metaobject)
|-- bundle_name
|-- bundle_price
|-- bundled_products (list.product_reference)
+-- discount_amount
Ventaja: El bundle existe de forma independiente. Responsabilidad clara.
Nuestra recomendacion: Opcion B (Metaobject)
Escenario 2: Especificaciones tecnicas
Opcion A: Muchos metafields individuales
- power_consumption
- operating_temperature
- noise_level
- warranty_months
Problema: Para cada nueva especificacion necesitas un nuevo metafield. Inflexible.
Opcion B: Un Metafield JSON
{
"key": "technical_specs",
"value": "{\"power\": 100, \"temp_min\": -20, \"temp_max\": 50, \"noise\": 65}",
"type": "json"
}
Ventaja: Flexible. Nuevas especificaciones sin cambios de esquema.
Nuestra recomendacion: Opcion B (JSON Metafield)
Limites y soluciones alternativas
Limite 1: Sin acceso a fuentes de datos externas
Problema: Necesitas datos de Shopify + datos de ERP combinados en GraphQL.
Solucion: Custom app como middleware
// Tu Custom App
app.post('/api/product-with-erp', async (req, res) => {
const { productId } = req.body;
// 1. Datos de Shopify
const shopifyProduct = await shopify.graphql(`
query { product(id: "${productId}") { ... } }
`);
// 2. Datos del ERP
const erpData = await erp.getProduct(shopifyProduct.sku);
// 3. Combinar
res.json({
...shopifyProduct,
erpInventory: erpData.inventory,
erpCost: erpData.cost
});
});
Limite 2: Rendimiento con muchos metafields
Problema: Un producto con 50 metafields = lento de cargar
Solucion: Usar metaobjects estrategicamente
// En lugar de:
// Product {
// metafield_1, metafield_2, ..., metafield_50
// }
// Mejor:
Product {
technical_specs: Metaobject, // 30 specs agrupadas
marketing_data: Metaobject, // 15 campos de marketing
inventory_settings: Metaobject // 5 campos de inventario
}
Limite 3: Sin transacciones entre entidades
Problema: Necesitas actualizaciones atomicas entre Product + Metaobject.
Solucion: Custom app con manejo de webhooks
// Cuando llega el webhook "order.create"
// 1. Actualizar Shopify Product Inventory
// 2. Actualizar Custom Metaobject "sales_stats"
// Con logica de reintentos si uno falla
try {
await updateProductInventory(orderId);
await updateSalesStats(orderId);
} catch (error) {
// Reintentar o revertir
await logError(orderId, error);
}
Mejores practicas
1. Convencion de namespace
<!-- CORRECTO: Namespace para organizacion -->
product.metafields.custom.material_composition
product.metafields.marketing.hero_text
product.metafields.technical.power_rating
<!-- INCORRECTO: Todo en uno -->
product.metafields.custom.everything
2. Establecer tipos correctamente
// CORRECTO
{
"key": "quantity",
"value": "100",
"type": "number_integer"
}
// INCORRECTO
{
"key": "quantity",
"value": "\"100\"", // String en lugar de integer
"type": "number_integer"
}
3. Relationships en lugar de JSONs complejos
// INCORRECTO: Fuertemente acoplado
{
"key": "related_products",
"value": "[{\"id\": \"123\", \"name\": \"Product\"}, ...]",
"type": "json"
}
// CORRECTO: Acoplamiento debil
{
"key": "related_products",
"value": "[\"gid://shopify/Product/123\", ...]",
"type": "list.product_reference"
}
4. Versionado para cambios
// Cuando cambias la estructura de metafields
// v1: { material: "cotton" }
// v2: { material: "cotton", origin_country: "India" }
// Versionalizar!
product.metafields.custom.material_data_v2
// Clientes antiguos usan v1, nuevos v2
Checklist: Diseno de modelo de datos
- He utilizado los campos estandar (Products, Collections, etc.)?
- Son los metafields la opcion correcta para mis datos personalizados?
- Deberia usar metaobjects en su lugar?
- Estan definidos los relationships?
- He elegido los tipos correctamente?
- Hay limites que estoy excediendo?
- Necesito datos externos (entonces: Custom App)?
- Son consistentes las convenciones de namespace?
- He considerado las implicaciones de rendimiento?
Sobre el autor
Claudio Gerlich es Technical Architect en smplx. y esta especializado en modelado de datos Shopify desde 2020. Con Bekateq resolvimos elegantemente un modelo de datos complejo con metaobjects -- cero dependencias externas, Shopify puro.
El modelado de datos es la base. Buenos modelos = funcionalidades simples. Malos modelos = caos.
smplx. es Shopify Technical Partner (desde 2020) con sede en Munsterland, NRW.