🧠 Conocimiento Técnico Adquirido
Documentación completa de descubrimientos técnicos críticos no documentados previamente, técnicas avanzadas PostgreSQL y lecciones de arquitectura destiladas durante las migraciones de módulos y procesos de lógica de negocio.
50+ Descubrimientos
Estructuras de datos no documentadas reveladas
15 Técnicas PostgreSQL
Patrones avanzados aplicados y validados
Conocimiento Práctico
Lecciones destiladas de migración real
🔍 Estructura de Datos Descubierta
📊 Sistema de Multiprecios en Dolibarr
Descubrimiento: Estructura completa de multiprecios no documentada con cálculos en cascada.
Estructura Real
-- Tabla principal de reglas de precios
llx_product_pricerules:
- level: Nivel de precio (1-n)
- fk_level: Nivel base para cálculo (puede referenciar otro nivel)
- var_percent: Porcentaje de variación respecto al nivel base
- var_min_percent: Porcentaje para precio mínimo
-- Nueva tabla creada para multiprecios
llx_product_multiprice:
- Almacena precios por nivel
- Relación 1:N con llx_product
🔄 Facturas Recurrentes - Estructura Real
Descubrimiento: Columnas críticas no documentadas en llx_facture_rec y diferencias con llx_facturedet.
Columnas Críticas
llx_facture_rec:
- frequency: Número entero (1, 2, 3...)
- unit_frequency: Carácter ('d', 'm', 'y')
- date_when: Fecha inicial de generación
- date_last_gen: Última generación (timestamp)
- nb_gen_done: Contador de generaciones
- nb_gen_max: Límite de generaciones (0 = infinito)
- suspended: 0/1 para activar/desactivar
- auto_validate: Validación automática
- generate_pdf: Generar PDF automáticamente
llx_facturedet_rec:
- date_start_fill: 1 para fechas dinámicas
- date_end_fill: 1 para calcular fin de periodo
📦 Sistema de Estados en Pedidos
Estados Descubiertos
fk_statut en llx_commande:
0: Borrador
1: Validado
2: Enviado
3: Cerrado
-1: Cancelado
-- Configuración de Stock
STOCK_CALCULATE_ON_VALIDATE_ORDER: '1' activa cálculo en validación
STOCK_WAREHOUSE_DEFAULT: ID del almacén por defecto
-- Movimientos de Stock
Tipo 0: Entrada
Tipo 1: Salida
Campo 'inventorycode' útil para agrupar movimientos relacionados
📂 Tipos de Categoría y Relaciones
Tipos de Categoría (campo 'type' es INTEGER)
0: product (productos)
1: supplier (proveedores)
2: customer (clientes)
3: member (miembros)
4: contact (contactos)
5: bank_account (cuentas bancarias)
-- Tablas de Relación Necesarias
llx_categorie_product
llx_categorie_societe (para clientes Y proveedores)
llx_categorie_contact
llx_categorie_member
llx_categorie_account
llx_categorie_project
llx_categorie_user
llx_categorie_warehouse
🏦 Cuentas Bancarias - Campos Reales
Nombres de Columnas Reales
llx_bank_account:
- clos (no status): 0=abierta, 1=cerrada
- min_allowed: Saldo mínimo permitido
- min_desired: Saldo mínimo deseado
- currency_code: Moneda
- rappro en llx_bank: 1=conciliado, 0=pendiente
-- Tipos de Movimiento
fk_type en llx_bank contiene códigos como 'CHQ', 'VIR', 'CB'
num_chq para número de cheque/referencia
🚀 Técnicas PostgreSQL Avanzadas Aplicadas
📋 Arrays en PostgreSQL
Almacenar y Manipular Datos Temporales
-- Almacenar precios múltiples
v_prices := array_fill(0::numeric, ARRAY[v_limit]);
v_prices[nivel] := precio;
-- Path IDs en jerarquías
ARRAY[c.rowid] as path_ids
ch.path_ids || c.rowid
-- Uso práctico en multiprecios
DECLARE
v_prices numeric[];
v_limit integer := 5;
BEGIN
v_prices := array_fill(0::numeric, ARRAY[v_limit]);
v_prices[1] := p_base_price;
FOR v_rule IN SELECT * FROM rules LOOP
v_prices[v_rule.level] := v_prices[v_rule.fk_level] *
(1 + v_rule.var_percent / 100);
END LOOP;
END;
📡 NOTIFY/LISTEN para Procesos Asíncronos
Notificaciones Automáticas
-- En triggers para notificar cambios
PERFORM pg_notify('product_multiprice_update',
json_build_object(
'product_id', NEW.rowid,
'price', NEW.price
)::text
);
-- Cliente PHP escuchando
while(true) {
$result = pg_get_notify($connection);
if($result) {
$data = json_decode($result['payload']);
// Procesar cambio
}
}
🔄 ON CONFLICT para Upserts
Inserción o Actualización Atómica
-- Upsert para sistemas de caché
INSERT INTO llx_bank_account_balance_cache (
fk_account, cache_date, balance
) VALUES (
p_account_id, p_date, p_balance
)
ON CONFLICT (fk_account, cache_date)
DO UPDATE SET
balance = EXCLUDED.balance,
calculated_at = CURRENT_TIMESTAMP;
-- Upsert para multiprecios
INSERT INTO llx_product_multiprice (...) VALUES (...)
ON CONFLICT (fk_product, price_level)
DO UPDATE SET
price = EXCLUDED.price,
tms = CURRENT_TIMESTAMP;
📅 generate_series para Datos Temporales
Generación Eficiente de Secuencias
-- Generar fechas para facturación recurrente
SELECT date_val
FROM generate_series(
'2025-01-01'::date,
'2025-12-31'::date,
'1 month'::interval
) AS date_val;
-- Generar números de prueba
SELECT generate_series(1, 100) as test_id;
-- Uso en facturas recurrentes
CREATE OR REPLACE FUNCTION llx_facture_rec_get_next_dates(
p_rec_id integer,
p_count integer DEFAULT 12
)
RETURNS TABLE(next_date date)
LANGUAGE plpgsql AS $$
DECLARE
v_rec record;
v_interval interval;
BEGIN
SELECT * INTO v_rec FROM llx_facture_rec WHERE rowid = p_rec_id;
v_interval := (v_rec.frequency || ' ' || v_rec.unit_frequency)::interval;
RETURN QUERY
SELECT (v_rec.date_when + (generate_series(0, p_count-1) * v_interval))::date;
END;
$$;
🌳 CTEs Recursivos para Jerarquías
Consultas Jerárquicas Optimizadas
-- Obtener árbol completo de categorías
WITH RECURSIVE category_tree AS (
-- Nodos raíz
SELECT
rowid, label, fk_parent, 0 as level,
label::text as path,
ARRAY[rowid] as path_ids
FROM llx_categorie
WHERE fk_parent = 0 AND type = 0
UNION ALL
-- Nodos hijos
SELECT
c.rowid, c.label, c.fk_parent, ct.level + 1,
ct.path || ' > ' || c.label,
ct.path_ids || c.rowid
FROM llx_categorie c
INNER JOIN category_tree ct ON c.fk_parent = ct.rowid
)
SELECT * FROM category_tree ORDER BY path;
🎯 Patrones de Migración Efectivos
💾 Caché Inteligente con Invalidación
Problema: Cálculos costosos ejecutados repetidamente (saldos, estadísticas)
Solución: Tabla de caché + invalidación por triggers
Implementación Completa
-- 1. Crear tabla de caché
CREATE TABLE llx_bank_account_balance_cache (
fk_account integer,
cache_date date,
balance numeric,
calculated_at timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (fk_account, cache_date)
);
-- 2. Función que usa caché
CREATE OR REPLACE FUNCTION llx_bank_account_get_balance(
p_account_id integer,
p_date date,
p_use_cache boolean DEFAULT true
)
RETURNS numeric AS $$
DECLARE
v_balance numeric;
BEGIN
IF p_use_cache THEN
-- Buscar en caché
SELECT balance INTO v_balance
FROM llx_bank_account_balance_cache
WHERE fk_account = p_account_id AND cache_date = p_date;
IF FOUND THEN
RETURN v_balance;
END IF;
END IF;
-- Calcular
SELECT SUM(amount) INTO v_balance
FROM llx_bank
WHERE fk_account = p_account_id
AND dateo <= p_date;
-- Guardar en caché
INSERT INTO llx_bank_account_balance_cache
VALUES (p_account_id, p_date, v_balance)
ON CONFLICT DO UPDATE SET
balance = EXCLUDED.balance,
calculated_at = CURRENT_TIMESTAMP;
RETURN v_balance;
END;
$$ LANGUAGE plpgsql;
-- 3. Trigger que invalida caché
CREATE OR REPLACE FUNCTION invalidate_balance_cache()
RETURNS trigger AS $$
BEGIN
-- Invalidar entradas posteriores al movimiento
DELETE FROM llx_bank_account_balance_cache
WHERE fk_account = NEW.fk_account
AND cache_date >= NEW.dateo::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER bank_balance_cache_invalidate
AFTER INSERT OR UPDATE OR DELETE ON llx_bank
FOR EACH ROW
EXECUTE FUNCTION invalidate_balance_cache();
📊 Vistas con Estadísticas Precalculadas
Problema: N+1 queries para obtener estadísticas de entidades relacionadas
Solución: Vista que incluye contadores en la misma consulta
Vista Optimizada
-- Vista con estadísticas incluidas
CREATE VIEW llx_categorie_stats_view AS
SELECT
c.*,
COUNT(DISTINCT cp.fk_product) as product_count,
COUNT(DISTINCT cs.fk_soc) as societe_count,
COUNT(DISTINCT cc.fk_socpeople) as contact_count,
-- Estadísticas financieras
COALESCE(SUM(p.price), 0) as total_product_value,
COALESCE(AVG(p.price), 0) as avg_product_price,
-- Jerarquía
EXISTS(
SELECT 1 FROM llx_categorie child
WHERE child.fk_parent = c.rowid
) as has_children
FROM llx_categorie c
LEFT JOIN llx_categorie_product cp ON c.rowid = cp.fk_categorie
LEFT JOIN llx_categorie_societe cs ON c.rowid = cs.fk_categorie
LEFT JOIN llx_categorie_contact cc ON c.rowid = cc.fk_categorie
LEFT JOIN llx_product p ON cp.fk_product = p.rowid
WHERE c.type = 0 -- Solo productos
GROUP BY c.rowid, c.label, c.description, c.color, c.fk_parent, c.visible;
📋 Funciones que Retornan Tablas
Problema: Lógica compleja que necesita retornar múltiples registros
Solución: Funciones RETURNS TABLE usables como vistas dinámicas
Función Tabla para Multiprecios
CREATE OR REPLACE FUNCTION llx_product_get_multiprices(
p_product_id integer
)
RETURNS TABLE(
level integer,
price numeric,
price_ttc numeric,
price_min numeric,
date_price timestamp
)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY
SELECT
pm.price_level::integer,
pm.price::numeric,
pm.price_ttc::numeric,
pm.price_min::numeric,
pm.date_price::timestamp
FROM llx_product_multiprice pm
WHERE pm.fk_product = p_product_id
ORDER BY pm.price_level;
END;
$$;
-- Uso como tabla normal
SELECT * FROM llx_product_get_multiprices(123)
WHERE level <= 3;
⚠️ Problemas Comunes y Soluciones
🔴 Tipos de Datos
-- ❌ Error
SELECT llx_facture_get_next_ref(1, 0);
-- ✅ Correcto
SELECT llx_facture_get_next_ref(1::integer, 0::smallint);
🔴 Palabras Reservadas
-- ❌ Error
SELECT position FROM llx_categorie;
-- ✅ Correcto
SELECT "position" FROM llx_categorie;
🟡 Columnas Faltantes
-- Verificar estructura
\d llx_facture_rec
-- Descubrir diferencias
\d llx_facturedet
\d llx_facturedet_rec
🟡 Triggers de Validación
-- Verificar triggers existentes
\dS llx_facture
-- Incluir campos obligatorios en INSERT
⚡ Optimización y Rendimiento
📇 Índices Descubiertos Útiles
-- Para caché de saldos (orden temporal)
CREATE INDEX idx_balance_cache_lookup
ON llx_bank_account_balance_cache(fk_account, cache_date DESC);
-- Para jerarquías (parent + posición)
CREATE INDEX idx_categorie_hierarchy
ON llx_categorie(fk_parent, "position")
WHERE fk_parent IS NOT NULL;
-- Para facturación recurrente (próximas fechas)
CREATE INDEX idx_facture_rec_next_gen
ON llx_facture_rec(date_when, suspended)
WHERE suspended = 0;
🔄 CTEs vs Subconsultas
- CTEs recursivos: Más eficientes para jerarquías
- WITH ordinario: Más legible pero no siempre más rápido
- Subconsultas: Mejor para consultas simples
📊 Materialización de Vistas
Recomendación: No usar MATERIALIZED VIEW, mejor tabla + triggers
Razón: Más control sobre actualización incremental
-- En lugar de MATERIALIZED VIEW
CREATE TABLE llx_categorie_stats AS
SELECT ... FROM llx_categorie ...;
-- Con trigger de actualización
CREATE TRIGGER update_categorie_stats
AFTER INSERT OR UPDATE OR DELETE ON llx_categorie_product
FOR EACH ROW
EXECUTE FUNCTION refresh_categorie_stats();
🔗 Integración con PHP
🔄 Patrón de Migración PHP
Antes y Después
// ❌ ANTES: Lógica compleja en PHP
public function generateMultiprices($basePrice, $levels) {
$prices = array();
// 50+ líneas de validaciones y cálculos
for($i = 1; $i <= $levels; $i++) {
$rule = $this->getPriceRule($i);
if($rule->fk_level > 0) {
$prices[$i] = $prices[$rule->fk_level] * (1 + $rule->var_percent / 100);
} else {
$prices[$i] = $basePrice * (1 + $rule->var_percent / 100);
}
// Más validaciones...
}
return $prices;
}
// ✅ DESPUÉS: Delegar a PostgreSQL
public function generateMultiprices($basePrice, $levels) {
global $db;
$sql = "SELECT * FROM llx_product_generate_multiprices(".
(float)$basePrice.", ".
(int)$levels.", ".
(int)$this->entity.")";
$resql = $db->query($sql);
if($resql) {
$prices = array();
while($obj = $db->fetch_object($resql)) {
$prices[$obj->level] = $obj->price;
}
return $prices;
}
return false;
}
📤 Manejo de Resultados
// Para funciones que retornan tabla
$results = array();
while ($obj = $db->fetch_object($resql)) {
$results[] = $obj;
}
// Para funciones escalares
$obj = $db->fetch_object($resql);
$result = $obj->nombre_funcion;
// Para funciones que retornan JSON
$obj = $db->fetch_object($resql);
$data = json_decode($obj->json_result, true);
🏭 Consideraciones de Producción
🔒 Transacciones Largas
- Evitar locks prolongados en tablas grandes
- Usar CONCURRENTLY para índices cuando sea posible
- Dividir migraciones grandes en lotes
📊 Monitoreo
-- Queries útiles para monitoreo
SELECT * FROM pg_stat_user_functions; -- Uso de funciones
SELECT * FROM pg_stat_user_tables; -- Estadísticas de tablas
SELECT * FROM pg_locks; -- Locks activos
💾 Backup y Restore
- Hacer backup de funciones por separado
- Documentar orden de ejecución para restore
- Testear restore en ambiente de pruebas
🎯 Conclusión del Conocimiento
Este conocimiento representa las lecciones aprendidas durante la migración real de módulos Dolibarr a PostgreSQL puro. Cada punto ha sido validado en la práctica y representa soluciones a problemas reales encontrados.
🔑 Claves del Éxito
- Entender la estructura de datos existente antes de migrar
- Aprovechar las características avanzadas de PostgreSQL
- Mantener la simplicidad en PHP delegando complejidad a la BD
- Testear exhaustivamente cada migración
- Documentar las decisiones y el conocimiento adquirido