Often we need to display data in a legacy schema. The usual way of doing this in any other application is to set up a new datasource, optionally set up an ORM, and display the data yourself. However, in Liferay there is a way to simplify this step considerably by using Service Builder.
"Service Builder?" you say, "I thought that was only for creating entities in the Liferay DB?"
Well, yes, it is - but by using stock functionality we can use Service Builder's powerful code generation tools to create our own service and persistence layer. Building on the method introduced here, we can modify the service generation and Spring config to get SB to talk to our existing DB and even use Hibernate Mappings!
Lets use an example.
Say we have a simple schema:
CREATE TABLE book(book_id INT NOT NULL PRIMARY KEY, book_name VARCHAR(50)); CREATE TABLE book_inventory(book_id INT NOT NULL PRIMARY KEY, book_quantity INT);
where the book_id columns are related.
The first step is to create a properly set-up service.xml to map to these tables:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_0_0.dtd">
<service-builder package-path="com.liferay.training">
<author>Sten Martinez</author>
<namespace>BookInventory</namespace>
<entity name="Book" table="book" local-service="true"
data-source="trainingDataSource" session-factory="trainingSessionFactory" tx-manager="trainingTransactionManager">
<column name="book_id" type="long" primary="true"/>
<column name="book_name" type="String"/>
</entity>
<entity name="BookInventory" table="book_inventory" local-service="true"
data-source="trainingDataSource" session-factory="trainingSessionFactory" tx-manager="trainingTransactionManager">
<column name="bookId" db-name="book_id" type="long" primary="true"/>
<column name="bookQuantity" db-name="book_quantity" type="int"/>
</entity>
</service-builder>
By using the table and db-name parameters we can force service builder to map to the right names. more importantly, we have specified our own persistence and transaction manager bean names.
After running build-service you may notice that the beans we specified arent created; that's cool, we get to do that.
If you look in the service.properties file, there is a reference to a spring config file called ext-spring,xml, which doesnt exist in our META-INF directory. This is a useful extension point for providing our own overrides and bean defs.
ext-spring:
<!-- Overrides -->
<bean id="transactionAdvice" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="trainingTransactionManager" />
<property name="transactionAttributeSource">
<bean class="org.springframework.transaction.annotation.AnnotationTransactionAttributeSource">
<constructor-arg>
<bean class="com.liferay.portal.spring.annotation.PortalTransactionAnnotationParser" />
</constructor-arg>
</bean>
</property>
</bean>
<bean id="basePersistence" abstract="true">
<property name="dataSource" ref="trainingDataSource" />
<property name="sessionFactory" ref="trainingSessionFactory" />
</bean>
<!-- Custom Beans -->
<bean id="trainingHibernateSessionFactory" class="com.liferay.portal.spring.hibernate.PortalHibernateConfiguration" lazy-init="true">
<property name="dataSource">
<ref bean="trainingDataSource" />
</property>
<property name="mappingResources">
<list>
<value>META-INF/portlet-hbm.xml</value>
</list>
</property>
</bean>
<bean id="trainingSessionFactory" class="com.liferay.portal.dao.orm.hibernate.SessionFactoryImpl" lazy-init="true">
<property name="sessionFactoryImplementor">
<ref bean="trainingHibernateSessionFactory" />
</property>
</bean>
<bean id="trainingTransactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager" lazy-init="true">
<property name="dataSource">
<ref bean="trainingDataSource" />
</property>
<property name="sessionFactory">
<ref bean="trainingHibernateSessionFactory" />
</property>
</bean>
<bean id="trainingDataSourceTarget" class="com.liferay.portal.spring.jndi.JndiObjectFactoryBean" lazy-init="true">
<property name="jndiName">
<value>jdbc/legacyDB</value>
</property>
</bean>
<bean id="trainingDataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy" lazy-init="true">
<property name="targetDataSource">
<ref bean="trainingDataSourceTarget" />
</property>
</bean>
now all we have to do to display this is a simple jsp with a search container:
<liferay-ui:search-container emptyResultsMessage="empty-results-message">
<liferay-ui:search-container-results
results="<%= BookLocalServiceUtil.getBooks(searchContainer.getStart(), searchContainer.getEnd()) %>"
total="<%= BookLocalServiceUtil.getBooksCount() %>"
/>
<liferay-ui:search-container-row
className="com.liferay.test.model.Book"
modelVar="aBook"
>
<liferay-ui:search-container-column-text property="bookId" />
<liferay-ui:search-container-column-text property="bookName" />
</liferay-ui:search-container-row>
<liferay-ui:search-iterator />
</liferay-ui:search-container>
and voila! we can see a list of book names. However, this is still only mapping half of the schema. You might have noticed that the portlet-hbm.xml is actually specified in the ext-spring config. This allows us to change it, and map the other (subordinate) table.
we first need to create a new file, portlet-hbm-ext.xml, and fill it with the contents of the original. then we add a single line:
<hibernate-mapping default-lazy="false" auto-import="false">
<import class="com.liferay.training.model.Book" />
<import class="com.liferay.training.model.BookInventory" />
<class name="com.liferay.training.model.impl.BookImpl" table="book">
<cache usage="read-write" />
<id name="book_id" type="long">
<generator class="assigned" />
</id>
<property name="book_name" type="com.liferay.portal.dao.orm.hibernate.StringType" />
<one-to-one name="bookInventory" class="com.liferay.training.model.impl.BookInventoryImpl"/>
</class>
<class name="com.liferay.training.model.impl.BookInventoryImpl" table="book_inventory">
<cache usage="read-write" />
<id name="bookId" column="book_id" type="long">
<generator class="assigned" />
</id>
<property name="bookQuantity" column="book_quantity" type="com.liferay.portal.dao.orm.hibernate.IntegerType" />
</class>
</hibernate-mapping>
This Hibernate mapping will, by default, join the other table to our own. We still have some work to do to get this into the BookImpl model object.
Since BookImpl is the actual persistent class, we need to modify it to put a field in for the display layer to get access to the BookQuantity property, and we also need a property for Hibernate to populate with the related BookInventoryImpl class.
So lets add some properties to BookImpl:
public int getBookQuantity() {
return _bookInventory.getBookQuantity();
}
public void setBookQuantity(int bookQuantity) {
_bookInventory.setBookQuantity(bookQuantity);
}
public BookInventory getBookInventory() {
return _bookInventory;
}
public void setBookInventory(BookInventory bookInventory) {
_bookInventory = bookInventory;
}
private BookInventory _bookInventory;
After re-building the service, our methods should be accessible to the display layer, so if we add
<liferay-ui:search-container-column-text property="bookQuantity"/>
to our search container, it will display the quantity.
Also note that BookQuantity is technically a persistent field via the BookInventory property. We can get and set using the Book model class and never touch the BookInventory service methods.


