Gobernanza SAP HANA

Estructura, versionado y mejores prácticas para proyectos escalables

📦 Versionado Git desde el Día Uno

Principio:

El código de HANA (tablas, vistas, cálculos, seguridad) debe estar en Git desde la primera línea. No es un "extra", es el repositorio de verdad.

Estructura del Repositorio:

proyecto-logali/ ├── .git/ (Git history) ├── .gitignore (Archivos no versionados) ├── README.md (Documentación general) ├── docs/ (Documentación técnica) │ ├── ARCHITECTURE.md │ ├── NAMING_CONVENTIONS.md │ └── DEPLOYMENT.md ├── db/ (Módulo de base de datos) │ ├── mta.yaml (Despliegue) │ ├── package.json (Dependencias npm) │ ├── src/ │ │ ├── logali/ (Dominio principal) │ │ │ ├── core/ (Tablas base, dim comunes) │ │ │ ├── sales/ (Dominio de ventas) │ │ │ ├── finance/ (Dominio de finanzas) │ │ │ ├── supply/ (Dominio de supply chain) │ │ │ └── shared/ (Dimensiones compartidas) │ └── cfg/ (Configuración) │ ├── HANA.yml │ └── credentials.env ├── srv/ (Servicios OData / CAP) └── app/ (Frontend UI5 / Fiori)

.gitignore para SAP HANA:

# No versionamos artifacts de compilación *.jar *.zip *.tar.gz /db/.hanarc /db/node_modules /db/package-lock.json # Configuración local y secretos .env .env.local credentials.json secrets/ *.key *.pem # IDEs locales .vscode/ .idea/ *.swp *.swo *~ # Builds /mta_archives/ dist/ build/ # Logs *.log # OS .DS_Store Thumbs.db

Commits estructurados:

# ✅ BUENO - Describe qué y por qué git commit -m "core: Add Products dimension for sales analytics - Add hdbtable Product with 8 fields (ID, name, category, price) - Add hdbindex for category lookups - Update namespace documentation - Reason: Foundation for sales cube, needed by SalesAgg calc view" # ❌ MALO - Vago git commit -m "Update tables" git commit -m "Fix stuff"
✅ Beneficio: Historial limpio = auditoría completa. Sabes quién cambió qué tabla, cuándo, y por qué. Rollback a cualquier punto en 30 segundos.

⚙️ Módulo db es Código

Archivos de HANA en el repo:

logali/core/ ├── data_types.hdbdd # Tipos de datos custom ├── Products.hdbtable # Tabla de productos ├── Products.hdbindex # Índice en PRODUCT_ID ├── Customers.hdbtable # Tabla de clientes ├── Sales.hdbtable # Tabla de hechos ├── SalesAgg.hdbcalculationview # Agregación de ventas ├── Products.hdbsynonym # Alias de tabla (si aplica) ├── MasterProductData.hdbtabledata # Data maestro inicial └── .hdbgrants # Permisos por rol logali/core/grants/ ├── tech_user_core.hdbgrants ├── analyst_user_sales.hdbgrants └── integration_service.hdbgrants

Cada tipo de archivo:

.hdbtable

Definición de tabla. Siempre en Git. Es el "schema definition".

.hdbindex

Índices para performance. Siempre en Git. Afecta queries.

.hdbcalculationview

Vistas de cálculo. Siempre en Git. Lógica de analítica.

.hdbsynonym

Aliases de tablas. Siempre en Git. Encapsulación.

.hdbgrants

Permisos por rol. Siempre en Git. Auditoría de seguridad.

.hdbtabledata

Datos maestros. Solo datos estables. NO para datasets grandes.

Archivo de ejemplo: Products.hdbtable

namespace logali.core; @AbapCatalog.sqlViewName: 'PRODUCTS' @AccessControl.authorizationCheck: #CHECK @EndUserText.label: 'Catálogo de Productos' context Products { type ProductKey : String(20); entity Products { key ID: ProductKey; NAME: String(255); CATEGORY: String(50); LIST_PRICE: Decimal(10,2); COST: Decimal(10,2); ACTIVE: Boolean default true; CREATED_AT: Timestamp; UPDATED_AT: Timestamp; }; define view Products as SELECT from Products as p { p.ID, p.NAME, p.CATEGORY, p.LIST_PRICE, p.COST, p.ACTIVE, p.CREATED_AT, p.UPDATED_AT }; };
✅ Ventaja: Todo en Git = puedes rollback, comparar versiones, hacer code review antes de deploy. Es como tener Git para tu BD.

🏢 Separación de Namespaces por Dominio Funcional

El problema:

⚠️ Anti-patrón: Todo en logali.hana.* → A los 6 meses tienes 500 tablas en el mismo namespace. Nadie sabe dónde está nada. Los pulls tardan 10 minutos. Merge conflicts diarios.

La solución: Namespaces por dominio:

