Optimistic locking in Liferay

Goal

Handle concurrent access to a domain model in Liferay

Description

In the last two weeks, I have been working in a project using Liferay. This project consists on a Web application that someone said it had to be installed inside a portal server, particularly in Liferay!

The domain model is being implemented using Liferay’s service builder and, although the service builder uses Hibernate underneath to support persistence, Liferay’s service.xml (actually, its DTD) file does not support the specification of the version that Hibernate already supports.

IMO, this recipe is not the final and most appropriate solution for a Liferay application to support optimistic locking in its domain model because the solution would be to add support directly to the DTD (http://www.liferay.com/dtd/liferay-service-builder_6_1_0.dtd) that defines what the service.xml file should be like and implementing the template that generates the hbm and orm files respectively, supporting something similar to the following in the service.xml file:

<entity name="MyEntity" local-service="true" remote-service="false" versionable="true">
  ...
</entity>

How to

The first thing we did was to change the pom.xml of the services-portlet-service module (we are using a Maven multi module project, created initially from the archetype com.liferay.maven.archetypes:liferay-servicebuilder-archetype, using the version for Liferay 6.1.2, which creates two modules inside the base myproject module: myproject-services-portlet and myproject-services-portlet-service). That change was to make Liferay’s maven plugin use a specific set of System properties that override the templates Liferay uses to generate the ORM mapping files:

<?xml version="1.0"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    ... project coordinates
    <build>
        <plugins>
            <plugin>
                <groupId>com.liferay.maven.plugins</groupId>
                <artifactId>liferay-maven-plugin</artifactId>
                <version>${liferay.maven.plugin.version}</version>
                <configuration>
                    <webappBaseDir>${basedir}/../myproject-services-portlet</webappBaseDir>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${javac.source.level}</source>
                    <target>${javac.target.level}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    ... dependencies
    <profiles>
        <profile>
            <!-- Generation of Liferay ServiceBuilder's persistence classes.
                 Usage:
                  $ mvn clean install -Pliferay-service-builder 
            -->
            <id>liferay-service-builder</id>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-resources-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>process-resources</id>
                                <phase>generate-sources</phase>
                                <goals>
                                    <goal>resources</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                       <!-- Override default Liferay's template for service.properties file -->
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>properties-maven-plugin</artifactId>
                        <version>1.0-alpha-2</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>set-system-properties</goal>
                                </goals>
                                <configuration>
                                    <properties>
                                        <property>
                                            <name>service.tpl.orm_xml</name>
                                            <value>servicebuilder/custom/orm_xml.ftl</value>
                                        </property>
                                        <property>
                                            <name>service.tpl.hbm_xml</name>
                                            <value>servicebuilder/custom/hbm_xml.ftl</value>
                                        </property>
                                    </properties>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>com.liferay.maven.plugins</groupId>
                        <artifactId>liferay-maven-plugin</artifactId>
                        <version>${liferay.maven.plugin.version}</version>
                        <configuration>
                            <webappBaseDir>${basedir}/../myproject-services-portlet</webappBaseDir>
                        </configuration>
                        <executions>
                            <execution>
                                <id>generate</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>build-service</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>3.1</version>
                        <configuration>
                            <source>1.6</source>
                            <target>1.6</target>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Next, we copies the templates that generate the hbm and orm files from Liferay’s default implementation and made the appropriate changes (notice that we assumed in the implementation that any entity having a property named version would want that property to be represented as an Hibernate/JPA version and not as a regular property.

First, the orm_xml.ftl freemarker template. Notice that we iterate twice through the columns because we need to make sure that the version tag comes before between the id and basic tags:

<#list entities as entity>
    <#if entity.hasColumns()>
        <mapped-superclass>
            <#if entity.hasCompoundPK()>
                <id-class />
            </#if>

            <attributes>
                <#if entity.hasCompoundPK()>
                    <#assign pkList = entity.getPKList()>

                    <#list pkList as column>
                        <id name="${column.name}"

                        <#if column.name != column.DBName || column.type == "Date">
                            >
                            <#if column.name != column.DBName>
                                <column name="${column.DBName?upper_case}" />
                            </#if>
                            <#if column.type == "Date">
                                <temporal>TIMESTAMP</temporal>
                            </#if>
                            </id>
                        <#else>
                            />
                        </#if>
                    </#list>
                <#else>
                    <#assign column = entity.getPKList()?first>

                    <id name="${column.name}"

                    <#if column.name != column.DBName || column.type == "Date">
                        >
                        <#if column.name != column.DBName>
                            <column name="${column.DBName?upper_case}" />
                        </#if>
                        <#if column.type == "Date">
                            <temporal>TIMESTAMP</temporal>
                        </#if>

                        </id>
                    <#else>
                        />
                    </#if>
                </#if>

                <#list entity.columnList as column>
                    <#if column.name == "version">
                        <version name="${column.name}" />
                    </#if>
                </#list>

                <#list entity.columnList as column>
                    <#if column.name != "version">
                        <#if column.EJBName??>
                            <#assign ejbName = true>
                        <#else>
                            <#assign ejbName = false>
                        </#if>

                        <#if !column.isPrimary() && !column.isCollection() && !ejbName>
                            <basic name="${column.name}"

                            <#if column.name != column.DBName || column.type == "Date">
                                >
                                <#if column.name != column.DBName>
                                    <column name="${column.DBName?upper_case}" />
                                </#if>
                                <#if column.type == "Date">
                                    <temporal>TIMESTAMP</temporal>
                                </#if>

                                </basic>
                            <#else>
                                />
                            </#if>
                        </#if>
                    </#if>
                </#list>

                <#list entity.parentTransients as transient>
                    <transient name="${transient}" />
                </#list>
            </attributes>
        </mapped-superclass>
    </#if>
</#list>

<#list entities as entity>
    <#if entity.hasColumns()>
        <entity name="${entity.name}">
            <table name="${entity.table}" />

            <attributes>
                <#list entity.transients as transient>
                    <transient name="${transient}" />
                </#list>
            </attributes>
        </entity>
    </#if>
</#list>

And last, the hbm_xml.ftl freemarker template. Notice again that we iterate twice through the columns because we need to make sure that the version tag comes before between the id and basic tags:

 <#list entities as entity>
    <import />
</#list>

<#list entities as entity>
    <#if entity.hasColumns()>
        <class name="${packagePath}.model.impl.${entity.name}Impl" table="${entity.table}">
            <#if entity.isCacheEnabled()>
                <cache usage="read-write" />
            </#if>

            <#if entity.hasCompoundPK()>
                <composite-id name="primaryKey">
                    <#assign pkList = entity.getPKList()>

                    <#list pkList as column>
                        <key-property name="${column.name}"

                        <#if column.name != column.DBName>
                            column="${column.DBName}"
                        </#if>

                        <#if column.isPrimitiveType() || column.type == "String">
                            type="com.liferay.portal.dao.orm.hibernate.${serviceBuilder.getPrimitiveObj("${column.type}")}Type"
                        </#if>

                        <#if column.type == "Date">
                            type="org.hibernate.type.TimestampType"
                        </#if>

                        <#if serviceBuilder.isHBMCamelCasePropertyAccessor(column.name)>
                            access="com.liferay.portal.dao.orm.hibernate.CamelCasePropertyAccessor"
                        </#if>

                        />
                    </#list>
                </composite-id>
            <#else>
                <#assign column = entity.getPKList()?first>

                <id name="${column.name}"
                    <#if column.name != column.DBName>
                        column="${column.DBName}"
                    </#if>

                    type="<#if !entity.hasPrimitivePK()>java.lang.</#if>${column.type}"

                    <#if serviceBuilder.isHBMCamelCasePropertyAccessor(column.name)>
                        access="com.liferay.portal.dao.orm.hibernate.CamelCasePropertyAccessor"
                    </#if>

                    >

                    <#if column.idType??>
                        <#assign class = serviceBuilder.getGeneratorClass("${column.idType}")>

                        <#if class == "class">
                            <#assign class = column.idParam>
                        </#if>
                    <#else>
                        <#assign class = "assigned">
                    </#if>

                    <generator

                    <#if class == "sequence">
                            ><param name="sequence">${column.idParam}</param>
                        </generator>
                    <#else>
                        />
                    </#if>
                </id>
            </#if>

            <!-- specific: if the column name is version, then we should generate the markup to version -->
            <#list entity.columnList as column>

                <#if column.name == "version">
                    <version name="${column.name}"

                    <#if serviceBuilder.isHBMCamelCasePropertyAccessor(column.name)>
                        access="com.liferay.portal.dao.orm.hibernate.CamelCasePropertyAccessor"
                    </#if>
                    
                    type="org.hibernate.type.LongType"
    
                    <#if column.name != column.DBName>
                        column="${column.DBName}"
                    </#if>
                    />

                </#if>
            </#list>

            <#list entity.columnList as column>
                <#if column.EJBName??>
                    <#assign ejbName = true>
                <#else>
                    <#assign ejbName = false>
                </#if>

                <#if column.name != "version">
                    <#if !column.isPrimary() && !column.isCollection() && !ejbName && ((column.type != "Blob") || ((column.type == "Blob") && !column.lazy))>
                        <property name="${column.name}"

                        <#if serviceBuilder.isHBMCamelCasePropertyAccessor(column.name)>
                            access="com.liferay.portal.dao.orm.hibernate.CamelCasePropertyAccessor"
                        </#if>

                        <#if column.isPrimitiveType() || column.type == "String">
                            type="com.liferay.portal.dao.orm.hibernate.${serviceBuilder.getPrimitiveObj("${column.type}")}Type"
                        <#else>
                            <#if column.type == "Date">
                                type="org.hibernate.type.TimestampType"
                            <#else>
                                type="org.hibernate.type.${column.type}Type"
                            </#if>
                        </#if>

                        <#if column.name != column.DBName>
                            column="${column.DBName}"
                        </#if>
                        />
                    </#if>
                </#if>

                <#if (column.type == "Blob") && column.lazy>
                    <one-to-one name="${column.name}BlobModel" access="com.liferay.portal.dao.orm.hibernate.PrivatePropertyAccessor" cascade="save-update" outer-join="false" constrained="true" />
                </#if>
            </#list>
        </class>

        <#list entity.blobList as blobColumn>
            <#if blobColumn.lazy>
                <class name="${packagePath}.model.${entity.name}${blobColumn.methodName}BlobModel" table="${entity.table}" lazy="true">
                    <#assign column = entity.getPKList()?first>

                    <id name="${column.name}" column="${column.name}">
                        <generator>
                            <param name="property">${packagePath}.model.impl.${entity.name}Impl</param>
                        </generator>
                    </id>
                    <property column="${blobColumn.DBName}" name="${blobColumn.name}Blob" type="blob" />
                </class>
            </#if>
        </#list>
    </#if>
</#list>

Finally, we have to define our versioned entitites:

 <entity name="MyVersionedEntity" local-service="true" remote-service="false">
   ... pk
   <column name="version" type="long" />
   ... other definitions
</entity>

Explanations

The implementation of this recipe that, as I said before, is not a final solution but could be seen as a clever “hack” for a specific problem that one faces today while developing applications for Liferay platform, consists on:

  • Specify the system properties to override the path for the templates that generate the orm and hbm files
  • Implement the templates that will include the version tag to the orm and hbm files
  • Add the version column (in this recipe, the name version is application specific and it’s the way we have to detect that a column is not a basic property but a version, with a specific semantic in Hibernate!) to the entities definition in the service.xml file

One Reply to “”

Leave a comment