Custom queries in Liferay (在liferay中使用传统的Query)

本文详细介绍了如何在Liferay 5.2中使用ServiceBuilder进行自定义查询的创建与优化,包括创建查找器、编写查询方法、实现查询、集成至本地服务以及在portlet中应用查询等步骤。文章还提供了两种实现方式,并讨论了如何在XML文件中分离SQL查询,使用常量存储查询ID,以及如何通过`CustomSQLUtil`类处理参数和优化查询逻辑。此外,文章还强调了遵循良好实践的重要性,如避免XML解析问题、在常量中存储查询ID、将查询分布在多个XML文件中以及正确处理参数。最后,文章总结了实现自定义查询的关键步骤和注意事项,旨在帮助开发者更高效地利用Liferay平台的灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文:http://www.liferay.com/en/community/wiki/-/wiki/1071674/Custom+queries+in+Liferay+5.2

Sometimes it is needed to perform joined queries with the Service Builder. It is not possible to do it with dynamic queries - custom queries are necessary. This article explains how to create custom queries since the documentation was pretty sparse and confusing.

Prerequisites 

You should know how to create services with Service Builder. Also, you should know a little about SQL: the basic syntax and a notion on how it is used inside programming language code.

The steps described here were performed over Liferay 5.2 but it still worked with Liferay 6.

Our example 

Suppose you need to create a simple blog portlet. It will use two service entities, representing posts and authors. We use the following service.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 5.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_5_2_0.dtd">
<service-builder package-path="br.com.seatecnologia.portlet.exampleblog">
    <author>Adam Victor Nazareth Brandizzi</author>
    <namespace>ExampleBlog</namespace>
    <entity name="Author" local-service="true">
        <column name="authorId" type="long" primary="true"/>
        <column name="name" type="String"/>
        <column name="posts" type="Collection" entity="Post" mapping-key="authorId"/>
    </entity>
    <entity name="Post" local-service="true">
        <column name="postId" type="long" primary="true"/>
        <column name="title" type="String"/>
        <column name="content" type="String"/>
        <column name="authorId" type="long"/>
    </entity>
</service-builder>

Also, we define a method in PostImpl which retrieves the author of a post based on thePost's author Id:

