Extension for Spring Data API

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

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

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

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

Creating the Maven project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

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

适用于 Windows 用户:

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

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令生成一个 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")

Define the Entity

在整个本指南的过程中,将使用以下 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;
    }
}

Configure database access properties

将以下属性添加到 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.database.generation=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

Prepare the data

为了更轻松地展示 Quarkus 上 Spring Data JPA 的一些功能,应该通过将以下内容添加到名为 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 的文件。

Define the repository

现在是时候定义将用于访问 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 ,其目的是返回符合指定颜色的所有水果实体。

Update the Jakarta REST resource

在使用仓库之后,下一项工作是创建将使用 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 操作。

Note on Spring Web

而 Jakarta REST 资源也可以替换为 Spring Web 控制器,因为 Quarkus 支持使用 Spring 控制器来定义 REST 端点。有关更多详情,请参阅 Spring Web guide

Update the test

要测试 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

Package and run the application

Quarkus 开发模式适用于已定义的存储库,就像适用于任何其他 Quarkus 扩展一样,大大提高了开发周期中的生产力。可以使用以下命令像平常一样在开发模式下启动应用程序:

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

Run the application as a native binary

您当然可以按照 this guide 的说明创建本机可执行文件。

Supported Spring Data JPA functionalities

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

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

What is supported

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

Automatic repository implementation generation

扩展以下任何 Spring Data 存储库的接口将自动实现:

  • org.springframework.data.repository.Repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.PagingAndSortingRepository

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

生成的存储库还注册为 Bean,以便可以注入到任何其他 Bean 中。此外,更新数据库的方法会自动使用 @Transactional 进行注释。

Fine-tuning of repository definition

这允许用户定义存储库接口从任何受支持的 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);
}

Customizing individual repositories using repository fragments

可以为存储库添加其他功能或覆盖受支持 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 {

}

Derived query methods

遵循 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);
}

User defined queries

@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 是可选的(在生成项目中默认处于激活状态)。

Naming Strategies

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

More examples

可以在 integration tests 目录中查看大量示例,该目录位于 Quarkus 源代码中。

What is currently unsupported

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

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

  • Using org.springframework.data.jpa.repository.JpaSpecificationExecutor

  • 为代码库中的所有资源库接口自定义基本资源库。

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

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

  • 在使用 @Query 时使用原生和已命名查询

  • Entity State-detection Strategies via EntityInformation.

  • The use of org.springframework.data.jpa.repository.Lock

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

Important Technical Note

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