编辑此页面

Spring Data API 的扩展

虽然我们鼓励用户使用 Hibernate ORM with Panache 进行关系数据库访问,但 Quarkus 以 spring-data-jpa 扩展的形式为 Spring Data JPA 仓库提供了一个兼容层。

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 仓库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载 归档文件

解决方案位于 spring-data-jpa-quickstart 目录中。

创建 Maven 项目

首先,我们需要一个新项目。使用以下命令创建一个新项目

CLI
quarkus create app org.acme:spring-data-jpa-quickstart \
    --extension='rest-jackson,spring-data-jpa,quarkus-jdbc-postgresql' \
    --no-code
cd spring-data-jpa-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=spring-data-jpa-quickstart \
    -Dextensions='rest-jackson,spring-data-jpa,quarkus-jdbc-postgresql' \
    -DnoCode
cd spring-data-jpa-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=spring-data-jpa-quickstart"

此命令会生成一个 Maven 项目并导入 spring-data-jpa 扩展。

如果您已经配置了 Quarkus 项目,则可以通过在项目根目录中运行以下命令将 spring-data-jpa 扩展添加到项目中

./mvnw quarkus:add-extension -Dextensions="spring-data-jpa"

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-spring-data-jpa</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-spring-data-jpa")

定义实体

在本指南中,将使用以下 JPA 实体