logali.core (Datos maestros compartidos) logali.core.dimensions (Dimensiones comunes) logali.sales (Ventas y órdenes) logali.sales.analytics (Agregaciones de ventas) logali.finance (Contabilidad, facturas) logali.finance.analytics (Reportes financieros) logali.supply (Inventory, supply chain) logali.supply.analytics (KPIs de logística) logali.integration (Integraciones externas) logali.security (Datos sensibles, encriptados)

Estructura de carpetas en Git:

src/ ├── logali/ │ ├── core/ │ │ ├── dimensions/ │ │ │ ├── Customer.hdbtable │ │ │ ├── Product.hdbtable │ │ │ ├── Time.hdbtable │ │ │ └── .hdbgrants │ │ └── shared/ │ │ ├── CommonTypes.hdbdd │ │ └── .hdbgrants │ │ │ ├── sales/ │ │ ├── Orders.hdbtable │ │ ├── OrderItems.hdbtable │ │ ├── SalesKPI.hdbcalculationview │ │ └── .hdbgrants │ │ │ ├── finance/ │ │ ├── Invoices.hdbtable │ │ ├── InvoiceLines.hdbtable │ │ ├── FinanceAgg.hdbcalculationview │ │ └── .hdbgrants │ │ │ └── security/ │ ├── SensitiveData.hdbtable │ ├── RLS_Rules.hdbgrants │ └── Encryption.hdbdd

Beneficios por dominio:

💡 Nota: Los namespaces también afectan OData. Un modelo CDS en logali.sales expone automáticamente las vistas como /odata/logali.sales/SalesKPI.

🔐 .hdbgrants - Principio de Menor Privilegio

Regla de oro:

Concede SOLO los permisos mínimos necesarios para que el usuario haga su trabajo. Si no lo necesita, no se lo des.

Tipos de usuarios en SAP HANA:

Usuario Técnico (Tech User)

Despliega código, ejecuta migraciones, desarrolla vistas.

Permisos: CREATE TABLE, EXECUTE, SELECT en su schema.

Usuario de Análisis (Analyst)

Lee dashboards, ejecuta reportes.

Permisos: SELECT en vistas (NO en tablas base).

Servicio de Integración

Carga datos desde sistemas externos (SAP ECC, etc).

Permisos: INSERT/UPDATE/DELETE en tablas staging.

Admin (Raro)

Mantenimiento crítico, recuperación.

Permisos: Acceso total (controlado).

Ejemplo: .hdbgrants para usuario técnico

role_name: "logali_tech_core"; privileges: # Crear y modificar tablas en su schema - privilege: "CREATE TABLE"; object_type: "SCHEMA"; object_name: "LOGALI_CORE"; # Ejecutar vistas de cálculo (para testing) - privilege: "SELECT"; object_type: "VIEW"; object_names: - "LOGALI_CORE::Products"; - "LOGALI_CORE::Customers"; # NO darle INSERT/UPDATE en tablas base (eso lo hace App) # NO darle DROP (peligroso) # NO darle ALTER SYSTEM (nivel DB) # Asignación al usuario: user: "TECH_LOGALI"; roles: - "logali_tech_core";

Ejemplo: .hdbgrants para usuario de análisis

role_name: "logali_analyst_sales"; privileges: # Solo lectura en vistas de agregación - privilege: "SELECT"; object_type: "VIEW"; object_names: - "LOGALI.SALES::SalesKPI"; - "LOGALI.SALES::RegionalAnalysis"; # NO acceso a tabla de hechos directa (usa vistas) # NO acceso a tablas de staging # NO acceso a datos sensibles # Asignación: user: "ANALYST_MARIA"; roles: - "logali_analyst_sales";

Ejemplo: .hdbgrants para servicio de integración

role_name: "logali_integration_ecc"; privileges: # INSERT/UPDATE en tablas de staging (no master) - privilege: "INSERT"; object_type: "TABLE"; object_names: - "LOGALI_INTEGRATION::SAP_ECC_ORDERS_STAGING"; - "LOGALI_INTEGRATION::SAP_ECC_ITEMS_STAGING"; - privilege: "UPDATE"; object_type: "TABLE"; object_names: - "LOGALI_INTEGRATION::SAP_ECC_ORDERS_STAGING"; # NO DELETE (evita borrados accidentales) # NO acceso a tablas master # NO modificar vistas # Asignación: user: "ECC_INTEGRATION_USER"; password: "(gestionar en vault, no en código)"; roles: - "logali_integration_ecc";
⚠️ Nunca hagas esto:
  • Conceder SYSTEM PRIVILEGE a usuarios de aplicación
  • Dar DELETE a usuarios que no lo necesitan
  • Usar una contraseña default en .hdbgrants
  • El mismo usuario para múltiples capas (app, integration, analytics)
✅ Beneficio: Si un usuario es comprometido, el daño es limitado a sus permisos. Si ANALYST_MARIA es hackeada, solo puede leer vistas de sales, no tocar datos sensibles.

📊 .hdbtabledata - Cuándo Usarlo (y Cuándo No)

¿Qué es .hdbtabledata?

