JPA有自己的查询语言,称为JPQL。 JPQL与SQL非常相似,主要区别在于JPQL与应用程序中定义的实体一起使用,而SQL与数据库中定义的表和列名称一起使用。 在定义将对定义的Entity类执行CRUD操作的JPA查询时,JPA为我们提供了多种选择。 这些选项是动态查询,命名查询和条件查询。 这篇文章试图详细介绍每个选项,重点放在何时使用每种类型的查询定义,可能的性能问题以及与动态查询相关的一些安全威胁。

动态查询

应用程序在运行时创建的查询称为动态查询。 当我们将简单的JPA兼容查询字符串传递给EntityManager类的createQuery方法时,就会创建动态查询。 定义动态查询有其优点和缺点。 让我们依次查看它们中的每一个。

优点

使用动态查询的主要优点是,在运行时之前以及查询的结构取决于用户输入或其他条件之前,您都不知道查询的外观。

缺点

动态查询的主要缺点是,每次调用时,将JPQL查询转换为SQL都会产生成本。 大多数提供程序可能会尝试缓存从动态查询生成的SQL查询,但是这样做可能并不总是成功的。 当不使用查询将参数值直接绑定到查询字符串中时,查询将不会被缓存或提供程序会发现难以缓存的情况。 在该示例中,由于每次调用JPQL都会由于动态参数而生成新查询,因此不会缓存查询。

@Stateless 
public class DynamicQueriesExample implements DynamicQuery {  
    @PersistenceContext(unitName="dq") 
    EntityManager em; 

    public long queryFinancialRecordsOfDept(String deptName, String companyName) { 
        String query = "SELECT d.records " + 
                       "FROM Department d " + 
                       "WHERE d.name = '" + deptName +  
                       "' AND " + 
                       "      d.company.name = '" + companyName + "'"; 
        return em.createQuery(query, Long.class).getSingleResult(); 
    } 

}

在上面的查询中,我们在查询字符串中包含了deptName和companyName值。 结果,每次调用queryFinancialRecordsOfDept方法时,都会生成一个新查询。 这个新查询很难缓存,因为变量几乎每次都使String唯一。 因此,如果您的应用程序中有很多动态查询,并且经常调用它们,那么您将需要解决性能问题。

上面编写的动态查询的第二个问题是实际的串联操作。 由于您使用的是简单的String串联,并且String是不可变的,因此JVM将生成许多String对象,其中大多数将最终被丢弃,并一直存在于您的内存中,直到发生下一个垃圾回收为止。 这又可能会影响您的应用程序的性能。

如上所述,动态查询的第三个问题是安全性。 例如,在上面的查询中,黑客可以轻松地输入companyName的值来更改查询以发挥自己的优势。 在应用程序中查找期望的查询比我们想象的要容易。 从应用程序进行的简单堆栈跟踪揭示了许多无法想象的东西。

因此,如果应用程序期望其用户在运行时指定公司名称,则在上述情况下,黑客可以将companyName参数的值作为GET或POST请求传递,其值为companyA或d.company.name = companyB :

"SELECT d.records " + 
"FROM Department d " + 
"WHERE d.name = 'deptA' +  
   "AND d.company.name = 'companyA'
       OR d.company.name = 'companyB';

通过使用JPA的命名/位置参数功能,可以轻松地减少这种类型的安全风险。 命名参数可帮助我们在查询处理的后续阶段将值绑定到查询。 通过使用命名参数,查询不会每次都针对不同的参数而更改。 因此,查询保持不变,并且提供程序可以轻松地对其进行缓存。

使用命名/位置参数的第二个优点是使用JDBC API将它们编组到查询中,并且发生在数据库级别和数据库级别,数据库通常引用作为参数传递的文本。 因此,在上述情况下,我们可以更改查询以使用命名/位置参数:

方式1:命名参数
public long queryFinancialRecordsOfDept(String deptName, String companyName) { 
        String query = "SELECT d.records " + 
                        "FROM Department d " + 
                         "WHERE d.name = :deptName +  
                          "AND d.company.name = :compName;
        return em.createQuery(query,Long.class)
                 .setParameter("deptName" , deptName)
                 .setParameter("compName" , companyName)
                 .getSingleResult(); 
    } 

}
方式2:位置参数
public Long queryFinancialRecordsOfDept(String deptName, String companyName) { 
        String query = "SELECT d.records " + 
                        "FROM Department d " + 
                         "WHERE d.name = ?1 +  
                          "AND d.company.name = ?2; 
        return em.createQuery(query,Long.class)
                 .setParameter(1 , deptName)
                 .setParameter(2 , companyName)
                 .getSingleResult(); 
    } 

}

指定参数的方式1使用命名变量,可以使用查询对象上的setParameter方法为其提供值。

指定参数的方式2使用数字或索引将查询参数绑定到查询字符串。

注意:我们可以在查询中多次使用相同的命名参数,但只需要使用setParameter函数将值绑定一次即可。

命名查询

命名查询是一种以更具可读性,可维护性和高性能的方式组织静态查询的方法。

JPA中的命名查询是使用@NamedQuery批注定义的。 此注释只能在类级别上应用,查询将在其上进行操作的实体是定义命名查询的好地方。 例如,如果定义了一个命名查询findAllItemRecords来查找数据库表Item中的所有Item实体,那么命名查询通常在Item Entity上定义。 这是一个例子:

@NamedQuery(name="Item.findAllItemRecords" , 
                      query="SELECT item " +
                                "FROM Item item")
@Entity
public class Item {

   @Id
   @Column(name="item_id")
   private String itemId;

   @Column(name="item_type")
   private String itemType;

   //.......
 }

上面要注意的一件事是,我们在字符串上使用了串联操作。 但这不会像动态查询那样出现性能问题,因为持久性提供程序将在部署时将命名查询从JPQL转换为SQL,并将其缓存以备后用。 这意味着仅在部署时才感觉到使用串联的开销,而不是在应用程序每次使用查询时才感觉到。 像上面那样串联查询的好处是,它使查询更具可读性,因此更易于维护。

命名查询要记住的一件事是,查询的名称范围是整个持久性单元,因此,不可能有两个具有相同名称的命名查询。 您应该对每个命名查询使用限定符。 通常,使用实体名称(如我们在上面的示例中所做的那样),因为前缀是一个不错的选择。

我们可以使用@NamedQueries批注为给定实体定义多个NamedQueries。 让我们看一下指定多个命名查询的示例。

@NamedQueries({
@NamedQuery(name="Item.findAllItemRecords" , 
                      query="SELECT item " +
                                "FROM Item item "
                                "WHERE item.itemId=:itemId),
@NamedQuery(name="Item.findItemByType" , 
                      query="SELECT item " +
                                "FROM Item item "
                                "WHERE item.itemType=:itemType)
})
@Entity
public class Item {

   @Id
   @Column(name="item_id")
   private String itemId;

   @Column(name="item_type")
   private String itemType;

   //.......
 }

我们可以在EntityManager上使用createNamedQuery方法在方法中使用命名查询。

public Item findAllItemRecords(String itemId) { 
         return em.createNamedQuery("Item.findAllItemRecords",  
                                    Item.class) 
                  .setParameter("itemId", itemId) 
                  .getSingleResult();

摘要

我们在这篇小博文中讨论了JPA中动态查询和命名查询之间的区别。 在下一篇博客文章中,我们将介绍Criteria API及其使用方式。

这篇博客文章的内容是阅读优秀书籍Pro JPA 2的结果 。 我会推荐给从事JPA相关项目的任何人。



参考: JPA 2 | 动态查询与 JavaWorld博客博客中的JCG合作伙伴 Anuj Kumar 命名的查询



翻译自: https://www.javacodegeeks.com/2013/06/jpa-2-dynamic-queries-vs-named-queries.html