Aprende a generar tu código de persistencia con la herramienta que los propios chicos de Liferay utilizan: Service Builder.
Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. Preparar el proyecto
- 4. Definir el modelo en el service.xml
- 5. Generar el código con Service Builder
- 6. Modificar base de datos ya creada
- 7. Operaciones CRUD con el portlet
- 8. Conclusiones
- 9. Referencias
1. Introducción
Imagina que quieres tener, por ejemplo, una colección de libros y escritores para poder listarla en uno de tus portlets de Liferay Portal. Además, quieres permitir añadir nuevos libros y escritores, modificar los ya existentes e incluso borrarlos. Para ello, por una parte, tienes que guardalos en base de datos; por otra, tienes que tener una serie de clases que te permitan manejar esta persistencia. Escribir el código para montar esto puede ser aburrido, pero no hay de qué preocuparse, pues Liferay nos proporciona una herramienta que nos lo autogenera: Service Builder. Básicamente, nosotros definimos nuestras entidades en un archivo XML (service.xml) y Service Builder se encargará de generar automáticamente las capas de modelo, persistencia y servicio a partir de él.
El uso de Service Builder nos ahorrará gran tiempo de desarrollo y dejará el código separado por capas. Además, nos permitirá definir nuestro propio código de persistencia si lo deseamos.
Los de Liferay aseguran que es una herramienta bastante robusta y fiable, y se basan en que ellos la utilizan para generar su código de persistencia y sus módulos de servicio. No obstante, indican que podemos prescindir de ella para el desarrollo de aplicaciones Liferay, aunque hacen hincapié en que la usemos por el tiempo que nos ahorra.
En este tutorial aprenderemos a utilizarla y veremos cómo solventar los problemas que, desafortunadamente, nos encontramos cuando intentamos seguir la documentación oficial a causa de la falta de completitud de la misma.
Puedes encontrar el código de este tutorial en este repositorio. He ido separando en diferentes commits los pasos que se dan en el tutorial para así facilitar el seguimiento del mismo.
2. Entorno
Este tutorial se ha desarrollado en el siguiente entorno:
- Portátil MacBook Pro (Retina, 15′, mediados 2015), macOS Sierra 10.12.5
- Liferay Community Edition Portal 7.0.2 GA3 (Wilberforce / Build 7002 / August 5, 2016)
- Java 1.8.0_131
- PostgreSQL 9.6.2
- IntelliJ IDEA Ultimate 2017.1.3
3. Preparar el proyecto
Vamos a construir un proyecto Liferay 7 de cero, para lo cual utilizaremos Blade CLI. En este otro tutorial se explica cómo crear el proyecto y cómo integrarlo con tu IDE favorito. Aquí, aunque no nos saltaremos ningún paso, no nos pararemos a volver a describir detalladamente qué hace cada uno; por tanto, recomiendo leerlo antes de empezar con este tutorial.
3.1. Usar Blade CLI para generar el proyecto
Vamos a utilizar ya Blade CLI para crear nuestro proyecto, así que abre la terminal y sigue los siguientes pasos:
- Creamos el proyecto Liferay:
~/workspaces/pruebas $ blade init tutorial-liferay7-servicebuilder
- Añadimos el paquete Liferay Portal + Tomcat:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder $ ./gradlew initBundle
- Arrancamos el servidor local:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder $ blade server start
- Una vez se haya levantado, accedemos a http://localhost:8080/ y vemos el asistente de configuración de Liferay Portal. Como vamos a persistir en base de datos, no usaremos Hypersonic, sino que emplearemos otra base de datos (puedes seguir el tutorial Configurar Liferay 7 con PostgreSQL para ello).
- Tras haber configurado Liferay Portal y accedido a él (nos habrá pedido reiniciarlo al elegir otra base de datos como PostgreSQL), utilizamos la plantilla service-builder de Blade CLI:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder $ blade create -t service-builder -p tutoriales.liferay.servicebuilder.libro libro
Este comando crea el módulo libro y, dentro de él, los módulos libro-api y libro-service.
- Creamos, desde el directorio libro, el módulo libro-web, donde tendremos nuestro portlet MVC:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder/modules/libro $ blade create -t mvc-portlet -p tutoriales.liferay.servicebuilder.libro -c MyMvcPortlet libro-web
- Abrimos el archivo bnd.bnd del módulo libro-web y cambiamos la línea:
Bundle-SymbolicName: tutoriales.liferay.servicebuilder.libro
por:
Bundle-SymbolicName: tutoriales.liferay.servicebuilder.libro.web
3.2. Desplegar los módulos
Llegados a este punto, tenemos tres módulos autogenerados —dos de ellos vacíos— que no hacen gran cosa, pero podemos probar a desplegarlos para ver que hicimos bien todos los pasos de la sección anterior. Para ello, con el servidor iniciado, ejecutamos:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder $ blade deploy
En el archivo de log tutorial-liferay7-servicebuilder/bundles/logs/liferay.yyyy-MM-dd.log, o en terminal si arrancamos el servidor con blade server start —sin la opción -b—, veremos lo siguiente:
hh:mm:ss.milliseconds INFO [Thread-68][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.web_1.0.0 [486] hh:mm:ss.milliseconds INFO [Thread-71][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.service_1.0.0 [487] hh:mm:ss.milliseconds INFO [Thread-74][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.api_1.0.0 [488]
Esto indica que los módulos se han instalado y activado correctamente. Si tuviésemos algún problema, podríamos probar a ejecutar gradle clean && gradle build && gradle deploy desde el directorio de cada módulo o a desinstalar los módulos antes de volver a desplegarlos (ver siguiente sección).
3.3. Ver módulos instalados y activos
Es posible ver en cualquier momento —con el servidor levantado, eso sí— qué módulos hay instalados y cuáles están activos. Para ello, ejecutamos en terminal lo siguiente para iniciar una consola de Apache Felix Gogo desde una sesión local telnet:
$ telnet localhost 11311
y nos aparecerá:
Trying ::1... telnet: connect to address ::1: Connection refused Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. ____________________________ Welcome to Apache Felix Gogo g!
Desde aquí podemos ejecutar el comando lb, que lista todos los módulos instalados, incluidos nuestros tres módulos:
g! lb START LEVEL 20 ID|State |Level|Name 0|Active | 0|OSGi System Bundle (3.10.200.v20150831-0856) 1|Active | 6|Liferay Util Taglib (2.4.1) ...|... | ...|... 485|Active | 10|product-app-theme (1.0.7) 486|Active | 1|libro-web (1.0.0) 487|Active | 1|libro-service (1.0.0) 488|Active | 1|libro-api (1.0.0)
Este comando puede ir acompañado de las opciones -s y -l:
- -s para que liste los módulos mostrando el nombre simbólico en lugar del nombre. En nuestros módulos, el nombre simbólico lo definimos en el archivo bnd.bnd de cada uno de ellos. Es por esto por lo que lo cambiamos antes en el módulo libro-web.
- -l para ver la ubicación de cada módulo. En los que hemos creado, nos mostrará que tenemos los archivos JAR en cada carpeta build de cada módulo.
Es importante que los módulos que hemos creado aparezcan como activos (estado Active) y no como únicamente instalados (estado Installed). Ante un módulo inactivo, podemos emplear el comando start <id_del_módulo> para intentar activarlo.
Para desinstalar un módulo, haremos uninstall <id_del_módulo>. Pero ¡atención!, parece que a veces la desinstalación no se lleva a cabo correctamente —aunque lb no nos muestre ya los módulos— y los JAR se mantienen en la carpeta bundles/osgi/modules (donde bundles es el directorio donde tenemos el paquete Liferay Portal + Tomcat). En ese caso, deberíamos eliminar los JAR a mano.
¡Importante! Para salir de la consola, emplearemos disconnect. Más comandos útiles en la documentación de Liferay sobre Felix Gogo Shell.
Por último, cabe destacar que podemos emplear Blade CLI para realizar las anteriores acciones sin tener que entrar a la consola Gogo. Para ello, ejecutamos desde terminal:
blade sh <comando_de_la_consola_gogo>
Por ejemplo: blade sh lb -s.
4. Definir el modelo en el service.xml
En la introducción, comentamos que sería en el service.xml donde definiríamos nuestro modelo. Si nos fijamos, la plantilla service-builder de Blade CLI nos generó este archivo en el módulo libro-service. Además, lo hizo con datos de ejemplo, con una entidad Foo, así que lo modificamos y lo dejamos así:
<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd"> <service-builder package-path="tutoriales.liferay.servicebuilder.libro"> <namespace>LIBRO</namespace> <entity name="Libro" uuid="true" local-service="true" remote-service="false"> <!-- PK fields --> <column name="libroId" primary="true" type="long"/> <!-- Group instance --> <column name="groupId" type="long"/> <!-- Audit fields --> <column name="companyId" type="long"/> <column name="userId" type="long"/> <column name="userName" type="String"/> <column name="createDate" type="Date"/> <column name="modifiedDate" type="Date"/> <!-- Other fields --> <column name="titulo" type="String"/> <column name="escritor" type="String"/> <column name="publicacion" type="Date"/> <!-- Order --> <order by="asc"> <order-column name="titulo"/> <order-column name="escritor"/> </order> <!-- Finder methods --> <finder name="Titulo" return-type="Collection"> <finder-column name="titulo"/> </finder> </entity> </service-builder>
4.1. Sintaxis del service.xml
La sintaxis del service.xml viene definida por el DTD Service Builder 7.0.0, así que, ante cualquier duda, viene bien echarle un vistazo. No obstante, describimos aquí las etiquetas y atributos empleados:
- namespace. Este nombre se prefijará al nombre de las entidades cuando se creen las tablas en base de datos. Ejemplo, si namespace es LIBRO y una entidad (entity) tiene nombre (name) Escritor, entonces la tabla generada en base de datos será LIBRO_Escritor. No debemos utilizar namespaces ya usados por Liferay como groups, quartz o users; para ver cuáles son, podemos abrir la base de datos generada por Liferay Portal y ver los prefijos del nombre de sus tablas.
- entity. Entidad a partir de la cual se generará la tabla en base de datos y el código relativo al modelo, la persistencia y el servicio. Los atributos que tenemos son:
- name. Nombre de la entidad.
- uuid. Si su valor es true, se generará una columna UUID que se rellenará automáticamente.
- local-service y remote-service. Si su valor es true, se generarán interfaces locales y remotas, respectivamente, para la capa de servicio. Como este código es de prueba y trabajamos en un servidor local, nos evitamos generar las remotas dando valor false a este último atributo.
- column. Atributo de la entidad y, por tanto, columna de la tabla. Su nombre viene dado por name y su tipo por type. Para especificar que forma parte de la clave primaria, incluimos primary=true; podemos, por tanto, indicar que varias columnas forman la clave primaria a través de este atributo. En cuanto al tipo, parece que los únicos disponibles son los tipos primitivos boolean, double, int y long y las clases Blob, Collection, Date y String. Y digo «parece» porque no veo que Liferay indique en ningún sitio cuáles son los tipos soportados. De hecho, el propio archivo DTD solamente especifica lo siguiente: «The type value specifies whether the column is a String, Boolean, or int, etc.». Así que lo que he hecho ha sido ver qué tipos nos deja elegir el asistente de Service Builder de Liferay IDE.
- order. A través de su atributo by —que admite los valores asc y desc—, nos permite obtener las entidades ordenadas cuando las recuperamos de base de datos, en función de las columnas que le indiquemos con order-column. Si proporcionamos más de una columna, entonces ordenará en base a la primera y luego a la segunda. En nuestro caso, que hemos indicado que ordene por titulo y por escritor ascendentemente, si tuviéramos dos libros con el mismo título, entonces nos devolvería primero aquel cuyo escritor fuese alfabéticamente anterior al otro.
- finder. Esta etiqueta hará que Service Builder genere métodos para recuperar, eliminar y contar la entidad en función de las columnas que indiquemos con finder-column. Lo nombraremos (atributo name) con la convención CamelCase, ya que Service Builder tomará esta palabra para dar nombre a los métodos que generará. Con return-type indicamos si queremos que devuelva una colección de entidades (Collection) o una única entidad (Libro, por ejemplo) si buscásemos por clave única.
4.2. Modelo definido en el service.xml
Nuestro modelo va a consistir, de momento, en una única entidad Libro. Los libros tendrán un título, el nombre de su escritor y una fecha de publicación. Se podrán buscar por título y se devolverán ordenados por título y nombre de escritor. Además, tendrán una clave primaria sintética: libroId. ¿Y el resto de campos? Si nos fijamos, los campos bajo los comentarios «Group instance» y «Audit fields» se generaron automáticamente cuando ejecutamos Service Builder. Unos sirven para guardar los identificadores del sitio y de la instancia del portal y así poder soportar multitenencia: cada instancia del portal y cada sitio dentro de cada instancia pueden tener conjuntos de datos independientes. Otros sirven para poder registrar quién creó las entidades y cuándo. ¿Qué guarda cada columna?
- groupId. Identificador del sitio.
- companyId. Identificador de la instancia del portal.
- userId y userName. Identificador y nombre del usuario que posee la instancia de la entidad.
- createDate y modifiedDate. Fecha en la que se creó la instancia de la entidad y fecha en la que se modificó por última vez.
No es obligatorio tener estos campos, pero sí conveniente por los propósitos expuestos. Al menos, deberíamos tener la columna companyId si queremos crear relaciones M-N (ya veremos por qué).
5. Generar el código con Service Builder
Ya tenemos preparado el service.xml, asi que procedemos a generar el código con Service Builder:
~/workspaces/pruebas/tutorial-liferay7-servicebuilder $ ./gradlew buildService
Vemos que ahora tenemos carpetas src en los módulos libro-api y libro-service —antes vacíos— pobladas de paquetes y clases. Antes de pararnos a analizarlas, recordemos que dijimos que Service Builder separaba el código en capas: modelo, persistencia y servicio. La capa de modelo es responsable de definir los objetos que representan nuestras entidades; la de persistencia, de tratar con la base de datos; y la de servicio, de exponer una API para realizar operaciones CRUD. Si nos fijamos, ambos módulos contienen paquetes model, persistence y service.
5.1. Análisis del código generado
En el service.xml, pusimos el atributo local-service=»true» en la entity Libro. Esto supuso que Service Builder generase los siguientes archivos: la interfaz LibroLocalService y las clases LibroLocalServiceBaseImpl, LibroLocalServiceImpl, LibroLocalServiceUtil y LibroLocalServiceWrapper. Con remote-service=»true» (nosotros le dimos valor false) se hubiesen generado las mismas pero con nombre LibroServiceXXX en lugar de LibroLocalServiceXXX. Los servicios locales contienen la lógica de negocio y el acceso a la capa de persistencia, y pueden ser invocados únicamente desde el servidor Liferay en el que son desplegados. Los remotos son accesibles desde cualquier lado, por lo que normalmente tienen código adicional para comprobar permisos.
Vamos ahora a comprender una parte importante del código autogenerado, pero antes entendamos que el módulo libro-api contiene la API y el módulo libro-service la implementa. Nuestro portlet, en el módulo libro-web, empleará la API y ni siquiera tendrá como dependencia al módulo libro-service.
- En cuanto a la persistencia, LibroPersistence es la interfaz que define los métodos CRUD y es implementada por LibroPersistenceImpl. Ambas indican que no deben ser referenciadas directamente, sino que hay que utilizar el envoltorio LibroUtil; sin embargo, éste solamente debe ser usado por la capa de servicio. Esto quiere decir que, para acceder a las operaciones CRUD desde nuestro portlet, no emplearemos LibroUtil; en su lugar, usaremos la clase LibroLocalServiceUtil.
- En cuanto al servicio local, LibroLocalService es la interfaz y es implementada por LibroLocalServiceImpl. Ambas indican que no deben ser referenciadas directamente, sino que hay que utilizar el envoltorio LibroLocalServiceUtil, el cual dijimos que sería utilizado por el portlet como punto de entrada a la capa de servicio.
- En cuanto al modelo, LibroModel es la interfaz base y es implementada por LibroModelImpl, clase que sirve únicamente como contenedor de las propiedades de acceso por defecto generadas por Service Builder. No deberíamos referenciar LibroModel, sino Libro, interfaz que la extiende y que es implementada por LibroImpl.
No debemos modificar ninguna clase generada a excepción de LibroImpl, LibroLocalServiceImpl y, si hubiésemos generado el servicio remoto (remote-service=»true»), LibroServiceImpl. Es en estas tres clases —todas ellas del módulo libro-service— en donde realizaremos cambios para que, al ejecutar Service Builder, se vean plasmados en el resto de clases generadas.
Pero ¿por qué querríamos realizar cambios? Imaginemos, por ejemplo, que desde nuestro portlet quisiésemos añadir un libro. Hemos dicho que debemos utilizar la clase LibroLocalServiceUtil para ello. Ésta tiene un método addLibro(Libro libro), pero nosotros queremos añadir a base de datos un libro a partir de su título, nombre de escritor y fecha de publicación, es decir, nos gustaría tener un método addLibro(String titulo, String escritor, LocalDate publicacion). Pues bien, lo que tenemos que hacer es definir este método en LibroLocalServiceImpl e implementarlo. Pero, claro, esta clase es del paquete libro-service, del cual libro-web —donde está nuestro portlet— ni siquiera depende, y es la implementación de la interfaz LibroLocalService (módulo libro-api). Si el portlet usa la API (el envoltorio LibroLocalServiceUtil), ¿cómo tiene acceso a este nuevo método? Pues básicamente gracias a Service Builder: creamos el nuevo método en LibroLocalServiceImpl y, al ejecutar Service Builder, se añadirá a la interfaz y estará disponible a través de LibroLocalServiceUtil.
Todavía no haremos ningún cambio, no añadiremos métodos propios. Además, antes de actualizar nuestros JAR con el código generado y desplegarlos, cambiaremos la longitud máxima que admite el título de un libro.
5.2. Modificar el límite de caracteres de las columnas String
Las columnas titulo y escritor, que definimos como String, son cadenas de caracteres de longitud máxima 75. Este es el valor por defecto que asigna Service Builder, y lo podemos comprobar si abrimos el archivo libro-service/src/main/resources/META-INF/sql/tables.sql (veremos titulo VARCHAR(75) null).
Si queremos aumentar el máximo número de caracteres, podemos pensar que nos basta con modificar el archivo tables.sql y poner, por ejemplo, titulo VARCHAR(200) null. Esto sería incorrecto, pues los archivos de los directorios spring y sql se sobreescriben cada vez que ejecutamos ./gradlew buildService. Entonces, ¿cómo hacemos para modificar ese valor por defecto? Pues a través del archivo portlet-model-hints.xml del módulo libro-service. Si lo abrimos, veremos la línea:
<field name="titulo" type="String"/>
La sustituimos por:
<field name="titulo" type="String"> <hint name="max-length">200</hint> </field>
Ahora, si ejecutamos ./gradlew buildService desde la raíz del proyecto, veremos que ha sobreescrito tables.sql y que tiene titulo VARCHAR(200) null. En cambio, portlet-model-hints.xml permanece intacto, con el cambio que habíamos realizado.
Tienes más información sobre model hints en la documentación oficial.
Ahora es momento de actualizar los JAR de nuestros módulos y desplegarlos. Si te falla en este punto, puedes probar a ejecutar gradle clean && gradle build && gradle deploy en libro-service, luego en libro-api y, por último, hacer blade deploy de nuevo. En los logs aparecerá la siguiente traza:
hh:mm:ss.milliseconds INFO [Thread-70][ServiceComponentLocalServiceImpl:317] Running LIBRO SQL scripts
Podemos acceder a nuestra base de datos (con pgAdmin, por ejemplo, si empleaste PostgreSQL) y ver que se ha creado la tabla LIBRO_Libro con los campos que definimos, incluido el campo titulo de longitud máxima 200.
6. Modificar base de datos ya creada
Si editásemos el service.xml para modificar nuestra entidad, ejecutásemos ./gradlew buildService y desplegásemos, Service Builder regeneraría el código pero, atención, no aplicaría los cambios en base de datos, como indica la documentación de Liferay en la última nota de esta página. Según ella, lo que tendríamos que hacer es, en resumen, eliminar la tabla y volver a crearla. Para ello, ejecutaríamos las siguientes sentencias:
DROP TABLE IF EXISTS LIBRO_Libro; DELETE FROM servicecomponent WHERE buildnamespace = 'LIBRO'; DELETE FROM release_ WHERE servletcontextname = 'tutoriales.liferay.servicebuilder.libro.service';
Por supuesto, esta medida solamente sirve en desarrollo; en producción, tendremos que conservar los datos que tenemos y aplicar a la base de datos únicamente los cambios que queremos hacer. Como ejemplo, añadiremos el género a nuestros libros. Antes de nada, comprobamos que, en efecto, Service Builder no altera la base de datos:
- Modificamos el archivo service.xml para añadir <column name=»genero» type=»String»/> y el archivo portlet-model-hints.xml para indicar la longitud máxima:
<field name="genero" type="String"> <hint name="max-length">60</hint> </field>
- Ejecutamos ./gradlew buildService para regenerar las clases y que así tengan en cuenta la nueva columna.
- Desplegamos de nuevo con blade deploy. Veremos en los logs que aparece la siguiente traza:
hh:mm:ss.milliseconds INFO [Thread-76][ServiceComponentLocalServiceImpl:326] Upgrading LIBRO database to build number X
Aunque ponga que se ha actualizado la base de datos, si accedemos a ella veremos que no lo ha hecho: nuestra tabla LIBRO_Libro no tiene la nueva columna genero.
Entonces, ¿qué tenemos que hacer para que cambie la base de datos? Pues básicamente crear una nueva versión del módulo libro-service y definir los cambios en una serie de clases:
- UpgradeProcess. Crearemos clases que extiendan esta clase de Liferay y en ellas pondremos el código SQL que refleja los cambios que queremos hacer (en nuestro caso, añadir una nueva columna genero).
- UpgradeStepRegistrator. Tendremos una única clase que extienda ésta. En ella indicaremos qué cambios —es decir, qué clases UpgradeProcess— se aplican entre versiones.
Vamos a ello:
- Añadimos la siguiente dependencia a nuestro archivo build.gradle del módulo libro-service:
compile group: "com.liferay", name: "com.liferay.portal.upgrade", version: "2.0.0"
- Modificamos el archivo bnd.bnd del módulo libro-service para aumentar la versión del módulo: subimos el valor de los atributos Bundle-Version y Liferay-Require-SchemaVersion de 1.0.0 a 1.0.1.
- Seguimos la estructura propuesta por Liferay en su documentación: creamos un paquete upgrade con la clase LibroUpgradeStepRegistrator y con un subpaquete v1_0_1 con una clase UpgradeLibro.
- Añadimos el código correspondiente a nuestra clase LibroUpgradeStepRegistrator:
package tutoriales.liferay.servicebuilder.libro.upgrade; import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator; import org.osgi.service.component.annotations.Component; import tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro; @Component(immediate = true, service = UpgradeStepRegistrator.class) public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator { private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service"; @Override public void register(Registry registry) { registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1", new UpgradeLibro()); } }
Básicamente estamos diciendo que cuando se pase de la versión 1.0.0 a la versión 1.0.1 se aplique el paso UpgradeLibro. Con el atributo immediate = true indicamos que el módulo se active inmediatamente después de ser instalado.
- Creamos nuestra clase UpgradeLibro, en la que ejecutamos el código SQL que necesitamos para añadir la columna genero:
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1; import com.liferay.portal.kernel.upgrade.UpgradeProcess; public class UpgradeLibro extends UpgradeProcess { @Override protected void doUpgrade() throws Exception { runSQL("ALTER TABLE LIBRO_Libro ADD COLUMN genero VARCHAR(60) NULL"); } }
Definimos la columna de la misma manera en la que está definida en el archivo tables.sql (archivo autogenerado a partir del contenido de service.xml y de portlet-model-hints.xml).
- Desplegamos con build.deploy. Veremos en los logs las siguientes trazas:
hh:mm:ss.milliseconds INFO [Thread-91][BundleStartStopLogger:38] STOPPED tutoriales.liferay.servicebuilder.libro.service_1.0.0 [492] hh:mm:ss.milliseconds INFO [Thread-91][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.service_1.0.1 [492] hh:mm:ss.milliseconds INFO [Thread-103][UpgradeProcess:82] Upgrading tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro hh:mm:ss.milliseconds INFO [Thread-103][UpgradeProcess:97] Completed upgrade process tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro in 5ms
Si desplegásemos de nuevo, no aparecerían las dos últimas trazas, pues el módulo ya está en su versión 1.0.1 y el proceso de actualización de base de datos se ejecuta cuando pasamos de la 1.0.0 a la 1.0.1.
- Comprobamos que, ahora sí, tenemos la nueva columna en nuestra tabla.
- Como última medida, listamos los módulos y comprobamos que no tenemos la versión 1.0.0 de libro-service. Si así fuese, la desinstalaríamos.
6.1. Relaciones entre entidades
En el ejemplo dado, únicamente tenemos una entidad, pero lo normal es que nuestro modelo tenga varias y estén relacionadas. Vamos a ver cómo debemos proceder para tener tanto relaciones uno a varios como relaciones varios a varios.
6.1.1. Relaciones uno a varios (1-N)
Cada uno de nuestros libros estará escrito por un único escritor (1), y cada escritor podrá escribir múltiples libros (N).
Empezamos reescribiendo el service.xml. Añadimos la entidad Escritor, eliminamos la columna escritor de Libro y le añadimos la columna escritorId, que será nuestra clave externa, pues hace referencia a la clave primaria de Escritor. Eliminamos del orden la columna escritor y, por último, añadimos un método de búsqueda para poder recoger todos los libros que pertenezcan a cierto escritor.
<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd"> <service-builder package-path="tutoriales.liferay.servicebuilder.libro"> <namespace>LIBRO</namespace> <entity name="Libro" uuid="true" local-service="true" remote-service="false"> <!-- PK fields --> <column name="libroId" primary="true" type="long"/> <!-- Group instance --> <column name="groupId" type="long"/> <!-- Audit fields --> <column name="companyId" type="long"/> <column name="userId" type="long"/> <column name="userName" type="String"/> <column name="createDate" type="Date"/> <column name="modifiedDate" type="Date"/> <!-- Other fields --> <column name="titulo" type="String"/> <column name="publicacion" type="Date"/> <column name="genero" type="String"/> <column name="escritorId" type="long"/> <!-- Order --> <order by="asc"> <order-column name="titulo"/> </order> <!-- Finder methods --> <finder name="Titulo" return-type="Collection"> <finder-column name="titulo"/> </finder> <finder name="EscritorId" return-type="Collection"> <finder-column name="escritorId"/> </finder> </entity> <entity name="Escritor" uuid="true" local-service="true" remote-service="false"> <!-- PK fields --> <column name="escritorId" primary="true" type="long"/> <!-- Group instance --> <column name="groupId" type="long"/> <!-- Audit fields --> <column name="companyId" type="long"/> <column name="userId" type="long"/> <column name="userName" type="String"/> <column name="createDate" type="Date"/> <column name="modifiedDate" type="Date"/> <!-- Other fields --> <column name="nombre" type="String"/> <!-- Order --> <order by="asc"> <order-column name="nombre"/> </order> <!-- Finder methods --> <finder name="Nombre" return-type="Collection"> <finder-column name="nombre"/> </finder> </entity> </service-builder>
Ejecutamos ./gradlew buildService y vemos que se han creado nuevas clases para la entidad Escritor y que se ha modificado el código existente referente a la entidad Libro. Por otra parte, nos fijamos en el detalle de que el archivo portlet-model-hints.xml ha sido modificado para registrar estos cambios pero que no ha sido regenerado totalmente, ya que ha mantenido los límites de longitud que definimos anteriormente.
Como ya sabemos, para que estos cambios se den en nuestra base de datos, no basta con desplegar los módulos. Debemos aumentar la versión en el bnd.bnd de libro-service a la 1.1.0, crear el subpaquete upgrade/v1_1_0, en él dos nuevas clases AddEscritor y UpgradeLibro y registrar los cambios en LibroUpgradeStepRegistrator:
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0; import com.liferay.portal.kernel.upgrade.UpgradeProcess; public class AddEscritor extends UpgradeProcess { @Override protected void doUpgrade() throws Exception { runSQL("CREATE TABLE LIBRO_Escritor (" + "uuid_ VARCHAR(75) NULL," + "escritorId LONG NOT NULL PRIMARY KEY," + "groupId LONG," + "companyId LONG," + "userId LONG," + "userName VARCHAR(75) NULL," + "createDate DATE NULL," + "modifiedDate DATE NULL," + "nombre VARCHAR(75) NULL" + ");"); } }
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0; import com.liferay.portal.kernel.upgrade.UpgradeProcess; public class UpgradeLibro extends UpgradeProcess { @Override protected void doUpgrade() throws Exception { runSQL("ALTER TABLE LIBRO_Libro DROP COLUMN escritor"); runSQL("ALTER TABLE LIBRO_Libro ADD COLUMN escritorId LONG"); } }
package tutoriales.liferay.servicebuilder.libro.upgrade; import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator; import org.osgi.service.component.annotations.Component; import tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.AddEscritor; @Component(immediate = true, service = UpgradeStepRegistrator.class) public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator { private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service"; @Override public void register(Registry registry) { registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1", new tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro()); registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.1", "1.1.0", new AddEscritor(), new tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.UpgradeLibro()); } }
El método Registry#register acepta una serie de UpgradeStep —interfaz que implementa UpgradeProcess— como último parámetro. Nosotros le enviamos AddEscritor y la versión 1.1.0 de UpgradeLibro. Cuántos UpgradeProcess crees y en qué paquete los coloques depende de cómo te guste a ti organizarlos; en nuestro caso, estamos siguiendo la recomendación de Liferay, como ya dijimos. De esta manera, el código es legible y podemos leerlo así: «cuando pasamos de la versión 1.0.1 a la 1.1.0, añadimos la entidad Escritor y actualizamos Libro».
Ahora ya podemos desplegar los módulos y ver que nuestra base de datos tiene una nueva tabla LIBRO_Escritor y que LIBRO_Libro se ha modificado.
6.1.2. Relaciones varios a varios (M-N)
Ahora vamos a cambiar las reglas y vamos a establecer que un libro pueda ser escrito por múltiples escritores (M) en lugar de por uno solo.
Vimos que en la relación 1-N bastaba con definir en el service.xml la columna <column name=»escritorId» type=»long»/> en la entidad Libro. Pues bien, sabemos que para tener una relación M-N entre Libro y Escritor no podemos tener en Libro la referencia al escritor, pues un libro puede estar escrito por varios escritores, sino que tenemos que tener una tercera tabla que relacione la clave primaria de Libro (libroId) y la de Escritor (escritorId), así que empezamos deshaciéndonos, en el service.xml, de la columna escritorId y del método de búsqueda por id de escritor. Añadimos, en su lugar:
<column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/>
De igual forma, en Escritor pondremos:
<column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/>
Nuestro service.xml quedaría así:
<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd"> <service-builder package-path="tutoriales.liferay.servicebuilder.libro"> <namespace>LIBRO</namespace> <entity name="Libro" uuid="true" local-service="true" remote-service="false"> <!-- PK fields --> <column name="libroId" primary="true" type="long"/> <!-- Group instance --> <column name="groupId" type="long"/> <!-- Audit fields --> <column name="companyId" type="long"/> <column name="userId" type="long"/> <column name="userName" type="String"/> <column name="createDate" type="Date"/> <column name="modifiedDate" type="Date"/> <!-- Other fields --> <column name="titulo" type="String"/> <column name="publicacion" type="Date"/> <column name="genero" type="String"/> <column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/> <!-- Order --> <order by="asc"> <order-column name="titulo"/> </order> <!-- Finder methods --> <finder name="Titulo" return-type="Collection"> <finder-column name="titulo"/> </finder> </entity> <entity name="Escritor" uuid="true" local-service="true" remote-service="false"> <!-- PK fields --> <column name="escritorId" primary="true" type="long"/> <!-- Group instance --> <column name="groupId" type="long"/> <!-- Audit fields --> <column name="companyId" type="long"/> <column name="userId" type="long"/> <column name="userName" type="String"/> <column name="createDate" type="Date"/> <column name="modifiedDate" type="Date"/> <column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/> <!-- Other fields --> <column name="nombre" type="String"/> <!-- Order --> <order by="asc"> <order-column name="nombre"/> </order> <!-- Finder methods --> <finder name="Nombre" return-type="Collection"> <finder-column name="nombre"/> </finder> </entity> </service-builder>
Esto no quiere decir que vayamos a tener estas dos columnas en nuestras tablas de base de datos, sino que lo que estamos haciendo es indicar a Service Builder que se trata de una relación M-N.
Ejecutamos Service Builder con ./gradlew buildService y vemos que, aunque ha generado código, nos da un error: java.lang.IllegalArgumentException: No entity column exist with column database name escritorId for entity Libro. Esto se debe a que el archivo indexes.sql tiene la línea create index IX_B94CE263 on LIBRO_Libro (escritorId); incluida. Cuando eliminemos un método de búsqueda (finder) del service.xml y nos dé este error, simplemente borramos el archivo indexes.sql. No pasa nada por ello, pues se generará de nuevo a partir de nuestro service.xml, así que lo borramos y ejecutamos ./gradlew buildService otra vez.
De nuevo, aumentamos el bnd.bnd a la versión 1.2.0 y creamos las clases para llevar a cabo el proceso de actualización:
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0; import com.liferay.portal.kernel.upgrade.UpgradeProcess; public class UpgradeLibro extends UpgradeProcess { @Override protected void doUpgrade() throws Exception { runSQL("ALTER TABLE LIBRO_Libro DROP COLUMN escritorId"); } }
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0; import com.liferay.portal.kernel.upgrade.UpgradeProcess; public class AddLibrosEscritores extends UpgradeProcess { @Override protected void doUpgrade() throws Exception { runSQL("CREATE TABLE LIBRO_Libros_Escritores (" + "companyId LONG NOT NULL," + "escritorId LONG NOT NULL," + "libroId LONG NOT NULL," + "PRIMARY KEY (escritorId, libroId)" + ");"); } }
package tutoriales.liferay.servicebuilder.libro.upgrade; import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator; import org.osgi.service.component.annotations.Component; import tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.AddEscritor; import tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0.AddLibrosEscritores; @Component(immediate = true, service = UpgradeStepRegistrator.class) public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator { private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service"; @Override public void register(Registry registry) { registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1", new tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro()); registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.1", "1.1.0", new AddEscritor(), new tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.UpgradeLibro()); registry.register(BUNDLE_SYMBOLIC_NAME, "1.1.0", "1.2.0", new tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0.UpgradeLibro(), new AddLibrosEscritores()); } }
Desplegamos y comprobamos que en nuestra base de datos existe una nueva tabla: LIBRO_Libros_Escritores. Esta tabla tiene el id del escritor y el id del libro, y ambos forman la clave primaria; sin embargo, no son claves externas, no hacen referencia a las columnas escritorId ni a libroId de las tablas Escritor y Libro, respectivamente.
La tabla LIBRO_Libros_Escritores tiene, además, la columna companyId. Ésta fue definida automáticamente por Service Builder cuando generamos el código a partir del service.xml. Por esta razón dijimos en la sección 4.2. Modelo definido en el service.xml que, al menos, necesitábamos tener la columna companyId en nuestra entidad Libro (y, posteriormente, en Escritor).
Como vemos que la tabla tiene, además de los ids de las tablas de la relación, otro campo —companyId—, podríamos pensar que podemos añadir las columnas que quisiésemos a esta tabla; por ejemplo, saber cuántos días dedicó cada escritor a cada libro. Por desgracia, parece que el soporte de Service Builder a las relaciones M-N no va más allá de tener los ids de las tablas que relaciona —además del companyId— y, por tanto, no lo contempla.
7. Operaciones CRUD con el portlet
En este punto, tenemos preparadas la base de datos y las capas autogeneradas por el Service Builder para operar con ella. Ahora nos gustaría tener un portlet que mostrase nuestra colección de libros y escritores existentes y que nos permitiese, además, crear, modificar y eliminar elementos, es decir, realizar las funciones básicas CRUD. Puedes ver cómo hacer esto en el tutorial Operaciones CRUD en Liferay 7 con MVCPortlet y JSP.
8. Conclusiones
Tras usar Service Builder nos damos cuenta de la gran cantidad de código que esta herramienta nos ahorra. Básicamente definimos en un XML nuestro modelo y éste es autogenerado. No obstante, no todo es tan sencillo, pues en cuanto queremos realizar cambios ya tenemos que definir procesos de actualización aparte del XML. Además, es necesario saber cómo lidiar con ciertos errores y cómo realizar ciertas acciones que, o bien se encuentran disgregadas en la documentación oficial, o bien no aparecen siquiera.
9. Referencias
- Service Builder | Liferay Developer Network
- Felix Gogo Shell | Liferay Developer Network
- DTD Service Builder 7.0.0
- Data Upgrades | Liferay Developer Network
[…] Aprende a realizar operaciones CRUD en Liferay 7 accediendo desde un MVCPortlet a la capa de servicio generada por Service Builder y creando la vista con JSP. […]
Me gustaria tener el portlet para hacerle pruebas en 7.2 , soy nuevo en Liferay te agradeceria mucho si me lo compartieras por favor , Saludos !!!!
Hola, Carlos. Como se indica en la introducción, todo el código producto de seguir este tutorial se encuentra en el siguiente repositorio: https://github.com/JavierSA/tutorial-liferay7-servicebuilder. Un saludo.
he tratado de seguir este tutorial un millon de veces y jamas me ha funcionado :/