If you’re going to use Liferay to develop serious, enterprise-grade systems and applications, you will need to learn how to take advantage of the various tools the platform provides, such as Service Builder, Dynamic Queries, and Custom SQLs. I use them in my everyday development and I’ll show you how to do that – step by step.
Service Builder – What is it?
Let’s start with the Service Builder. It plays a crucial role in Liferay, providing an abstraction layer between the application and the database. This allows for the separation of business logic from the data access layer, resulting in easier management and development of the application. Thanks to the Service Builder, it is possible to generate a set of service classes that implement interfaces for each entity in the database. Additionally, model objects representing corresponding records in the database are generated.
When building an application based on Service Builder, it is necessary to configure the appropriate file and directory structure. All files related to Service Builder are located in a dedicated folder named “service.”
The main elements of the structure are:
- service.xml file – it contains data model definitions in XML format, where we specify the entities and their fields that should be stored in the database
- service-impl folder – it contains the generated service classes that implement interfaces for individual entities
- service-api folder – it stores service interfaces used for communication with the application’s business logic layer
How to get started with Service Builder?
The first step is to define the data model, i.e., the entities and fields we want to store in the database. To do this, we create a service.xml file in the service folder. Here’s sample data model definition created for the purposes of this article:
<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.4.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_4_0.dtd"> <service-builder package-path="com.blogpost.book"> <namespace>example</namespace> <entity name="Book" local-service="true" remote-service="true"> <column name="bookId" type="long" primary="true"/> <column name="title" type="String" /> <column name="author" type="String" /> <finder name="FindByAuthor" return-type="Collection"> <finder-column name="author" /> </finder> </entity> </service-builder>
Let’s take a look at each of the tags used in the script above:
- <service-builder> – the main tag that specifies this is a Service Builder file. The package-path attribute defines the package path in which the service classes and model objects will be generated
- <namespace> – it specifies a unique prefix used in the generated names of service classes and model objects
- <entity> – defines the data model, i.e., the entity in the database. The name, local-service, and remote-service attributes indicate the entity’s name, whether the local service should be generated (true or false), and whether the remote service should be generated (true or false)
- <column> – specifies a field for a given entity. The name and type attributes represent the field’s name and its type, respectively
- <finder> – defines a finder, which is a database query that allows filtering data based on selected criteria. The name attribute is the finder’s name, and return-type specifies the type of data to be returned (e.g., Collection, List, Single)
- <finder-column> – specifies the field by which data should be filtered in a particular finder
In this example, I have defined a model called Book with fields bookId, title, and author. Additionally, I have created a finder named FindByAuthor, which allows searching for books by the author’s name. With this definition, Service Builder will automatically generate service classes and model objects for this entity, which I can then use to manage data in the database in my Liferay application.
The next step is to generate the service classes and model objects based on the service.xml file. For this purpose, I can use either tools provided by Liferay or Gradle. Executing the appropriate command will automatically generate service classes in the service-impl folder and model objects in the service-api folder.
After generating the service classes and model objects, I can use them in my application, which provides simple access to CRUD (Create, Read, Update, Delete) operations on data in the database.
Here’s a sample script that demonstrates how to use the service to add a new book to the database:
// Create new book Book book = BookLocalServiceUtil.createBook(CounterLocalServiceUtil.increment(Book.class.getName())); book.setTitle("Example book"); book.setAuthor("John Doe"); // Add book to database book = BookLocalServiceUtil.addBook(book); System.out.println("New book was added ID: " + book.getBookId()); System.out.println("Book: " + book);
This results in displaying the following message:
New book was added ID: 3
Book: {“bookId”: 3, “title”: “Example book”, “author”: “John Doe”}
Need a team with Liferay experience? 🔥 Let us know at hello@pretius.com and we’ll arrange free consultations
Dynamic Queries
Dynamic Queries are another useful Liferay feature. They allow you to generate database queries programmatically. Thanks to this, you can build more flexible queries that adapt to various criteria and conditions. In Liferay, Dynamic Query is accessible through the DynamicQuery class API and allows data manipulation in a dynamic manner.
Here’s an example of using Dynamic Queries to retrieve all books written by the author “John Doe”:
// Create new dynamicQuery DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery(); // Add a condition for the "author" field equal to "John Doe" dynamicQuery.add(RestrictionsFactoryUtil.eq("author", "John Doe")); // Retrieve a list of books that meet the condition List<Book> booksByJohnDoe = BookLocalServiceUtil.dynamicQuery(dynamicQuery); System.out.println(booksByJohnDoe);
Conjunction
Conjunction and Disjunction allow combining conditions in Dynamic Queries, enabling more advanced data filtering.
Conjunction represents the logical AND condition. This means that all specified conditions must be met for a record to be included in the search results. For example, if you have two conditions – A and B – using conjunction, both must be true for a record to be returned in the results.
Disjunction
Disjunction represents the logical OR condition. This means that at least one specified condition must be met for a record to be included in the search results. For example, if you have two conditions – A and B – and one of the conditions is true for a record, it’s enough for it to be returned in the results when using disjunction.
Here’s an example of using disjunction to retrieve books written by the author “John Doe” or books containing the phrase “Harry Potter” in the title:
// Create a new dynamic query object DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery(); // Create a criterion for the "author" field equal to "John Doe" Criterion authorCriterion = RestrictionsFactoryUtil.eq("author", "John Doe"); // Create a criterion for the "title" field containing "Harry Potter" Criterion titleCriterion = RestrictionsFactoryUtil.like("title", "%Harry Potter%"); // Create a Disjunction object and add both criteria to it Disjunction disjunction = RestrictionsFactoryUtil.disjunction(); disjunction.add(authorCriterion); disjunction.add(titleCriterion); // Add the Disjunction as a condition to the dynamic query dynamicQuery.add(disjunction); // Retrieve a list of books that meet the condition List<Book> books = BookLocalServiceUtil.dynamicQuery(dynamicQuery); System.out.println(books);
Projection
In the context of Liferay’s Dynamic Query API, Projection can be applied to specify which entity fields should be included in the query results. This allows fetching only those fields required to meet the application’s needs, avoiding excessive data transfer, and reducing the system’s load.
In Liferay’s Dynamic Query API, you can apply Projection using the setProjection() method on the dynamic query object (DynamicQuery). For example, if you want to retrieve only the title and author fields from the Book entity instead of the entire Book object, you can do it using the following script:
DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery(); ProjectionList projectionList = ProjectionFactoryUtil.projectionList(); projectionList.add(ProjectionFactoryUtil.property("title")); projectionList.add(ProjectionFactoryUtil.property("author")); dynamicQuery.setProjection(projectionList); // Now you can execute the query, which will return only the "title" and "author" fields List<Object[]> results = BookLocalServiceUtil.dynamicQuery(dynamicQuery); // The results are a list of object arrays, where each array contains the "title" and "author" fields for (Object[] result : results) { String title = (String) result[0]; String author = (String) result[1]; System.out.println("Title: " + title + ", Author: " + author); }
The query result will be a list of object arrays, where each array contains the values of the title and author fields corresponding to the Book entity. This allows you to limit the retrieved data to only these two fields, which can speed up the application, especially if the entities have many other fields that are currently unnecessary.
Custom SQL

