← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·11 min de lectura

Objects en Liferay DXP: modelado de datos sin escribir código

LiferayAPI

La evolución desde Service Builder

Durante más de una década, Service Builder fue la forma estándar de definir entidades de datos en Liferay. Escribías un archivo service.xml con la definición de tu modelo, ejecutabas el generador de código, y obtenías las clases Java para persistencia, servicios locales, servicios remotos y finders. Era potente, pero tenía un costo alto:

  • Requería conocimiento de Java, OSGi y el ciclo de vida de módulos de Liferay
  • El archivo service.xml tenía una curva de aprendizaje significativa
  • Cada cambio en el modelo requería regenerar código, recompilar y redesplegar
  • Solo desarrolladores podían crear o modificar entidades

En proyectos empresariales, esto creaba un cuello de botella constante. Un analista de negocio identificaba la necesidad de una nueva entidad -- digamos, "Sucursales" con campos como nombre, dirección, región y horario -- y tenía que esperar a que un desarrollador Java la implementara en Service Builder. Para algo que conceptualmente era una tabla con columnas, el proceso podía tomar días.

Liferay Objects, introducido en DXP 7.4, resuelve exactamente este problema. Permite crear entidades de datos completas desde la interfaz de administración, sin escribir una sola línea de código. Y lo más importante: cada entidad creada con Objects expone automáticamente una API REST completa.

Crear un Object Definition paso a paso

Vamos a recorrer el proceso completo usando un ejemplo real: una entidad "Sucursales" para gestionar las oficinas de una empresa, con relación a "Regiones".

Definición básica

Desde el Panel de Control de Liferay, en la sección Object Definitions, creas un nuevo objeto con:

  • Label: Sucursales (lo que ven los usuarios)
  • Plural Label: Sucursales
  • Object Name: Sucursal (nombre interno, usado en la API)

Liferay genera automáticamente el endpoint REST basándose en el nombre: /o/c/sucursals (pluraliza en inglés por convención interna, algo que confunde al principio pero es consistente).

Campos disponibles

Estos son los tipos de campo que más uso en el día a día (la lista completa incluye además Aggregation, Auto-Increment, Encrypted, Formula, Multiselect Picklist, Phone Number y Precision Decimal):

  • Text: cadenas de texto de hasta 280 caracteres, con longitud máxima configurable por debajo de ese tope (y opción de exigir valores únicos)
  • Long Text: texto largo con soporte para multilínea
  • Integer: números enteros
  • Long Integer: enteros grandes, de hasta 16 dígitos según la documentación (frente a los 9 dígitos de Integer)
  • Decimal: números con punto decimal, con un límite de 16 dígitos
  • Precision Decimal: el tipo de alta precisión (el que antes se llamaba BigDecimal), que no redondea -- es el que quieres para importes y cálculos financieros
  • Boolean: verdadero/falso
  • Date: fecha sin hora
  • Date and Time: fecha con hora y timezone
  • Picklist: lista de opciones predefinidas (dropdown)
  • Attachment: archivos adjuntos almacenados en el Document Library
  • Rich Text: HTML con editor WYSIWYG
  • Relationship: referencia a otro Object

Para nuestra entidad Sucursales, definimos:

| Campo | Tipo | Requerido | Notas | |-------|------|-----------|-------| | nombre | Text (200) | Sí | Nombre de la sucursal | | direccion | Long Text | Sí | Dirección completa | | telefono | Text (20) | No | Teléfono de contacto | | activa | Boolean | Sí | Default: true | | fechaApertura | Date | No | Cuándo abrió | | capacidad | Integer | No | Capacidad máxima | | tipo | Picklist | Sí | Valores: Principal, Secundaria, Express |

Validaciones

Cada campo permite definir reglas de validación sin código. Para el campo nombre, por ejemplo, puedes establecer:

  • Longitud mínima: 3 caracteres
  • Longitud máxima: 200 caracteres
  • Expresión regular: patrón personalizado si necesitas restricciones adicionales

Para campos de tipo Integer, puedes definir rango mínimo y máximo. Para Picklists, las opciones válidas se definen en la sección de Picklists del Panel de Control, lo que permite reutilizar la misma lista en múltiples Objects.

La API REST automática

Una vez publicado el Object, Liferay genera automáticamente endpoints REST con CRUD completo. No necesitas configurar nada adicional. Los endpoints siguen el patrón:

GET    /o/c/sucursals          → Listar (con paginacion)
GET    /o/c/sucursals/{id}     → Obtener por ID
POST   /o/c/sucursals          → Crear
PUT    /o/c/sucursals/{id}     → Actualizar (completo)
PATCH  /o/c/sucursals/{id}     → Actualizar (parcial)
DELETE /o/c/sucursals/{id}     → Eliminar

