11、Java持久化查询语言(JPQL)的使用指南

Java持久化查询语言(JPQL)的使用指南

在开发使用Java持久化API(JPA)的应用程序时,我们经常需要从数据库中检索和操作实体实例。传统的通过主键检索实体的方法存在一定局限性,而Java持久化查询语言(JPQL)则提供了更强大的解决方案。

1. JPQL概述

JPQL在很多方面与SQL相似,它们都是用于与关系型数据库交互的工具,通过非过程化语句来访问和操作数据。但两者也存在关键区别:
- 操作对象 :SQL直接操作数据库对象,如特定的表或视图;而JPQL处理的是实体,查询结果也是实体实例,而非表或视图记录。
- 数据库独立性 :JPQL是一种与数据库供应商无关的语言,无论实体映射到何种底层数据库,对JPQL来说都没有区别。

使用JPQL的好处还体现在简化实体实例的检索任务上。在事务上下文中调用查询方法时,返回的所有实体实例都会自动被管理,无需执行合并操作。

2. 何时使用JPQL

在开发使用JPA的应用程序时,选择JPQL而非SQL有以下几个原因:
- 检索实体集合 :当需要根据非主键列的条件检索一组实体实例时,JPQL非常合适。它允许在SELECT语句的WHERE子句中指定条件来限制查询结果,同时也支持GROUP BY、HAVING和ORDER BY子句。
- 执行连接查询 :可以对具有关系的实体执行连接查询。
- 批量操作 :不仅可以执行SELECT查询,还能对特定实体类或继承层次结构的实体执行批量删除和更新操作。
- 可移植性 :由于JPQL是数据库供应商无关的语言,使用它创建的查询可以在不同的底层数据库上移植。

3. JPQL支持的操作

JPQL支持的语句有三种:
- SELECT
- UPDATE
- DELETE

其中,SELECT语句除了必需的SELECT和FROM子句外,还有一些可选子句,如下表所示:
| 子句 | 描述 |
| — | — |
| WHERE | 该子句中指定的条件用于限制查询的结果集。 |
| GROUP BY | 根据该子句中指定的实体字段的值,将检索到的实体实例集合划分为多个组。 |
| HAVING | 与GROUP BY结合使用,将检索到的实体组限制为满足指定条件的组。 |
| ORDER BY | 根据该子句中指定的实体字段的排序规则,对检索到的实体实例集合进行排序。 |

此外,JPQL还提供了一组可在查询子句中使用的运算符和函数,其语法和用法大多与SQL相似。

4. 处理JPQL语句

EntityManager接口提供了两个方法,可与Query接口方法结合使用,来创建、绑定和执行JPQL语句,如下表所示:
| 方法 | 描述 |
| — | — |
| public Query createQuery(String jpql_stmt) | 创建一个在业务逻辑代码中定义的动态查询。它接受一个JPQL语句字符串作为参数,并返回一个用于执行该JPQL语句的Query实例。 |
| public Query createNamedQuery(String query_name) | 创建一个在元数据中定义的静态查询。它接受一个使用NamedQuery注解定义的JPQL查询或使用NamedNativeQuery注解定义的原生SQL查询的名称,并返回一个用于执行该命名查询的Query实例。 |

以下是一个创建和执行SELECT查询的示例:

Query query = em.createQuery("SELECT e FROM Employee e");
List<Employee> employees = (List<Employee>)query.getResultList();

在实际应用中,也可以使用以下语法:

List<Employee> employees = (List<Employee>)em.createQuery(
    "SELECT e FROM Employee e")
    .getResultList();
5. 使用Query API