public Author getAuthor() {
    try {
        return AuthorLocalServiceUtil.getAuthor(getAuthorId());
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
 // end

The problem 

Besides providing forms for inserting and listing all posts and authors, our portlet should provide a form for searching posts based on title, content and author name. The challenge is that, since the author name is not in thePost table, we cannot search on it using finders or dynamic queries. We need to do a joined search, so we need a custom query.

Creating a finder 

To use custom queries, we need a finder class. This class should be created after runningant build-service.

After our services was created, let us create a class called PostFinderImpl in the packagebr.com.seatecnologia.portlet.exampleblog.service.persistence of thedocroot/WEB-INF/src source folder.[1] This class should extend com.liferay.portal.service.persistence.impl.BasePersistenceImp<Post>:

package br.com.seatecnologia.portlet.exampleblog.service.persistence;

import br.com.seatecnologia.portlet.exampleblog.model.Post;
import com.liferay.portal.service.persistence.impl.BasePersistenceImpl;

public class PostFinderImpl extends BasePersistenceImpl<Post> { }

Once we have written the PostFinderImpl class we execute ant build-service again. The interface br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder and the util classbr.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinderUtil will be created at thedocroot/WEB-INF/service source folder. Once they are created, you should make thePostFinderImpl class implement the PostFinder interface:

public class PostFinderImpl extends BasePersistenceImpl<Post> implements PostFinder { }

It is very important to implement the interface. A recurrent error is to forget to declare that theFinderImpl implements theFinder interface. Usually it results in a error message like this:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'br.com.seatecnologia.portlet.exampleblog.service.AuthorLocalService' defined in ServletContext resource
[/WEB-INF/classes/META-INF/portlet-spring.xml]: Initialization of bean failed; nested exception is
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'br.com.seatecnologia.portlet.exampleblog.service.AuthorService' defined in ServletContext resource
[/WEB-INF/classes/META-INF/portlet-spring.xml]: Initialization of bean failed; nested exception is
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'br.com.seatecnologia.portlet.exampleblog.service.PostLocalService' defined in ServletContext resource
[/WEB-INF/classes/META-INF/portlet-spring.xml]: Initialization of bean failed; nested exception is
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'br.com.seatecnologia.portlet.exampleblog.service.PostService': Could not inject BeanReference fields;
nested exception is java.lang.IllegalArgumentException: Can not set
br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder field
br.com.seatecnologia.portlet.exampleblog.service.base.PostServiceBaseImpl.postFinder to $Proxy262

If you find something like this in the log of a faulty portlet it is very likely that you forgot declare the Impl class an implement the interface.

Writing the query method

Now we can create the query method in the PostFinderImpl class:

public List<Post> findByTitleContentAuthor(String title, String content, String author, int begin, int end) {
    return null; // end

We run ant build-service again and voilà new methods are created atPostFinder andPostFinderUtil.

Every time we add a new method at some FinderImpl we should executeant build-service for propagating the method to the interface and util class. However, if we just change a method inFinderImpl but does not change the method signature neither add a new method to the finder, we do not need to execute the ant task.


第一种方法:

SQL写在类里面,

1、写一个基本的SQL

2、根据参数在原SQL后面添加条件。

Implementing the query

The process of creating and executing a query is pretty similar to the SQL execution in various programming languages and frameworks. We will not explain it in detail. Below, we present the implementation:

package br.com.seatecnologia.portlet.exampleblog.service.persistence;

import java.util.ArrayList;
import java.util.List;

import br.com.seatecnologia.portlet.exampleblog.model.Post;
import br.com.seatecnologia.portlet.exampleblog.model.impl.PostImpl;

import com.liferay.portal.SystemException;
import com.liferay.portal.kernel.dao.orm.QueryPos;
import com.liferay.portal.kernel.dao.orm.QueryUtil;
import com.liferay.portal.kernel.dao.orm.SQLQuery;
import com.liferay.portal.kernel.dao.orm.Session;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.service.persistence.impl.BasePersistenceImpl;

public class PostFinderImpl extends BasePersistenceImpl<Post> {

    public List<Post> findByTitleContentAuthor(String title, String content, String author, int begin, int end) throws SystemException {
        /// This stuff is basic set up
        Session session = null;
        try {
            session = openSession();
            // Here ends the basic set up; 

            //now we build the query. Note that we use the name of the tables from the database!
            String base = "SELECT {ExampleBlog_Post.*} FROM ExampleBlog_Post " +
                    "JOIN ExampleBlog_Author " +
                    "ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId ";
            StringBuffer sqlBuilder = new StringBuffer(base);
            List<String> criteria = new ArrayList<String>();
            if (Validator.isNotNull(title)) {
                criteria.add("ExampleBlog_Post.title like ?");
            }
            if (Validator.isNotNull(content)) {
                criteria.add("ExampleBlog_Post.content like ?");
            }
            if (Validator.isNotNull(author)) {
                criteria.add("ExampleBlog_Author.name like ?");
            }
            if (!criteria.isEmpty()) {
                sqlBuilder.append("WHERE ");
                sqlBuilder.append(criteria.remove(0) + " ");
                for (String criterion : criteria) {
                    sqlBuilder.append(" AND "+criterion);
                }
            }
            /* If all the parameters was given, the "sql" variable content should be something like
                 SELECT {ExampleBlog_Post.*} FROM ExampleBlog_Post
                  JOIN ExampleBlog_Author
                   ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
                 WHERE ExampleBlog_Post.title like ?
                      AND ExampleBlog_Post.content like ?
                      AND ExampleBlog_Author.name like ?//
             */
             String sql = sqlBuilder.toString();
            // Now that we have built the query, we can do all the usual stuff.
            SQLQuery q = session.createSQLQuery(sql);
            q.setCacheable(false);
            q.addEntity("ExampleBlog_Post", PostImpl.class);

            QueryPos qPos = QueryPos.getInstance(q);
            // Setting the positions
            if (Validator.isNotNull(title)) {
                qPos.add("%"+title+"%");
            }
            if (Validator.isNotNull(content)) {
                qPos.add("%"+content+"%");
            }
            if (Validator.isNotNull(author)) {
                qPos.add("%"+author+"%");
            }

            //execute the query and return a list from the db
            return (List<Post>) QueryUtil.list(q, getDialect(), begin, end);
        } catch (Exception e) {
            throw new SystemException(e);
        } finally {
            //must have this to close the hibernate session..
            // if you fail to do this.. you will have a lot of open sessions… 
            closeSession(session);
        }
    } // end

Pay attention to the imported classes, since probably there are a lot of Session, SystemExceptions etc. classes in your path.

Adding the method to the local service 

Although we have created a FinderImpl and generated a FinderUtil our portlet class cannot use the FinderUtil directly, only aLocalServiceImpl class can do so. It is so for encouraging proper layer separation in our portlets: the finders (and persistences) classes care only about, well, persistence, dealing with database and so on. Just the service and local service classes provide business logic for portlets, so the finders should not be directly accessed by portlets. It would not be possible, anyway: theFinderUtil classes have dependencies which are injected by the LocalServiceImpl classes.

The practical consequence is: we should create a method in the PostLocalServiceImpl class calling the finder util:

public List<Post> findByTitleContentAuthor(String title, String content, String author, int begin, int end) throws SystemException {
    return PostFinderUtil.findByTitleContentAuthor(title, content, author, begin, end); // end

Once we have written the method, we execute ant build-service and the PostLocalServiceUtil will have the method.

Using the query in the portlet

Once we have followed all the steps above, we can just use the new method of the Local Service in the portlet, as below:

public class BlogPortlet extends GenericPortlet {
    // . . .
    @Override
    public void doView(RenderRequest renderRequest,
            RenderResponse renderResponse) throws IOException, PortletException {
        // ...
        List<Post> posts = PostLocalServiceUtil.findByTitleContentAuthor(title, content, author, 0, 10); 
        // . . .
    } // end

At the attachments, you can find the resulting portlet.


第二种方法:

推荐使用此方法,将第一种方法中的SQL 写到 XML中,然后在JAVA代码中用replace进行替换。

Bonus Point: CustomSQLUtil

In our example we wrote the SQL query in a string inside the code. However, it is not really elegant, so Liferay provides a way to separate the SQL queries from the Java code.

Creating the default.xml file

First, we should create the folder custom-sql at the docroot/WEB-INF/src source folder. There, we create a file calleddefault.xml where we put the queries. This is a XML file like the one below:

<?xml version="1.0" encoding="UTF-8"?>
<custom-sql>
    <sql id="br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor">
        SELECT {ExampleBlog_Post.*}
        FROM ExampleBlog_Post
        INNER JOIN 
            ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
        WHERE
            (ExampleBlog_Post.title LIKE ?) AND
            (ExampleBlog_Post.content LIKE ?) AND
            (ExampleBlog_Author.name LIKE ?)
    </sql>
</custom-sql>

Each SQL query comes inside a <sql> element. Each one of these<sql> elements should have anid attribute. The attribute can be anything unique in the portlet (that is, you cannot repeat ids) but it is usual to use the name of the finder interface followed by a dot and the name of the finder method. In fact, it is what we did when defined the id asbr.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor.

Once we have created the docroot/WEB-INF/src/custom-sql/default.xml file, we update theFinderImpl class. We replace the code which builds the SQL query manually...

我们来替换方法一中的代码:

 
//now we build the query. Note that we use the name of the tables from the database!
 String base = "SELECT {ExampleBlog_Post.*} FROM ExampleBlog_Post " +
 "JOIN ExampleBlog_Author " +
 "ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId ";
 StringBuffer sqlBuilder = new StringBuffer(base);
 List<String> criteria = new ArrayList<String>();
 if (Validator.isNotNull(title)) {
     criteria.add("ExampleBlog_Post.title like ?");
 }
 if (Validator.isNotNull(content)) {
     criteria.add("ExampleBlog_Post.content like ?");
 }
 if (Validator.isNotNull(author)) {
     criteria.add("ExampleBlog_Author.name like ?");
 }
 if (!criteria.isEmpty()) {
     sqlBuilder.append("WHERE ");
     sqlBuilder.append(criteria.remove(0) + " ");
     for (String criterion : criteria) {
         sqlBuilder.append(" AND "+criterion);
     }
 }
 /* If all the parameters was given, the "sql" variable content should be something like
     SELECT {ExampleBlog_Post.*} FROM ExampleBlog_Post
      JOIN ExampleBlog_Author
       ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
     WHERE ExampleBlog_Post.title like ?
          AND ExampleBlog_Post.content like ?
          AND ExampleBlog_Author.name like ?
 */
 String sql = sqlBuilder.toString();

 // Now that we have built the query, we can do all the usual stuff.
 SQLQuery q = session.createSQLQuery(sql);

...with the use of the CustomSQLUtil class:

替换后的代码:

 //now we build the query. Note that we use the name of the tables from the database!
 String sql = CustomSQLUtil.get("br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor");
 // Now that we have built the query, we can do all the usual stuff.
 SQLQuery q = session.createSQLQuery(sql);

Note how we retrieve the SQL query using the id of the <sql> element from thedefault.xml file.

Also, we replace the current query parameter setting code...

我们也来替换参数:

 
QueryPos qPos = QueryPos.getInstance(q);
 // Setting the positions 
 if (Validator.isNotNull(title)) {
     qPos.add("%"+title+"%");
 }
 if (Validator.isNotNull(content)) {
     qPos.add("%"+content+"%");
 }
 if (Validator.isNotNull(author)) {
     qPos.add("%"+author+"%");
 }
 //execute the query and return a list from the db

...with one which always set all parameters (since the query retrieved from thedefault.xml file will always have three parameters to be set:

替换后的代码:

 QueryPos qPos = QueryPos.getInstance(q);  
 qPos.add("%"+title+"%");
 qPos.add("%"+content+"%");
 qPos.add("%"+author+"%");

[$AND_OR_NULL_CHECK$] and [$AND_OR_CONNECTOR$] 

It will not work, however. The code will compile and will run without any exception, but the logic of the query is wrong: if some of the parameters is null, it will be added as"%null%", so only the posts which contains such string in one of its fields will be found. Certainly, it isnot what we are looking for.

Fortunately, the CustomSQLUtil class has a trick to solve this problem. First, we should update the query. instead of

方法二中不推荐的代码:

SELECT {ExampleBlog_Post.*}
FROM ExampleBlog_Post
INNER JOIN 
    ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
WHERE
    (ExampleBlog_Post.title LIKE ?) AND
    (ExampleBlog_Post.content LIKE ?) AND
    (ExampleBlog_Author.name LIKE ?)

we will write

替换后的代码:这个很重要,看清楚这两个替换符号的位置和括号在哪里

SELECT {ExampleBlog_Post.*}
FROM ExampleBlog_Post
INNER JOIN 
    ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
WHERE
    (ExampleBlog_Post.title LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$] 
    (ExampleBlog_Post.content LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$]
    (ExampleBlog_Author.name LIKE ? [$AND_OR_NULL_CHECK$])

Once we made it, we invoke the CustomSQLUtil.replaceAndOperator method on the retrieved query string:

 String sql = sqlBuilder.toString();
 String sql = CustomSQLUtil.get("br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor");
 sql = CustomSQLUtil.replaceAndOperator(sql, true);
 // Now that we have built the query, we can do all the usual stuff.

当我们 对要将title作为查询条件,则要设置

CustomSQLUtil.replaceAndOperator(sql, true)
当我们不要title为查询条件,则

CustomSQLUtil.replaceAndOperator(sql, false);//或者不设置,具体看源码

Note that the CustomSQLUtil.replaceAndOperator method receives two arguments. The first one is the query to be altered; the second one is a flag saying if the query is a disjunction (that is, only the elements which satisfy all criteria are returned) or a conjunction (that is, all elements which satisfy at least one criteria are returned). When the value is true, it is a disjunction: the[$AND_OR_NULL_CHECK$] string is replaced by"OR ? IS NULL" and the[$AND_OR_CONNECTOR$] is replaced with"AND". So, in the code above, the resulting query would be:

SELECT {ExampleBlog_Post.*}
FROM ExampleBlog_Post
INNER JOIN 
    ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
WHERE
    (ExampleBlog_Post.title LIKE ? OR ? IS NULL) AND
    (ExampleBlog_Post.content LIKE ? OR ? IS NULL) AND
    (ExampleBlog_Author.name LIKE ? OR ? IS NULL)

(If the second argument of CustomSQLUtil.replaceAndOperator were false,[$AND_OR_CONNECTOR$] would be replaced byOR and[$AND_OR_NULL_CHECK$] would be replaced by"AND ? IS NOT NULL". The resulting query would be:

SELECT {ExampleBlog_Post.*}
FROM ExampleBlog_Post
INNER JOIN 
    ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
WHERE
    (ExampleBlog_Post.title LIKE ? AND ? IS NOT NULL) OR
    (ExampleBlog_Post.content LIKE ? AND ? IS NOT NULL) OR
    (ExampleBlog_Author.name LIKE ? AND ? IS NOT NULL)

Note how the [$AND_OR_NULL_CHECK$] replacements add a new parameter to the query each one[2], so we should add more parameters to theQueryPos object:


QueryPos qPos = QueryPos.getInstance(q);
qPos.add("%"+title+"%");
qPos.add(title);
qPos.add("%"+content+"%");
qPos.add(content);
qPos.add("%"+author+"%");
qPos.add(author);


Now, the query should work correctly.

还有一个更好的办法:


QueryPos qPos = QueryPos.getInstance(q);
qPos.add("%"+title+"%",2);
qPos.add("%"+content+"%",2);
qPos.add("%"+author+"%",2);

如果这个title有多个,那么放到数组中:

String[] titles = null;
titles = new String {"%title01%","%title02%"};
sql = CustomSQLUtil.replaceKeywords( sql, "title", StringPool.LIKE, false, titles);

后面还是:
QueryPos qPos = QueryPos.getInstance(q);
qPos.add("%"+title+"%",2);
qPos.add("%"+content+"%",2);
qPos.add("%"+author+"%",2);


Good practices 

Looking inside the Liferay source code, we can find various custom queries, and some common practices. Some of these practices bring some safety and elegance to the code, so we describe them below.

Use <![CDATA[ on default.xml

Instead of writing a query "freely" inside the <sql> element, put the query inside a<![CDATA[ session:

<sql id="br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor">
<![CDATA[
    SELECT {ExampleBlog_Post.*}
    FROM ExampleBlog_Post
    INNER JOIN 
        ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
    WHERE
        (ExampleBlog_Post.title LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$] 
        (ExampleBlog_Post.content LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$]
        (ExampleBlog_Author.name LIKE ? [$AND_OR_NULL_CHECK$])
    ]]>
</sql>

It avoids problems with XML parsing.

Put the id of the query in a constant 

Instead of writing

CustomSQLUtil.get("br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor");

create a constant containing the id

public static final String FIND_BY_TITLE_CONTENT_AUTHOR = "br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor";

and use it to find the query:

String sql = CustomSQLUtil.get(FIND_BY_TITLE_CONTENT_AUTHOR);

If we follow the practice of using the finder name in the id, we can use the name of the class to create the constant:

public static final String FIND_BY_TITLE_CONTENT_AUTHOR = PostFinder.class.getName()+".findByTitleContentAuthor";

Indeed, it is a fairly common idiom.

Separate queries in more than one XML file 

In our example, we created only one finder, for one entity only: Post. Eventually, we can create more finders for more entities. In these cases, the default.xml could become very big and cumbersome.

Fortunately, we can put queries in various files and import them into the default.xml. Let us suppose we will create a finder for the Author entity, too. This finder will contain various queries, so it would be better to separate theAuthor queries and thePost queries. We change thedefault.xml for just including two other files:

<?xml version="1.0" encoding="UTF-8"?>
<custom-sql>
    <sql file="custom-sql/post.xml" />
    <sql file="custom-sql/author.xml" />
</custom-sql>

The content of the post.xml file would be:

<?xml version="1.0" encoding="UTF-8"?>
<custom-sql>
    <sql id="br.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder.findByTitleContentAuthor"><![CDATA[
        SELECT {ExampleBlog_Post.*}
        FROM ExampleBlog_Post
        INNER JOIN 
            ExampleBlog_Author ON ExampleBlog_Post.authorId = ExampleBlog_Author.authorId
        WHERE
            (ExampleBlog_Post.title LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$] 
            (ExampleBlog_Post.content LIKE ? [$AND_OR_NULL_CHECK$]) [$AND_OR_CONNECTOR$]
            (ExampleBlog_Author.name LIKE ? [$AND_OR_NULL_CHECK$])
    ]]></sql>
</custom-sql>

and the content of author.xml file would be similar.




Here are some sources used for writing this article:

Notes 

[1] Note that it is not created at the docroot/WEB-INF/service folder. The Service Builder will generate two new files in thedocroot/WEB-INF/service source folder: the interfacebr.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinder and the util classbr.com.seatecnologia.portlet.exampleblog.service.persistence.PostFinderUtil. We implement the query method inPostFinderImpl but will usePostFinderUtil in our portlet.

[2] Each ? in the query is a parameter.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值