Custom SQL is another advanced technique to help you level up your Liferay projects. It allows direct writing of SQL queries to the database in applications based on Liferay.
Although Dynamic Queries are powerful tools for data filtering, there are situations when you need more complex operations or access to advanced database functions that aren’t provided by the DynamicQuery interface. In such cases, Custom SQL can be a solution. Custom SQL consists of SQL code snippets that can be directly inserted into your Liferay applications. This allows for the direct execution of complex database queries. However, it is important to be careful when using Custom SQL as it requires more complex management and may pose security risks, such as SQL injection attacks.
Examples of situations where it is worth using Custom SQL:
- Performing more advanced table joins
- Accessing aggregate functions (e.g., SUM, AVG) or database-specific functions
- Optimizing queries to improve performance
How to use Custom SQL in Liferay?
One of the main challenges with Custom SQL is ensuring security. Avoid directly inserting user variables into queries to prevent SQL injection attacks. Liferay offers several mechanisms to execute Custom SQL queries safely – like the SQLQuery and SQLQueryFactory classes in Liferay API, for example, which automatically protect the system against SQL injection attacks through query parameterization.
To make use of this, you need to add the implementation of the following methods to the BookLocalServiceImpl class and then rebuild the module using Gradle:
public List<Book> books() { String sql = "SELECT * FROM example_book WHERE title LIKE ? OR author = ?"; return executeQuery(sql); } private List<Book> executeQuery(String sql) { Session session = null; List<Book> details = null; try { session = bookPersistence.openSession(); SQLQuery query = session.createSQLQuery(sql); query.addEntity("Book", BookImpl.class); query.setString(0, "%Harry%"); query.setString(1, "John Doe"); details = (List<Book>) QueryUtil.list(query, bookPersistence.getDialect(), -1, -1); } catch (UndeclaredThrowableException ex) { Throwable cause = ex.getCause(); cause.printStackTrace(); } finally { bookPersistence.closeSession(session); } return details; }
I’d like to stress once again – you need to manage Custom SQL carefully to avoid potential performance and security issues. I’d advise you to use Dynamic Queries when it is sufficient to achieve your goals, and resort to Custom SQL only when necessary.
Conclusion
While learning the basics of Liferay application development is relatively easy (check out the Liferay tutorial I wrote with Patryk Rutkowski if you want to refresh your memory), mastering the platform is something else. As you can see, the solution offers plenty of powerful tools and features you can use to make your apps more customizable, flexible, and powerful. This article only scratches the surface of what’s possible – you can expect follow-ups in the coming months, with detailed information regarding custom portlets, MVCResource, MVCAction, MVCRender, and much, much more. If you have any questions, contact me at tmajcher@pretius.com – and see you in the next one!
Interested in Liferay-based apps?
Pretius has experience with using the Liferay platform to create excellent enterprise-grade applications. Reach out to us at hello@pretius.com and tell us about your needs. We’ll see what we can do to help!