创建Query实例后,需要执行查询并检索结果,这些任务以及参数绑定和分页控制都由Query接口方法处理。以下是一些常用的Query接口方法:
| 方法 | 描述 |
| — | — |
| public List getResultList() | 执行由EntityManager的createQuery或createNamedQuery方法创建的SELECT查询,并将一组实体作为列表检索。 |
| public Object getSingleResult() | 执行由EntityManager的createQuery或createNamedQuery方法创建的SELECT查询,并将单个实体作为结果检索。 |
| public int executeUpdate() | 执行由EntityManager的createQuery或createNamedQuery方法创建的UPDATE或DELETE查询,并返回受影响的实体数量。 |
| public Query setMaxResults(int maxResult) | 定义要检索的实体实例的最大数量,并返回相同的Query实例。 |
| public Query setFirstResult(int startPosition) | 定义要检索的第一个实体实例的位置,并返回相同的Query实例。 |
| public Query setHint(String hintName, Object value) | 指定特定于供应商的提示,并返回相同的Query实例。 |
| public Query setParameter(String name, Object value) | 执行命名参数绑定,并返回相同的Query实例。 |
| public Query setParameter(String name, Date value, TemporalType temporalType) | 将Date实例绑定到命名参数,并返回相同的Query实例。 |
| public Query setParameter(int position, Object value) | 执行位置参数绑定,并返回相同的Query实例。 |
| public Query setParameter(int position, Date value, TemporalType temporalType) | 将Date实例绑定到位置参数,并返回相同的Query实例。 |

6. 使用JPQL检索实体
6.1 简单示例

以下是一个简单的示例,展示了如何使用JPQL检索所有员工实体实例:

// JpqlTestBean.java
package ejbjpa.ejb;
import java.io.Serializable;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import ejbjpa.entities.*;

@Stateless
public class JpqlTestBean implements JpqlTest {
    @PersistenceContext
    private EntityManager em;

    public List<Employee> getEmployees() {
        List<Employee> employees = null;
        try {
            employees = (List<Employee>)em.createQuery("SELECT e FROM Employee e")
               .getResultList();
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return employees;
    }
}

为了测试这个会话Bean,可以创建一个Servlet来调用其getEmployees方法:

// JpqlTestServlet.java
package ejbjpa.servlets;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.ejb.EJB;
import java.util.List;
import java.util.Iterator;
import ejbjpa.entities.*;
import ejbjpa.ejb.*;

public class JpqlTestServlet extends HttpServlet {
    @EJB private JpqlTest jpqlTest;

