The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Spring Data API のエクステンション

ユーザーにはリレーショナルデータベースアクセスに Hibernate ORM with Panache の使用が勧められていますが、Quarkus は spring-data-jpa エクステンションの形式で Spring Data JPA リポジトリーの互換性レイヤーを提供します。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 11+ がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.8.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Git リポジトリーのクローンを作成: git clone https://github.com/quarkusio/quarkus-quickstarts.git 、または アーカイブ をダウンロードします。

ソリューションは spring-data-jpa-quickstart ディレクトリー にあります。

Maven プロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:spring-data-jpa-quickstart \
    --extension='resteasy-reactive-jackson,spring-data-jpa,quarkus-jdbc-postgresql' \
    --no-code
cd spring-data-jpa-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法や使用方法については、<a href="cli-tooling.html">Quarkus CLIガイド</a> を参照してください。

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

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

このコマンドは、Maven プロジェクトを生成し、spring-data-jpa エクステンションをインポートします。

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに sspring-data-jpa エクステンションを追加することができます。

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

これにより、 pom.xml に以下が追加されます:

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 javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.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.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 を変更してください。

データの準備

Quarkus で Spring DataJ PA の一部の機能を簡単に示すために、次のコンテンツを 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 は、アプリケーションの起動時にこれらのクエリーを実行します。

リポジトリーの定義

次に、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 で使用できることを意味します。さらに、指定された色に一致するすべての Fruit エンティティーを返すことを目的とした findByColor が定義されています。

JAX-RS リソースの更新

リポジトリーを配置したら、次に FruitRepository を使用する JAX-RS リソースを作成します。次の内容で FruitResource を作成します。

package org.acme.spring.data.jpa;

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.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 は、Fruit で CRUD 操作を実行するために使用できるいくつかの REST エンドポイントを提供するようになりました。

Spring Web に関する注意

Quarkus は Spring コントローラーを使用した REST エンドポイント定義をサポートしているため、JAX-RS リソースを Spring Web コントローラーに置き換えることもできます。詳細については、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 エクステンションと同じように、定義済みリポジトリーで機能し、開発サイクル中の生産性を大幅に向上させます。アプリケーションは、通常どおり、以下を使用して開発モードで起動できます。

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

アプリケーションをネイティブバイナリーとして実行

もちろん、このガイド に記載された手順で、ネイティブ実行可能ファイルを作成することもできます。

サポートされている Spring Data JPA 機能

Quarkus は現在、Spring Data JPA の機能のサブセット、つまり最も有用で最も一般的に使用される機能をサポートしています。

このサポートで重要なのは、すべてのリポジトリー生成がビルド時に行われるため、サポートされているすべての機能がネイティブモードで正しく機能することを保証することです。さらに、開発者はビルド時に、リポジトリーメソッド名を適切な JPQL クエリーに変換できるかどうかを認識しています。これは、メソッド名がエンティティーの一部ではないフィールドを使用する必要があることを示している場合、開発者はビルド時に関連するエラーを受け取ることも意味します。

サポート対象

次のセクションでは、Spring Data JPA でサポートされている最も重要な機能について説明しています。

自動リポジトリー実装生成

次の SpringData リポジトリーのいずれかを拡張するインターフェイスが自動的に実装されます。

  • org.springframework.data.repository.Repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.PagingAndSortingRepository

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

生成されたリポジトリーも Bean として登録されるため、他の Bean に注入できます。さらに、データベースを更新するメソッドには、自動的に @Transactional のアノテーションが付けられます。

リポジトリー定義の微調整

これにより、ユーザー定義のリポジトリーインターフェイスは、インターフェイスを拡張することなく、サポートされている Spring Data リポジトリーインターフェイスのメソッドのみ選択できます。これは、たとえばリポジトリーが CrudRepository のいくつかのメソッドを使用する必要があるが、そのインターフェイスのメソッドの完全なリストを公開することが望ましくない場合に特に役立ちます。

たとえば PersonRepository は、CrudRepository を拡張するべきではないが、そのインターフェイスで定義されている save メソッドと findById メソッドを使用したいと想定します。この場合、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);
}

@Modified でアノテーションが付けられたすべてのメソッドには、自動的に`@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 インターフェイスのメソッド。そのいずれかが呼び出されると、ランタイム例外が出力されます。

  • QueryDSL のサポート。QueryDSL 関連リポジトリーの実装を生成する試みは行われません。

  • コードベース内のすべてのリポジトリーインターフェイスのベースリポジトリーをカスタマイズします。

    • Spring Data JPA では、これは org.springframework.data.jpa.repository.support.SimpleJpaRepository を拡張するクラスを登録することによって行われますが、Quarkus ではこのクラスはいっさい使用されません (必要な配置設定はすべてビルド時に行われるため)。将来、同様のサポートが Quarkus に追加される可能性があります。

  • java.util.concurrent.Future と、それをリポジトリーメソッドのリターンタイプとして拡張するクラスを使用します。

  • @Query を使用する場合のネイティブクエリーと名前付きクエリー

  • EntityInformation 経由の エンティティーステート検出戦略.

Quarkus チームは、JPA と Reactive の間のギャップを埋めるために、さまざまな代替案を模索しています。

重要な技術的注意点

Quarkus での Spring サポートは、Spring アプリケーションコンテキストを開始せず、Spring インフラストラクチャークラスも実行されないことに注意してください。Spring クラスとアノテーションは、メタデータの読み取りにのみ使用されるか、ユーザーコードメソッドのリターンタイプまたはパラメータータイプとして使用されます。