Paginación

La API usa paginación por defecto. El formato de respuesta incluye metadatos útiles:

{
  "items": [...],
  "page": 1,
  "pageSize": 20,
  "totalCount": 47,
  "lastPage": 3,
  "actions": {}
}

Puedes controlar la paginación con parámetros: ?page=2&pageSize=50.

Filtrado con OData

Esta es una de las funcionalidades más potentes de Objects. Los endpoints soportan filtrado usando sintaxis OData, lo que permite consultas complejas sin necesidad de endpoints personalizados:

# Sucursales activas
GET /o/c/sucursals?filter=activa eq true

# Sucursales con capacidad mayor a 50
GET /o/c/sucursals?filter=capacidad gt 50

# Sucursales de tipo Principal abiertas despues de 2023
GET /o/c/sucursals?filter=tipo eq 'Principal' and fechaApertura gt 2023-01-01

# Busqueda por texto parcial en nombre
GET /o/c/sucursals?filter=contains(nombre, 'Centro')

# Ordenamiento
GET /o/c/sucursals?sort=nombre:asc

# Combinacion de filtro, orden y paginacion
GET /o/c/sucursals?filter=activa eq true&sort=fechaApertura:desc&page=1&pageSize=10

Los operadores disponibles incluyen eq, ne, gt, ge, lt, le, contains, startswith, and, or, y not. Esto cubre la gran mayoría de casos de uso sin necesidad de lógica personalizada en el backend.

Nested fields y relaciones

Cuando un Object tiene relaciones, puedes solicitar los datos relacionados en una sola petición usando el parámetro nestedFields:

GET /o/c/sucursals?nestedFields=region&nestedFieldsDepth=1

Esto devuelve cada sucursal con su región embebida en la respuesta, evitando el problema de N+1 queries que sería común si tuvieras que hacer una petición adicional por cada sucursal para obtener su región.

Relaciones entre Objects

Objects soporta dos tipos de relación:

One-to-Many

La más común. Por ejemplo, una Región tiene muchas Sucursales. Se configura creando un campo de tipo Relationship en el Object hijo (Sucursal) apuntando al Object padre (Región).

En la API, esto se refleja como:

{
  "id": 42,
  "nombre": "Sucursal Centro",
  "r_region_c_regionId": 5,
  "region": {
    "id": 5,
    "nombre": "Region Metropolitana"
  }
}

El campo r_region_c_regionId sigue la convención de nombres interna de Liferay para relaciones. Es verboso, pero consistente.

Many-to-Many

Soportada por Objects desde Liferay 7.4. Al definir la relación eliges el tipo Many to Many y Liferay añade la tabla de relación por ti, en ambos lados. Por ejemplo, si una Sucursal puede pertenecer a múltiples Programas de Fidelización, defines una relación many-to-many y Liferay gestiona la tabla de unión.

Integración con workflows y permisos

Workflows

Objects se integra con el motor de workflows de Liferay. Puedes asignar un workflow a un Object para que cada nueva entrada pase por un proceso de aprobación. Por ejemplo, una nueva Sucursal podría requerir aprobación de un gerente regional antes de quedar activa.

Esto se configura desde la sección de Workflow del Object Definition, sin código adicional.

Permisos

Cada Object respeta el sistema de permisos de Liferay. Puedes configurar:

  • Qué roles pueden crear, leer, actualizar o eliminar entradas
  • Permisos a nivel de entrada individual (scope por site, por compañía, o por usuario)
  • Permisos de aplicación separados de los de recurso: quién puede acceder al Object desde el Panel de Control no es lo mismo que quién puede ver o modificar sus entradas

La API REST aplica estos permisos automáticamente. Si un usuario no tiene permiso para ver Sucursales, el endpoint devuelve 403, no datos vacíos.

Lo que el modelo de permisos de Objects no cubre es el nivel de campo: no hay una opción para que un rol vea unos campos sí y otros no dentro de la misma entrada. Si necesitas esa granularidad, tienes que resolverla exponiendo vistas distintas o filtrando en la capa que consume la API.

Objects vs Service Builder: cuándo usar cada uno

Esta es la pregunta más común en equipos que migran a Liferay 7.4. La respuesta depende de la complejidad de la lógica de negocio:

Usa Objects cuando:

  • La entidad es principalmente CRUD (crear, leer, actualizar, eliminar)
  • No necesitas lógica de negocio compleja en el backend
  • Quieres que usuarios no técnicos puedan modificar la estructura
  • Necesitas una API REST rápida para prototipar o para Client Extensions
  • Las validaciones se pueden expresar con reglas simples (requerido, longitud, regex)