    public void doGet(
        HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setBufferSize(8192);
        PrintWriter out = response.getWriter();
        try{
            List<Employee> employees = jpqlTest.getEmployees();
            Iterator i = employees.iterator();
            Employee employee;
            while (i.hasNext()) {
                employee = (Employee) i.next();
                out.println("Employee id: "+ employee.getEmpno() +"<br/>");
                out.println("First name: "+ employee.getFirstname() +"<br/>");
                out.println("Last name: "+ employee.getLastname() +"<br/>");
                out.println("----------"+ "<br/>");
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
}
6.2 检索的实体是否被管理

根据JPA规范,在事务上下文中执行查询时,无论以何种方式检索实体,它们都会自动附加到当前持久化上下文。可以通过以下方法进行测试:

// JpqlTestBean.java 增加 checkIfManaged 方法
import java.util.Iterator;
@Stateless
public class JpqlTestBean implements JpqlTest {
    @PersistenceContext
    private EntityManager em;

    public List<Employee> getEmployees() {
        // ...
    }

    public boolean checkIfManaged(){
        List<Employee> employees = null;
        try {
            employees = (List<Employee>)em.createQuery("SELECT e FROM Employee e")
               .getResultList();
            Employee employee;
            Iterator i = employees.iterator();
            while (i.hasNext()) {
                employee = (Employee) i.next();
                if (!em.contains(employee)) {
                    return false;
                }
            }
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return true;
    }
}

同时更新Servlet来调用这个方法:

// JpqlTestServlet.java 更新
public class JpqlTestServlet extends HttpServlet {
    @EJB private JpqlTest jpqlTest;

    public void doGet(
        HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
        // ...
        try{
            // ...
            out.println("All employee entities were managed during the " +
                "checkIfManaged call: "+ jpqlTest.checkIfManaged() +"<br/>");
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
}
6.3 导航检索实体的关系

JPQL查询返回的实体实例自动被管理,那么它们的关联实体呢?实际上,关联实体是否随查询结果一起被检索,取决于实体之间关系的获取模式(fetch mode)。默认情况下,一对多和多对多关系的获取模式为LAZY,一对一和多对一关系的获取模式为EAGER。

以下示例展示了关联实体在被访问时会自动被管理:

// JpqlTestBean.java 更新 checkIfManaged 方法
@Stateless
public class JpqlTestBean implements JpqlTest {
    @PersistenceContext
    private EntityManager em;

    public List<Employee> getEmployees() {
        // ...
    }

    public boolean checkIfManaged(){
        List<Employee> employees = null;
        List<Order> orders =null;
        try {
            employees = (List<Employee>)em.createQuery("SELECT e FROM Employee e")
               .getResultList();
            Employee employee;
            Order order;
            Iterator i = employees.iterator();
            Iterator j;
            while (i.hasNext()) {
                employee = (Employee) i.next();
                if (!em.contains(employee)) {
                    return false;
                }
                orders = (List<Order>)employee.getOrders();
                j= orders.iterator();
                while (j.hasNext()) {
                    order = (Order) j.next();
                    if (!em.contains(order)) {
                        return false;
                    }
                }
            }
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return true;
    }
}
6.4 使用JPQL FETCH JOINS

当使用LAZY模式时,关联实体不会随查询结果一起被检索。有时,我们希望在查询结果中同时检索关联实体,这时可以使用JPQL FETCH JOINS。

以下是一个示例,创建一个JpqlJoinsTestBean会话Bean,其countOrders业务方法用于执行测试:

// JpqlJoinsTestBean.java
package ejbjpa.ejb;
import java.io.Serializable;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import java.util.List;
import ejbjpa.entities.*;

@Stateless
public class JpqlJoinsTestBean implements JpqlJoinsTest {
    @PersistenceUnit(unitName="jpqljoins0-pu")
    private EntityManagerFactory emf0;
    @PersistenceUnit(unitName="jpqljoins1-pu")
    private EntityManagerFactory emf1;
    @PersistenceUnit(unitName="jpqljoins2-pu")
    private EntityManagerFactory emf2;

    public Integer[] countOrders(Integer empno){
        List<Order> orders0 = null;
        List<Order> orders1 = null;
        List<Order> orders2 = null;
        EntityManager em0 = emf0.createEntityManager();
        EntityManager em1 = emf1.createEntityManager();
        EntityManager em2 = emf2.createEntityManager();
        Integer[] numOfOrders= new Integer[3];
        try {
            // 执行查询
            Employee employee0 = (Employee)em0.createQuery("SELECT e " +
                "FROM Employee e WHERE e.empno=:empno")
               .setHint("toplink.refresh", "true")
               .setParameter("empno", empno)
               .getSingleResult();
            Employee employee1 = (Employee)em1.createQuery("SELECT DISTINCT e " +
                "FROM Employee e LEFT JOIN FETCH e.orders WHERE e.empno=:empno")
               .setHint("toplink.refresh", "true")
               .setParameter("empno", empno)
               .getSingleResult();
            Employee employee2 = (Employee)em2.createQuery("SELECT e " +
                "FROM Employee e WHERE e.empno=:empno")
               .setHint("toplink.refresh", "true")
               .setParameter("empno", empno)
               .getSingleResult();

            // 统计第一个查询中员工的订单数量
            orders0 = (List<Order>)employee0.getOrders();
            numOfOrders[0] = orders0.size();

            // 为第一个查询中的员工增加一个订单并提交更改
            Order order = new Order();
            order.setEmployee(employee0);
            em0.getTransaction().begin();
            em0.persist(order);
            em0.getTransaction().commit();

            // 统计第二个和第三个查询中员工的订单数量
            orders1 = (List<Order>)employee1.getOrders();
            numOfOrders[1] = orders1.size();
            orders2 = (List<Order>)employee2.getOrders();
            numOfOrders[2] = orders2.size();
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        em0.close();
        em1.close();
        em2.close();
        return numOfOrders;
    }
}

为了测试这个会话Bean,可以创建一个Servlet:

// JpqlJoinsTestServlet.java
package ejbjpa.servlets;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.ejb.EJB;
import ejbjpa.entities.*;
import ejbjpa.ejb.*;

public class JpqlJoinsTestServlet extends HttpServlet {
    @EJB private JpqlJoinsTest jpqlJoinsTest;

    public void doGet(
        HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setBufferSize(8192);
        PrintWriter out = response.getWriter();
        Integer[] numOfOrders= new Integer[3];
        numOfOrders=jpqlJoinsTest.countOrders(1);
        try{
            out.println("Number of orders associated with employee 1 before " +
                "increasing: "+numOfOrders[0] +"<br/>");
            out.println("Number of orders returned by the JOIN query after " +
                "increasing: "+numOfOrders[1] +"<br/>");
            out.println("Number of orders you actually got after " +
                "increasing: "+numOfOrders[2] +"<br/>");
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
}

执行该Servlet时,输出结果可能如下:

Number of orders associated with employee 1 before increasing: 25
Number of orders returned by the JOIN query after increasing: 25
Number of orders you actually got after increasing: 26

通常,前两个数字应该相等,最后一个数字应该比前两个数字大1。

7. 使用原生SQL查询

在开发使用JPA的Java应用程序时,仍然可以使用原生SQL表达查询。当需要利用JPQL无法实现的数据库特定功能时,选择SQL是一个不错的选择。

7.1 处理原生SQL查询

与JPQL类似,使用EntityManager的特殊方法来创建用原生SQL表达的查询。以下是相关方法:
| 方法 | 描述 |
| — | — |
| public Query createNamedQuery(String query_name) | 创建一个在元数据中定义的静态查询,使用NamedQuery注解或NamedNativeQuery注解应用于实体,并返回一个用于执行该命名查询的Query实例。 |
| public Query createNativeQuery(String sql_stmt) | 创建一个在业务逻辑代码中定义的动态查询,接受一个原生SQL语句(UPDATE或DELETE)作为参数,并返回一个用于执行该SQL语句的Query实例。 |
| public Query createNativeQuery(String sql_stmt, Class result_class) | 创建一个在业务逻辑代码中定义的动态查询,接受两个参数:一个SELECT SQL语句和结果实例的实体类,并返回一个用于执行指定SQL语句的Query实例。 |
| public Query createNativeQuery(String sql_stmt, String result_SetMapping) | 创建一个在业务逻辑代码中定义的动态查询,接受两个参数:一个SELECT SQL语句和使用SqlResultSetMapping注解在元数据中定义的结果集映射的名称,并返回一个用于执行指定SQL语句的Query实例。 |

7.2 原生SQL查询的简单示例

以下是一个在无状态会话Bean中使用动态原生SQL查询的简单示例:

// NativeQueryTestBean.java
package ejbjpa.ejb;
import java.io.Serializable;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import ejbjpa.entities.*;

@Stateless
public class NativeQueryTestBean implements NativeQueryTest {
    @PersistenceContext
    private EntityManager em;

    public List<Employee> getEmployees() {
        List<Employee> employees = null;
        try {
            employees = (List<Employee>)em.createNativeQuery("SELECT * FROM " +
                "employees", ejbjpa.entities.Employee.class)
               .getResultList();
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return employees;
    }
}

测试这个会话Bean时,可以使用与测试JpqlTestBean会话Bean相同的Servlet,只需更改使用@EJB注解注入的Bean名称。

总结

JPQL为访问和操作实体实例提供了强大的方式,它是一种与数据库供应商无关的语言,无论实体映射到何种底层数据库都能正常工作。但当需要利用数据库特定功能时,仍然可以使用原生SQL对JPA实体创建和执行查询。通过掌握JPQL和原生SQL的使用,我们可以更灵活地开发使用JPA的Java应用程序。

Java持久化查询语言(JPQL)的使用指南

8. JPQL与原生SQL的选择决策流程

在实际开发中,如何选择使用JPQL还是原生SQL是一个关键问题。下面通过一个mermaid流程图来展示选择的决策流程:

graph TD;
    A[是否需要数据库特定功能?] -->|是| B[选择原生SQL];
    A -->|否| C[是否需要检索实体集合或执行连接查询等通用操作?];
    C -->|是| D[选择JPQL];
    C -->|否| E[根据具体情况再评估];

这个流程图清晰地展示了决策的步骤:首先判断是否需要数据库特定功能,如果需要则选择原生SQL;如果不需要,再看是否有检索实体集合或执行连接查询等通用操作的需求,若有则选择JPQL,若没有则需要根据具体情况进一步评估。

9. 实际应用中的注意事项

在使用JPQL和原生SQL进行开发时,还有一些实际应用中的注意事项需要我们关注:
- 事务管理 :无论是JPQL还是原生SQL查询,在涉及数据修改(如UPDATE、DELETE操作)时,都需要确保在事务上下文中执行。例如,在前面的示例中,为员工增加订单并提交更改的操作就需要在事务中进行:

em0.getTransaction().begin();
em0.persist(order);
em0.getTransaction().commit();
  • 异常处理 :在执行查询时,可能会出现各种异常,如数据库连接异常、查询语法错误等。因此,需要对异常进行适当的处理,避免程序崩溃。在前面的示例中,我们使用了try-catch块来捕获异常并抛出EJBException:
try {
    // 查询操作
} catch (Exception e) {
    throw new EJBException(e.getMessage());
}
  • 性能优化 :对于复杂的查询,尤其是涉及大量数据的查询,需要考虑性能优化。可以通过合理使用索引、优化查询语句、避免不必要的关联查询等方式来提高查询性能。例如,在使用JPQL的FETCH JOIN时,可以一次性检索关联实体,减少后续的数据库查询次数。
10. 综合示例:一个完整的业务场景

假设我们有一个电商系统,需要查询某个客户的订单信息,并根据订单金额进行分组统计。以下是一个综合使用JPQL的示例:

// CustomerOrderService.java
package com.example.service;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Stateless
public class CustomerOrderService {
    @PersistenceContext
    private EntityManager em;

    public List<Object[]> getCustomerOrderStatistics(Long customerId) {
        String jpql = "SELECT o.status, SUM(o.amount) " +
                      "FROM Order o " +
                      "JOIN o.customer c " +
                      "WHERE c.id = :customerId " +
                      "GROUP BY o.status";
        return em.createQuery(jpql)
                 .setParameter("customerId", customerId)
                 .getResultList();
    }
}
// CustomerOrderServlet.java
package com.example.servlets;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

@WebServlet("/customerOrderStatistics")
public class CustomerOrderServlet extends HttpServlet {
    @EJB
    private CustomerOrderService customerOrderService;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        Long customerId = Long.parseLong(request.getParameter("customerId"));
        List<Object[]> statistics = customerOrderService.getCustomerOrderStatistics(customerId);
        out.println("<html><body>");
        out.println("<h2>Customer Order Statistics</h2>");
        out.println("<table border='1'>");
        out.println("<tr><th>Order Status</th><th>Total Amount</th></tr>");
        for (Object[] row : statistics) {
            String status = (String) row[0];
            Double totalAmount = (Double) row[1];
            out.println("<tr><td>" + status + "</td><td>" + totalAmount + "</td></tr>");
        }
        out.println("</table>");
        out.println("</body></html>");
    }
}

在这个示例中, CustomerOrderService 类中的 getCustomerOrderStatistics 方法使用JPQL查询某个客户的订单信息,并根据订单状态进行分组统计。 CustomerOrderServlet 类则负责接收客户端请求,调用服务方法并将结果以HTML表格的形式展示给用户。

11. 总结与展望

通过前面的介绍,我们详细了解了Java持久化查询语言(JPQL)和原生SQL的使用方法、适用场景以及实际应用中的注意事项。JPQL为我们提供了一种与数据库无关的方式来操作实体,使得我们的代码具有更好的可移植性;而原生SQL则在需要利用数据库特定功能时发挥重要作用。

在未来的开发中,随着数据库技术和Java Persistence API的不断发展,我们可以期待更多的功能和优化。例如,可能会有更智能的查询优化器,能够自动根据数据库的特性和查询需求生成最优的查询语句;也可能会有更方便的工具来帮助我们进行事务管理和异常处理。我们需要不断学习和掌握这些新技术,以提高我们的开发效率和应用程序的性能。

同时,我们也应该注意代码的可维护性和可读性。在编写查询语句时,尽量遵循良好的编程规范,使用有意义的变量名和注释,使得代码易于理解和维护。通过合理地使用JPQL和原生SQL,我们可以构建出更加高效、稳定和可扩展的Java应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值