package org.acme.spring.data.jpa;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Fruit {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String color;


    public Fruit() {
    }

    public Fruit(String name, String color) {
        this.name = name;
        this.color = color;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

配置数据库访问属性

将以下属性添加到 application.properties 以配置对本地 PostgreSQL 实例的访问。

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql:quarkus_test
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
quarkus.hibernate-orm.schema-management.strategy=drop-and-create

此配置假定 PostgreSQL 将在本地运行。

实现此目的的一个非常简单的方法是使用以下 Docker 命令

docker run -it --rm=true --name quarkus_test -e POSTGRES_USER=quarkus_test -e POSTGRES_PASSWORD=quarkus_test -e POSTGRES_DB=quarkus_test -p 5432:5432 postgres:14.1

如果您计划使用不同的设置,请相应地更改您的 application.properties

准备数据

为了更容易地展示 Spring Data JPA 在 Quarkus 上的某些功能,应该将一些测试数据插入到数据库中,方法是将以下内容添加到名为 src/main/resources/import.sql 的新文件中

INSERT INTO fruit(id, name, color) VALUES (1, 'Cherry', 'Red');
INSERT INTO fruit(id, name, color) VALUES (2, 'Apple', 'Red');
INSERT INTO fruit(id, name, color) VALUES (3, 'Banana', 'Yellow');
INSERT INTO fruit(id, name, color) VALUES (4, 'Avocado', 'Green');
INSERT INTO fruit(id, name, color) VALUES (5, 'Strawberry', 'Red');

Hibernate ORM 将在应用程序启动时执行这些查询。

除了 import.sql 之外,用户还可以使用名为 data.sql 的文件

定义仓库

现在是时候定义将用于访问 Fruit 的仓库了。 以典型的 Spring Data 方式创建一个如下的仓库

package org.acme.spring.data.jpa;

import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface FruitRepository extends CrudRepository<Fruit, Long> {

    List<Fruit> findByColor(String color);
}

上面的 FruitRepository 扩展了 Spring Data 的 org.springframework.data.repository.CrudRepository,这意味着后者的所有方法都可用于 FruitRepository。 此外,还定义了 findByColor,其目的是返回与指定颜色匹配的所有 Fruit 实体。

更新 Jakarta REST 资源

有了仓库,下一步就是创建将使用 FruitRepository 的 Jakarta REST 资源。 使用以下内容创建 FruitResource

package org.acme.spring.data.jpa;

import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;

import java.util.List;
import java.util.Optional;

@Path("/fruits")
public class FruitResource {

    private final FruitRepository fruitRepository;

    public FruitResource(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    @GET
    public Iterable<Fruit> findAll() {
        return fruitRepository.findAll();
    }


    @DELETE
    @Path("{id}")
    public void delete(long id) {
        fruitRepository.deleteById(id);
    }

    @POST
    @Path("/name/{name}/color/{color}")
    public Fruit create(String name, String color) {
        return fruitRepository.save(new Fruit(name, color));
    }

    @PUT
    @Path("/id/{id}/color/{color}")
    public Fruit changeColor(Long id, String color) {
        Optional<Fruit> optional = fruitRepository.findById(id);
        if (optional.isPresent()) {
            Fruit fruit = optional.get();
            fruit.setColor(color);
            return fruitRepository.save(fruit);
        }

        throw new IllegalArgumentException("No Fruit with id " + id + " exists");
    }

    @GET
    @Path("/color/{color}")
    public List<Fruit> findByColor(String color) {
        return fruitRepository.findByColor(color);
    }
}

现在,FruitResource 提供了一些 REST 端点,可用于对 Fruit 执行 CRUD 操作。

Spring Web 注意事项

Jakarta REST 资源也可以替换为 Spring Web 控制器,因为 Quarkus 支持使用 Spring 控制器定义 REST 端点。 有关更多详细信息,请参见Spring Web 指南

更新测试

要测试 FruitRepository 的功能,请继续将 FruitResourceTest 的内容更新为

package org.acme.spring.data.jpa;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.core.IsNot.not;

@QuarkusTest
class FruitResourceTest {

    @Test
    void testListAllFruits() {
        //List all, should have all 3 fruits the database has initially:
        given()
                .when().get("/fruits")
                .then()
                .statusCode(200)
                .body(
                        containsString("Cherry"),
                        containsString("Apple"),
                        containsString("Banana")
                );

        //Delete the Cherry:
        given()
                .when().delete("/fruits/1")
                .then()
                .statusCode(204)
        ;

        //List all, cherry should be missing now:
        given()
                .when().get("/fruits")
                .then()
                .statusCode(200)
                .body(
                        not(containsString("Cherry")),
                        containsString("Apple"),
                        containsString("Banana")
                );

        //Create a new Fruit
        given()
                .when().post("/fruits/name/Orange/color/Orange")
                .then()
                .statusCode(200)
                .body(containsString("Orange"))
                .body("id", notNullValue())
                .extract().body().jsonPath().getString("id");

        //List all, Orange should be present now:
        given()
                .when().get("/fruits")
                .then()
                .statusCode(200)
                .body(
                        not(containsString("Cherry")),
                        containsString("Apple"),
                        containsString("Orange")
                );
    }

    @Test
    void testFindByColor() {
        //Find by color that no fruit has
        given()
                .when().get("/fruits/color/Black")
                .then()
                .statusCode(200)
                .body("size()", is(0));

        //Find by color that multiple fruits have
        given()
                .when().get("/fruits/color/Red")
                .then()
                .statusCode(200)
                .body(
                        containsString("Apple"),
                        containsString("Strawberry")
                );

        //Find by color that matches
        given()
                .when().get("/fruits/color/Green")
                .then()
                .statusCode(200)
                .body("size()", is(1))
                .body(containsString("Avocado"));

        //Update color of Avocado
        given()
                .when().put("/fruits/id/4/color/Black")
                .then()
                .statusCode(200)
                .body(containsString("Black"));

        //Find by color that Avocado now has
        given()
                .when().get("/fruits/color/Black")
                .then()
                .statusCode(200)
                .body("size()", is(1))
                .body(
                        containsString("Black"),
                        containsString("Avocado")
                );
    }

}

可以通过发出以下命令轻松运行测试

Maven
./mvnw test
Gradle
./gradlew test

打包并运行应用程序

Quarkus 开发模式与定义的仓库一起工作,就像使用任何其他 Quarkus 扩展一样,从而大大提高了开发周期内的生产力。 可以像往常一样使用以下命令在开发模式下启动应用程序

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

将应用程序作为本机二进制文件运行

当然,您可以按照本指南的说明创建一个本机可执行文件。

支持的 Spring Data JPA 功能

Quarkus 当前支持 Spring Data JPA 功能的一个子集,即最有用和最常用的功能。

这种支持的一个重要部分是,所有仓库生成都在构建时完成,从而确保所有支持的功能在本机模式下都能正常工作。 此外,开发人员在构建时就知道其仓库方法名称是否可以转换为正确的 JPQL 查询。 这也意味着,如果方法名称表明应该使用不属于实体的字段,则开发人员将在构建时收到相关错误。

支持的内容

以下各节描述了 Spring Data JPA 最重要的受支持功能。

自动仓库实现生成

扩展以下任何 Spring Data 仓库的接口都会自动实现

  • org.springframework.data.repository.Repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.ListCrudRepository

  • org.springframework.data.repository.PagingAndSortingRepository

  • org.springframework.data.repository.ListPagingAndSortingRepository

  • org.springframework.data.jpa.repository.JpaRepository

生成的仓库也注册为 Bean,因此可以将它们注入到任何其他 Bean 中。 此外,更新数据库的方法会自动使用 @Transactional 注释。

仓库定义的微调

这允许用户定义的仓库接口从任何受支持的 Spring Data 仓库接口中选择方法,而无需扩展这些接口。 例如,当仓库需要使用 CrudRepository 中的某些方法,但不希望公开所述接口的完整方法列表时,这尤其有用。

例如,假设 PersonRepository 不应扩展 CrudRepository,但希望使用 savefindById 方法,这些方法在所述接口中定义。 在这种情况下,PersonRepository 看起来像这样

package org.acme.spring.data.jpa;

import org.springframework.data.repository.Repository;

public interface PersonRepository extends Repository<Person, Long> {

    Person save(Person entity);

    Optional<Person> findById(Person entity);
}

使用仓库片段自定义单个仓库

可以使用其他功能来丰富仓库,或者覆盖受支持的 Spring Data 仓库的方法的默认实现。 最好的方法是用一个例子来说明。

仓库片段定义如下

public interface PersonFragment {

    // custom findAll
    List<Person> findAll();

    void makeNameUpperCase(Person person);
}

该片段的实现如下所示

import java.util.List;

import io.quarkus.hibernate.orm.panache.runtime.JpaOperations;

public class PersonFragmentImpl implements PersonFragment {

    @Override
    public List<Person> findAll() {
        // do something here
        return (List<Person>) JpaOperations.findAll(Person.class).list();
    }

    @Override
    public void makeNameUpperCase(Person person) {
        person.setName(person.getName().toUpperCase());
    }
}

然后,要使用的实际 PersonRepository 接口如下所示

public interface PersonRepository extends JpaRepository<Person, Long>, PersonFragment {

}

派生查询方法

可以自动实现遵循 Spring Data 约定的仓库接口的方法(除非它们属于稍后列出的不受支持的情况之一)。 这意味着以下方法都将起作用

public interface PersonRepository extends CrudRepository<Person, Long> {

    List<Person> findByName(String name);

    Person findByNameBySsn(String ssn);

    Optional<Person> findByNameBySsnIgnoreCase(String ssn);

    boolean existsBookByYearOfBirthBetween(Integer start, Integer end);

    List<Person> findByName(String name, Sort sort);

    Page<Person> findByNameOrderByJoined(String name, Pageable pageable);

    List<Person> findByNameOrderByAge(String name);

    List<Person> findByNameOrderByAgeDesc(String name, Pageable pageable);

    List<Person> findByAgeBetweenAndNameIsNotNull(int lowerAgeBound, int upperAgeBound);

    List<Person> findByAgeGreaterThanEqualOrderByAgeAsc(int age);

    List<Person> queryByJoinedIsAfter(Date date);

    Collection<Person> readByActiveTrueOrderByAgeDesc();

    Long countByActiveNot(boolean active);

    List<Person> findTop3ByActive(boolean active, Sort sort);

    Stream<Person> findPersonByNameAndSurnameAllIgnoreCase(String name, String surname);
}

用户定义的查询

包含在 @Query 注释中的用户提供的查询。 例如,以下所有方法都有效

public interface MovieRepository extends CrudRepository<Movie, Long> {

    Movie findFirstByOrderByDurationDesc();

    @Query("select m from Movie m where m.rating = ?1")
    Iterator<Movie> findByRating(String rating);

    @Query("from Movie where title = ?1")
    Movie findByTitle(String title);

    @Query("select m from Movie m where m.duration > :duration and m.rating = :rating")
    List<Movie> withRatingAndDurationLargerThan(@Param("duration") int duration, @Param("rating") String rating);

    @Query("from Movie where title like concat('%', ?1, '%')")
    List<Object[]> someFieldsWithTitleLike(String title, Sort sort);

    @Modifying
    @Query("delete from Movie where rating = :rating")
    void deleteByRating(@Param("rating") String rating);

    @Modifying
    @Query("delete from Movie where title like concat('%', ?1, '%')")
    Long deleteByTitleLike(String title);

    @Modifying
    @Query("update Movie m set m.rating = :newName where m.rating = :oldName")
    int changeRatingToNewName(@Param("newName") String newName, @Param("oldName") String oldName);

    @Modifying
    @Query("update Movie set rating = null where title =?1")
    void setRatingToNullForTitle(String title);

    @Query("from Movie order by length(title)")
    Slice<Movie> orderByTitleLength(Pageable pageable);
}

所有使用 @Modifying 注释的方法都将自动使用 @Transactional 注释。

在 Quarkus 中,当参数名称已编译为字节码时(默认情况下在生成的项目中处于活动状态),@Param 是可选的。

命名策略

Hibernate ORM 使用物理命名策略和隐式命名策略映射属性名称。 如果您希望使用 Spring Boot 的默认命名策略,则需要设置以下属性

quarkus.hibernate-orm.physical-naming-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
quarkus.hibernate-orm.implicit-naming-strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy

更多示例

可以在 集成测试 目录中看到大量的示例,该目录位于 Quarkus 源代码中。

当前不支持的内容

  • org.springframework.data.repository.query.QueryByExampleExecutor 接口的方法 - 如果调用其中任何一个方法,将抛出 Runtime 异常。

  • QueryDSL 支持。 不会尝试生成与 QueryDSL 相关的仓库的实现。

  • 使用 org.springframework.data.jpa.repository.JpaSpecificationExecutor

  • 自定义代码库中所有仓库接口的基础仓库。

    • 在 Spring Data JPA 中,这是通过注册一个扩展 org.springframework.data.jpa.repository.support.SimpleJpaRepository 的类来完成的,但是在 Quarkus 中,根本不使用此类(因为所有必要的管道都在构建时完成)。 未来可能会向 Quarkus 添加类似的支持。

  • 使用 java.util.concurrent.Future 和扩展它的类作为仓库方法的返回类型。

  • 使用 @Query 时的本机和命名查询

  • 实体状态检测策略 通过 EntityInformation

  • 使用 org.springframework.data.jpa.repository.Lock

Quarkus 团队正在探索各种替代方案,以弥合 JPA 和 Reactive 世界之间的差距。

重要的技术说明

请注意,Quarkus 中的 Spring 支持不会启动 Spring Application Context,也不会运行任何 Spring 基础结构类。 Spring 类和注释仅用于读取元数据,和/或用作用户代码方法的返回类型或参数类型。

相关内容