Un archivo CSV o JSON que se carga automáticamente en una tabla durante el despliegue. Útil para datos maestros estables que nunca cambian.

✅ CORRECTO: Usar .hdbtabledata para:

Datos maestros pequeños y estables:
  • Catálogo de países (200 registros)
  • Configuración de tipos de producto (50 registros)
  • Dimensión Tiempo (7 años × 365 días = 2.555 registros)
  • Códigos de estado (10 valores: 'Pendiente', 'Aprobado', etc.)

Ejemplo correcto: Countries.hdbtabledata

{ "format_version": 1, "imports": [ { "target_table": "LOGALI_CORE::Countries", "source_data": { "data_file": "Countries.csv" }, "import_settings": { "create_target": false, "mode": "REPLACE" } } ] } # Countries.csv: COUNTRY_CODE,COUNTRY_NAME,REGION,CURRENCY ES,Spain,Europe,EUR UK,United Kingdom,Europe,GBP FR,France,Europe,EUR US,USA,Americas,USD

❌ INCORRECTO: NO usar .hdbtabledata para:

Datasets grandes o transaccionales:
  • Tabla de Ventas (millones de registros) → Sistema lentísimo en deploy
  • Datos de clientes activos (50M registros) → NO cabe en un archivo
  • Staging de integraciones → Cambia diariamente
  • Datos históricos mensurales → Crece constantemente
  • Credenciales o datos sensibles → NUNCA en .hdbtabledata

Comparación:

Scenario .hdbtabledata Alternativa
Cargar 200 países una sola vez ✅ Perfecto
Importar 5M registros de ventas ❌ No API REST + JDBC, batch job
Datos maestro estable (lista de categorías) ✅ Sí
Staging diario de ECC ❌ No Data replication, BW extractor
Dimensión Tiempo (fechas precargadas) ✅ Sí
Contraseñas o API keys ❌ NUNCA Credential store, environment vars
💡 Regla práctica: Si tienes más de 10K registros o los datos cambian más de 1x al mes, NO uses .hdbtabledata.

⚡ Cálculo Views: Filtros Tempranos

Principio:

Cuanto antes reduzcas filas, más rápido el motor HANA. Los filtros en la proyección base son más eficientes que en el join.

Ejemplo: Problema (Lento)

-- ❌ MALO: Lee 50M filas, luego filtra define view SalesAnalysis as SELECT Sales.ID, Sales.AMOUNT, Customer.NAME, Customer.COUNTRY FROM SALES as Sales INNER JOIN CUSTOMER as Customer ON Sales.CUSTOMER_ID = Customer.ID WHERE Customer.COUNTRY = :SELECTED_COUNTRY; -- Filtro al final → Lee todo primero

Solución: Filtro temprano

-- ✅ BUENO: Filtra en la proyección base define view SalesAnalysis as SELECT Sales.ID, Sales.AMOUNT, Customer.NAME, Customer.COUNTRY FROM SALES as Sales INNER JOIN ( -- Proyección base filtrada PRIMERO SELECT * FROM CUSTOMER WHERE COUNTRY = :SELECTED_COUNTRY ) as Customer ON Sales.CUSTOMER_ID = Customer.ID; -- Resultado: Solo 10M filas en join (no 50M)

Ejemplo avanzado: Árbol de filtros

-- Proyección 1: Filtrar tabla de hechos por rango de fecha projection_sales as ( SELECT * FROM SALES WHERE SALE_DATE BETWEEN :START_DATE AND :END_DATE ), -- Proyección 2: Filtrar clientes por país projection_customers as ( SELECT * FROM CUSTOMER WHERE COUNTRY IN (:SELECTED_COUNTRIES) ), -- Proyección 3: Filtrar productos por categoría projection_products as ( SELECT * FROM PRODUCT WHERE CATEGORY IN (:SELECTED_CATEGORIES) ), -- Ahora los joins ya están pre-filtrados final_result as ( SELECT ps.ID, ps.AMOUNT, pc.NAME as CUSTOMER, pp.NAME as PRODUCT FROM projection_sales ps INNER JOIN projection_customers pc ON ps.CUSTOMER_ID = pc.ID INNER JOIN projection_products pp ON ps.PRODUCT_ID = pp.ID ) SELECT * FROM final_result;

Comparación de performance:

Escenario Sin Filtro Temprano Con Filtro Temprano
50M ventas, filtrar por país (ES) 8.2 segundos 0.3 segundos
100M registros, 2 joins 15+ segundos 1-2 segundos
Agregación por mes (últimos 6 meses) 5.1 segundos 0.2 segundos

Checklist de optimización:

✅ Resultado: Queries que tardan 15 segundos → 0.5 segundos. Dashboards responsivos. Usuarios felices.

✅ Checklist de Gobernanza

Antes de hacer commit:

Antes de hacer deploy:

💡 Última regla: Si no está en Git, no existe. Si existe pero no está en Git, peligra. Siempre: Git primero, deploy segundo.