Usa Service Builder cuando:

  • Necesitas lógica de negocio compleja (cálculos, integraciones con sistemas externos, transformaciones de datos)
  • Requieres consultas SQL personalizadas que OData no puede expresar
  • Necesitas triggers o eventos personalizados complejos que van más allá de los workflows estándar
  • El rendimiento es crítico y necesitas control fino sobre las consultas
  • Necesitas herencia de entidades o patrones de diseño complejos

En la práctica, muchos proyectos usan ambos. Objects para las entidades simples de configuración y catálogo, Service Builder para el dominio core con lógica compleja. Lo importante es no forzar una entidad compleja en Objects ni sobredimensionar una entidad simple con Service Builder.

Limitaciones reales de Objects

Después de usar Objects extensivamente en producción, estas son las limitaciones que he encontrado:

La lógica de negocio está acotada a lo que permiten las Object Actions: si necesitas que al crear una Sucursal se calculen distancias o se sincronice con un sistema externo, Objects no te da un módulo Java donde poner ese código. Lo que sí te da son cinco tipos de acción sobre eventos de entrada: Notification (correo o notificación al usuario), Add an Object Entry, Update an Object Entry, Webhook y Groovy Script. Así que el caso de avisar por email al gerente regional se resuelve nativamente con una Notification, y un script Groovy corre dentro del portal; solo el Webhook delega de verdad la lógica en un servicio externo. La letra pequeña de Groovy: solo está disponible en Liferay PaaS y en instalaciones self-hosted, y desde DXP 2024.Q2/Portal GA120 el scripting viene deshabilitado por defecto, así que hay que habilitarlo a mano.

Rendimiento a escala: aquí conviene deshacer un malentendido frecuente, porque Objects no guarda los datos en una estructura genérica tipo EAV. Al publicar un Object Definition, Liferay crea una tabla dedicada en la que cada campo es una columna real, con las relaciones y campos que existían en el borrador en ese momento. El detalle que sí importa es lo que pasa después: los campos y relaciones que agregas una vez publicado el Object no van a esa tabla inicial, sino a una tabla lateral ([Tabla_Inicial]_x), de modo que las consultas que los tocan implican un join adicional. Para miles de registros esto es irrelevante. Para millones con consultas complejas -- sobre todo si encadenas relaciones anidadas, donde nestedFieldsDepth admite hasta cinco niveles y la propia documentación advierte que el rendimiento empeora con cada nivel -- un esquema de Service Builder afinado a mano, con sus índices y finders propios, sigue teniendo ventaja.

Migraciones de esquema: Modificar un Object publicado tiene restricciones, y el criterio no es el que uno esperaría. De un campo ya publicado solo puedes editar la Label; el resto de valores y ajustes, incluido el tipo, quedan congelados. Y para eliminarlo no cuenta si tiene datos o no, sino en qué tabla vive: los campos que formaban parte de la tabla inicial al publicar no se pueden eliminar, mientras que los que agregaste después sí, porque Liferay los guardó en la tabla lateral [Tabla_Inicial]_x. Puedes seguir agregando campos nuevos sin problema, pero tocar la tabla inicial de forma destructiva sigue implicando recrear el Object.

Nombres de campos en la API: Los nombres generados para relaciones (r_relation_c_objectId) son poco intuitivos. Tus consumidores de API necesitarán documentación clara.

Cómo Client Extensions consumen Objects

La combinación más poderosa en Liferay 7.4 es Objects + Client Extensions. Tu Client Extension (React, Angular, o cualquier framework) consume los datos de Objects a través de la API REST generada automáticamente.

Un ejemplo típico en una Client Extension React:

async function getSucursales(filtroRegion) {
  const filter = filtroRegion
    ? `?filter=r_region_c_regionId eq ${filtroRegion}&sort=nombre:asc`
    : '?sort=nombre:asc'

  const response = await Liferay.Util.fetch(
    `/o/c/sucursals${filter}`,
    {
      headers: { 'Accept': 'application/json' },
    }
  )

  const data = await response.json()
  return data.items
}

Liferay.Util.fetch es un wrapper disponible en el contexto de Liferay que maneja automáticamente la autenticación (agrega el token CSRF y las cookies de sesión). Para llamadas externas, usarías OAuth2.

Objects transforma a Liferay de un CMS que requiere Java para todo, a una plataforma donde un equipo frontend puede crear entidades de datos y consumirlas desde Client Extensions sin depender de un equipo backend. Eso no reemplaza a Service Builder para casos complejos, pero cubre un porcentaje significativo de las necesidades de proyectos empresariales típicos.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.