1. 简介

在本篇文章中,我们将探讨如何在 Jakarta Persistence API (JPA) 中调用存储过程(Stored Procedure)。

2. 项目配置

2.1. Maven 配置

首先,我们需要在 pom.xml 文件中定义以下依赖项:

  • 一个 JPA 实现:本例中使用的是 Hibernate(通过 hibernate-core 实现 Jakarta Persistence 3.1)
  • jakarta.xml.bind-api
  • jakarta.annotation-api
  • MySQL 数据库连接器:mysql-connector-j
<properties>
    <mysql.version>8.4.0</mysql.version>
    <hibernate.version>6.5.2.Final</hibernate.version>
    <jakarta.xml.bind-api.version>4.0.0</jakarta.xml.bind-api.version>
    <jakarta.annotation.version>3.0.0</jakarta.annotation.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>${hibernate.version}</version>
        <exclusions>
           <exclusion>
              <groupId>jakarta.xml.bind</groupId>
              <artifactId>jakarta.xml.bind-api</artifactId>
           </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
        <version>${jakarta.xml.bind-api.version}</version>
    </dependency>
    <dependency>
        <groupId>jakarta.annotation</groupId>
        <artifactId>jakarta.annotation-api</artifactId>
        <version>${jakarta.annotation.version}</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>${mysql.version}</version>
    </dependency>
</dependencies>

2.2. 持久化单元配置

第二步是创建 src/main/resources/META-INF/persistence.xml 文件,定义持久化单元:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">

    <persistence-unit name="jpa-db">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>com.baeldung.jpa.model.Car</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/baeldung" />
            <property name="jakarta.persistence.jdbc.user" value="baeldung" />
            <property name="jakarta.persistence.jdbc.password" value="YourPassword" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.proc.param_null_passing" value="true" />
        </properties>
    </persistence-unit>

</persistence>

如果在 JEE 环境中使用 JNDI 数据源,上述 Hibernate 属性可以省略,只需指定:

<jta-data-source>java:jboss/datasources/JpaStoredProcedure</jta-data-source>

2.3. 表结构创建脚本

接下来创建数据库表 CAR,包含三个字段:ID, MODEL, YEAR

CREATE TABLE `car` (
  `ID` int(10) NOT NULL AUTO_INCREMENT,
  `MODEL` varchar(50) NOT NULL,
  `YEAR` int(4) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

假设数据库 schema 和权限已经配置完成。

2.4. 存储过程定义

在进入 Java 代码之前,我们需要在 MySQL 数据库中创建存储过程:

DELIMITER $$
CREATE DEFINER=`root`@`localhost` PROCEDURE `FIND_CAR_BY_YEAR`(in p_year int)
begin
SELECT ID, MODEL, YEAR
    FROM CAR
    WHERE YEAR = p_year;
end
$$
DELIMITER ;

3. 使用 JPA 调用存储过程

现在我们可以使用 JPA 与数据库交互,调用上面定义的存储过程,并将结果映射为 Car 对象列表。

3.1. Car 实体类

以下是与 CAR 表对应的实体类。我们还通过 @NamedStoredProcedureQueries 注解将存储过程直接绑定到实体类上:

@Entity
@Table(name = "CAR")
@NamedStoredProcedureQueries({
  @NamedStoredProcedureQuery(
    name = "findByYearProcedure", 
    procedureName = "FIND_CAR_BY_YEAR", 
    resultClasses = { Car.class }, 
    parameters = { 
        @StoredProcedureParameter(
          name = "p_year", 
          type = Integer.class, 
          mode = ParameterMode.IN) }) 
})
public class Car {

    private long id;
    private String model;
    private Integer year;

    public Car(String model, Integer year) {
        this.model = model;
        this.year = year;
    }

    public Car() {
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", unique = true, nullable = false, scale = 0)
    public long getId() {
        return id;
    }

    @Column(name = "MODEL")
    public String getModel() {
        return model;
    }

    @Column(name = "YEAR")
    public Integer getYear() {
        return year;
    }
    
    // 标准 setter 方法
}

3.2. 数据库访问测试

现在我们编写两个测试方法,使用 JPA 调用存储过程获取指定年份的汽车数据:

public class StoredProcedureTest {

    private static EntityManagerFactory factory = null;
    private static EntityManager entityManager = null;

    @BeforeClass
    public static void init() {
        factory = Persistence.createEntityManagerFactory("jpa-db");
        entityManager = factory.createEntityManager();
    }

    @Test
    public void findCarsByYearWithNamedStored() {
        StoredProcedureQuery findByYearProcedure = 
          entityManager.createNamedStoredProcedureQuery("findByYearProcedure");
        
        StoredProcedureQuery storedProcedure = 
          findByYearProcedure.setParameter("p_year", 2015);
        
        storedProcedure.getResultList()
          .forEach(c -> Assert.assertEquals(new Integer(2015), ((Car) c).getYear())); 
    }

    @Test
    public void findCarsByYearNoNamedStored() {
        StoredProcedureQuery storedProcedure = 
          entityManager
            .createStoredProcedureQuery("FIND_CAR_BY_YEAR",Car.class)
            .registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN)
            .setParameter(1, 2015);
       
        storedProcedure.getResultList()
          .forEach(c -> Assert.assertEquals(new Integer(2015), ((Car) c).getYear()));
    }

}

注意:第二个测试没有使用实体类上定义的命名存储过程,而是动态创建的。
这在无法修改实体类或不想重新编译的场景下非常实用。

4. 向存储过程传递 null 参数

默认情况下,Hibernate 不会将 null 值传递给存储过程,这是为了允许数据库使用默认参数值。
但如果你确实需要传递 null,可以通过在 persistence.xml 中设置如下属性来启用:

<property name="hibernate.proc.param_null_passing" value="true" />

我们可以通过 JUnit 5 测试验证 null 参数的传递:

@Test
public void givenStoredProc_whenNullParamPassed_thenNoExceptionThrown() {
    final StoredProcedureQuery storedProcedure = 
      entityManager.createStoredProcedureQuery("FIND_CAR_BY_YEAR", Car.class)
        .registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN);
             
    assertDoesNotThrow(() -> {
      storedProcedure.setParameter(1, null);
        });
    }

此外,还可以通过 API 为单个调用开启 null 传递:

ProcedureCall procedureCall = getSession()
  .createStoredProcedureCall("findByYearProcedure");

procedureCall
  .registerParameter("nullable_param", String.class, ParameterMode.IN)
  .enablePassingNulls(true);

5. 总结

本文介绍了如何在 JPA 中调用存储过程,并展示了如何通过命名查询和动态创建两种方式来实现。同时也提到了如何处理 null 参数传递的问题。对于需要在 Java 应用中与数据库存储过程交互的开发者来说,这些内容非常实用。


原始标题:A Guide to Stored Procedures with JPA | Baeldung