The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

シンプルになったMongoDB with Panache

MongoDB は広く利用されている有名な NoSQL データベースですが、エンティティとクエリを MongoDB Document として表現する必要があるため、生のAPIを使用するのは面倒です。

MongoDB with Panacheは、 Hibernate ORM with Panacheにあるようなアクティブレコードスタイルのエンティティ(およびリポジトリ)を提供し、Quarkusでエンティティを簡単に楽しく書けるようにすることに重点を置いています。

これは、 MongoDB Clientエクステンションの上に構築されています。

最初に:例

Panacheでは、MongoDBのエンティティをこのように書くことができます。

public class Person extends PanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteLoics(){
        delete("name", "Loïc");
    }
}

MongoDBのAPIを使った場合と比べて、コードがどれだけコンパクトで読みやすくなったかお気づきでしょうか?これは面白いと思いませんか?読んでみてください。

list() の方法は、最初は驚くかもしれません。これは、PanacheQLのクエリ(JPQLのサブセット)の断片を取り出し、残りの部分を文脈化したものです。これにより、非常に簡潔でありながら読みやすいコードになっています。MongoDBのネイティブクエリもサポートしています。
上で説明したものは基本的に アクティブレコードパターンで、単にエンティティパターンと呼ばれることもあります。MongoDB with Panacheでは、 PanacheMongoRepository を通じて、より古典的な リポジトリパターンを使うこともできます。

ソリューション

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

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

The solution is located in the mongodb-panache-quickstart directory.

Maven プロジェクトの作成

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

コマンドラインインタフェース
quarkus create app org.acme:mongodb-panache-quickstart \
    --extension='rest-jackson,mongodb-panache' \
    --no-code
cd mongodb-panache-quickstart

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

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.17.3:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=mongodb-panache-quickstart \
    -Dextensions='rest-jackson,mongodb-panache' \
    -DnoCode
cd mongodb-panache-quickstart

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

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=mongodb-panache-quickstart"

This command generates a Maven structure importing the Quarkus REST (formerly RESTEasy Reactive) Jackson and MongoDB with Panache extensions. After this, the quarkus-mongodb-panache extension has been added to your build file.

新しいプロジェクトを生成したくない場合は、ビルドファイルに依存関係を追加してください。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-mongodb-panache")

Panache による MongoDB のセットアップおよび設定

始めるには

  • application.properties に設定を追加する。

  • エンティティが PanacheMongoEntity を継承するようにする(リポジトリパターンを使用している場合はオプション)。

  • オプションとして、 @MongoEntity アノテーションを使用して、コレクションの名前、データベースの名前、またはクライアントの名前を指定します。

そして、application.properties に関連する設定プロパティーを追加します。

# configure the MongoDB client for a replica set of two nodes
quarkus.mongodb.connection-string = mongodb://mongo1:27017,mongo2:27017
# mandatory if you don't specify the name of the database using @MongoEntity
quarkus.mongodb.database = person

quarkus.mongodb.database プロパティは、MongoDB with Panache でエンティティを永続化するデータベースの名前を決定するのに使われます( @MongoEntity でオーバーライドされていない場合)。

@MongoEntity のアノテーションでは次の設定が可能です。

  • the name of the client for multitenant application, see Multiple MongoDB Clients. Otherwise, the default client will be used.

  • the name of the database, otherwise the quarkus.mongodb.database property or a MongoDatabaseResolver implementation will be used.

  • コレクションの名前。そうでない場合はクラスのシンプルな名前が使われます。

MongoDBクライアントの高度な設定については、 Configuring the MongoDB database ガイドに従ってください。

解決策1:アクティブレコードパターンを使用する

エンティティの定義

Panacheのエンティティを定義するには、 PanacheMongoEntity を拡張して、カラムをパブリックフィールドとして追加するだけです。コレクション、データベース、またはクライアントの名前をカスタマイズする必要がある場合は、 @MongoEntity アノテーションをエンティティに追加することができます。

@MongoEntity(collection="ThePerson")
public class Person extends PanacheMongoEntity {
    public String name;

    // will be persisted as a 'birth' field in MongoDB
    @BsonProperty("birth")
    public LocalDate birthDate;

    public Status status;
}
@MongoEntity でのアノテーションはオプションです。ここでは、エンティティは、デフォルトの Person コレクションではなく、 ThePerson コレクションに保存されます。

MongoDB with Panacheでは、PojoCodecProvider を使用してエンティティを MongoDB Document に変換します。

このマッピングをカスタマイズするために、以下のアノテーションを使用することができます。

  • @BsonId: IDフィールドをカスタマイズすることができます。「 カスタムID」を参照してください。

  • @BsonProperty: フィールドのシリアル化された名前をカスタマイズします。

  • @BsonIgnore: シリアル化の際にフィールドを無視することができます。

アクセサを書く必要がある場合は、以下のようにできます。

public class Person extends PanacheMongoEntity {

    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

また、当社のフィールドアクセスリライトのおかげで、ユーザーが person.name を読むときには、実際に getName() アクセサが呼び出されます。これはフィールドの書き込みやセッターについても同様です。これにより、すべてのフィールドの呼び出しが、対応するゲッター/セッターの呼び出しに置き換えられるため、実行時に適切なカプセル化が可能になります。

最も便利な操作

エンティティを記述したら、ここでは実行できる最も一般的な操作を紹介します。

// creating a person
Person person = new Person();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver
person.persist();

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
person.update();

// delete it
person.delete();

// getting a list of all Person entities
List<Person> allPersons = Person.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
person = Person.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = Person.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = Person.list("status", Status.Alive);

// counting all persons
long countAll = Person.count();

// counting all living persons
long countAlive = Person.count("status", Status.Alive);

// delete all living persons
Person.delete("status", Status.Alive);

// delete all persons
Person.deleteAll();

// delete by id
boolean deleted = Person.deleteById(personId);

// set the name of all living persons to 'Mortal'
long updated = Person.update("name", "Mortal").where("status", Status.Alive);

すべての list メソッドは、同等の stream バージョンがあります。

Stream<Person> persons = Person.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());
persistOrUpdate() メソッドは、データベース内のエンティティを永続化または更新するために存在し、MongoDB の upsert 機能を使用して単一のクエリでそれを行います。

エンティティメソッドの追加

エンティティに対するカスタムクエリを、エンティティ自体の中に追加できます。そうすることで、自分や同僚が簡単に見つけることができ、クエリは操作するオブジェクトと一緒に配置されます。エンティティクラスにスタティックメソッドとして追加するのがPanache Active Recordのやり方です。

public class Person extends PanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteLoics(){
        delete("name", "Loïc");
    }
}

解決策2:リポジトリパターンの使用

エンティティの定義

エンティティは通常のPOJOとして定義することができます。コレクション、データベース、またはクライアントの名前をカスタマイズする必要がある場合は、 @MongoEntity アノテーションをエンティティに追加できます。

@MongoEntity(collection="ThePerson")
public class Person  {
    public ObjectId id; // used by MongoDB for the _id field
    public String name;
    public LocalDate birth;
    public Status status;
}
@MongoEntity でのアノテーションはオプションです。ここでは、エンティティは、デフォルトの Person コレクションではなく、 ThePerson コレクションに保存されます。

MongoDB with Panacheでは、 PojoCodecProvider を使用してエンティティを MongoDB Document に変換します。

このマッピングをカスタマイズするために、以下のアノテーションを使用することができます。

  • @BsonId: IDフィールドをカスタマイズすることができます。「 カスタムID」を参照してください。

  • @BsonProperty: フィールドのシリアル化された名前をカスタマイズします。

  • @BsonIgnore: シリアル化の際にフィールドを無視することができます。

ゲッターやセッターを使って、パブリックフィールドやプライベートフィールドを使うことができます。IDを自分で管理したくない場合は、 PanacheMongoEntity を拡張したエンティティを作ることができます。

リポジトリの定義

Repositories を使用する場合、それらが PanacheMongoRepository を実装するようにし、Repository に注入することで、アクティブレコードパターンと全く同じ便利なメソッドを取得することができます。

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepository<Person> {

   // put your custom logic here as instance methods

   public Person findByName(String name){
       return find("name", name).firstResult();
   }

   public List<Person> findAlive(){
       return list("status", Status.Alive);
   }

   public void deleteLoics(){
       delete("name", "Loïc");
  }
}

PanacheMongoEntityBase で定義されているすべての操作は、あなたのリポジトリで利用できます。そのため、これを使用することは、注入する必要があることを除けば、active record パターンを使用することとまったく同じです。

@Inject
PersonRepository personRepository;

@GET
public long count(){
    return personRepository.count();
}

最も便利な操作

リポジトリを書くことで実行可能な最も一般的な操作は以下の通りです。

// creating a person
Person person = new Person();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver
personRepository.persist(person);

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
personRepository.update(person);

// delete it
personRepository.delete(person);

// getting a list of all Person entities
List<Person> allPersons = personRepository.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
person = personRepository.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = personRepository.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = personRepository.list("status", Status.Alive);

// counting all persons
long countAll = personRepository.count();

// counting all living persons
long countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
personRepository.delete("status", Status.Alive);

// delete all persons
personRepository.deleteAll();

// delete by id
boolean deleted = personRepository.deleteById(personId);

// set the name of all living persons to 'Mortal'
long updated = personRepository.update("name", "Mortal").where("status", Status.Alive);

すべての list メソッドは、同等の stream バージョンがあります。

Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());
persistOrUpdate() メソッドは、データベース内のエンティティを永続化または更新するために存在し、MongoDB の upsert 機能を使用して単一のクエリでそれを行います。
残りのドキュメントでは、アクティブレコードパターンに基づく使用法のみを示していますが、リポジトリパターンでも実行できることを覚えておいてください。リポジトリパターンの例は簡潔にするために省略しています。

Jakarta RESTリソースの書き方

First, include one of the RESTEasy extensions to enable Jakarta REST endpoints, for example, add the io.quarkus:quarkus-rest-jackson dependency for Jakarta REST and JSON support.

そして、次のようなリソースを作成することで、Personエンティティの作成/読み取り/更新/削除が可能になります。

@Path("/persons")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {

    @GET
    public List<Person> list() {
        return Person.listAll();
    }

    @GET
    @Path("/{id}")
    public Person get(String id) {
        return Person.findById(new ObjectId(id));
    }

    @POST
    public Response create(Person person) {
        person.persist();
        return Response.created(URI.create("/persons/" + person.id)).build();
    }

    @PUT
    @Path("/{id}")
    public void update(String id, Person person) {
        person.update();
    }

    @DELETE
    @Path("/{id}")
    public void delete(String id) {
        Person person = Person.findById(new ObjectId(id));
        if(person == null) {
            throw new NotFoundException();
        }
        person.delete();
    }

    @GET
    @Path("/search/{name}")
    public Person search(String name) {
        return Person.findByName(name);
    }

    @GET
    @Path("/count")
    public Long count() {
        return Person.count();
    }
}

アドバンスドクエリー

ページング

list および stream メソッドは、コレクションに含まれるデータセットが十分に小さい場合にのみ使用してください。より大きなデータセットの場合は、同等の find メソッドを使用して、ページングが可能な PanacheQuery を返すことができます。

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));

// get the first page
List<Person> firstPage = livingPersons.list();

// get the second page
List<Person> secondPage = livingPersons.nextPage().list();

// get page 7
List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();

// get the number of pages
int numberOfPages = livingPersons.pageCount();

// get the total number of entities returned by this query without paging
int count = livingPersons.count();

// and you can chain methods of course
return Person.find("status", Status.Alive)
    .page(Page.ofSize(25))
    .nextPage()
    .stream()

PanacheQuery 型には、ページングやリターンストリームを処理するための他の多くのメソッドがあります。

ページではなく範囲を使用する

PanacheQuery では、範囲ベースのクエリも可能です。

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);

// get the range
List<Person> firstRange = livingPersons.list();

// to get the next range, you need to call range again
List<Person> secondRange = livingPersons.range(25, 49).list();

範囲とページを混在させることはできません。範囲を使用した場合、現在のページを持っていることに依存するすべてのメソッドは UnsupportedOperationException をスローします。page(Page) もしくは page(int, int) を使用してページングに切り換えられます。

ソート

クエリ文字列を受け付けるすべてのメソッドは、オプションで Sort パラメータも受け付けるので、ソートを抽象化することができます。

List<Person> persons = Person.list(Sort.by("name").and("birth"));

// and with more restrictions
List<Person> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

Sort クラスには、列を追加したり、ソート方向を指定したりするメソッドが豊富に用意されています。

シンプルなクエリ

通常、MongoDB のクエリは {'firstname': 'John', 'lastname':'Doe'} 形式です。これを MongoDB ネイティブクエリと呼んでいます。

You can use them if you want, but we also support what we call PanacheQL that can be seen as a subset of JPQL (or HQL) and allows you to easily express a query. MongoDB with Panache will then map it to a MongoDB native query.

{ で始まらないクエリは、PanacheQL クエリとみなします。

  • <singlePropertyName> (そしてシングルパラメーター) で、{'singleColumnName': '?1'} に展開されます。

  • <query>{<query>} に展開され、PanacheQL のクエリを MongoDB のネイティブクエリ形式にマッピングします。以下の演算子をサポートしており、対応するMongoDBの演算子にマッピングされます。'and', 'or' ('and' と 'or' の混合は現在サポートされていません)、 '=', '>', '>=', '<', '<=', '!=', 'is null', 'is not null', そして MongoDB $regex 演算子にマッピングされる 'like' (String と JavaScript の両方のパターンをサポートしています)。

クエリの例を紹介します。

  • firstname = ?1 and status = ?2{'firstname': ?1, 'status': ?2} にマッピングされます。

  • amount > ?1 and firstname != ?2{'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}} にマッピングされます。

  • lastname like ?1{'lastname': {'$regex': ?1}} にマッピングされます。これは MongoDB の正規表現をサポートするもので、SQL のようなパターンではないことに注意しましょう。

  • lastname is not null{'lastname':{'$exists': true}} にマッピングされます。

  • status in ?1 will be mapped to {'status':{$in: ?1}}

MongoDBのクエリは、有効なJSONドキュメントでなければなりません。同じフィールドをクエリ内で複数回使用することは、無効なJSONを生成することになるため、PanacheQLでは許可されません ( GitHub のこの問題を参照)。
Prior to Quarkus 3.16, when using $in with a list, you had to write your query with {'status':{$in: [?1]}}. Starting with Quarkus 3.16, make sure you use {'status':{$in: ?1}} instead. The list will be properly expanded with surrounding square brackets.

また、基本的な日付型の変換も行います。 Date, LocalDate, LocalDateTime もしくは Instant 型のすべてのフィールドは、 ISODate 型 (UTC datetime) を使って BSON Dateにマッピングされます。MongoDB POJO コーデックは ZonedDateTimeOffsetDateTime をサポートしていないので、使う前に変換しておく必要があります。

MongoDB with Panacheは、 Document クエリを提供することでMongoDBの拡張クエリもサポートしています。これはfind/list/stream/count/deleteメソッドでサポートされています。

MongoDB with Panache はアップデートドキュメントおよびクエリーに基づいて複数のドキュメントを更新する操作を提供します: Person.update("foo = ?1 and bar = ?2", fooName, barName).where("name = ?1", name)

これらの操作では、クエリを表現するのと同じように、更新文書を表現することができます。以下に例を示します。

  • <singlePropertyName>` (そしてシングルパラメーター) で、更新ドキュメント {'$set' : {'singleColumnName': '?1'} に展開されます。

  • firstname = ?1 and status = ?2 は、更新ドキュメント {'$set' : {'firstname': ?1, 'status': ?2}} にマッピングされます。

  • firstname = :firstname and status = :status は、更新ドキュメント {'$set' : {'firstname': :firstname, 'status': :status}} にマッピングされます。

  • {'firstname' : ?1 and 'status' : ?2} は、更新ドキュメント {'$set' : {'firstname': ?1, 'status': ?2}} にマップされます。

  • {'firstname' : :firstname and 'status' : :status} will be mapped to the update document {'$set' : {'firstname': :firstname, 'status': :status}}

  • {'$inc': {'cpt': ?1}} はそのまま使用されます。

クエリパラメーター

ネイティブクエリとPanacheQLクエリの両方で、以下のようにインデックス(1から始まる)ごとにクエリパラメータを渡すことができます。

Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive);
Person.find("{'name': ?1, 'status': ?2}", "Loïc", Status.Alive);

または、 Map を使った名前で、

Map<String, Object> params = new HashMap<>();
params.put("name", "Loïc");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);
Person.find("{'name': :name, 'status', :status}", params);

または便利なクラスである Parameters をそのまま使用するか、 Map を構築します。

// generate a Map
Person.find("name = :name and status = :status",
         Parameters.with("name", "Loïc").and("status", Status.Alive).map());

// use it as-is
Person.find("{'name': :name, 'status': :status}",
         Parameters.with("name", "Loïc").and("status", Status.Alive));

すべてのクエリ操作は、インデックス (Object…​) または名前 (Map<String,Object> または Parameters) でパラメータを渡すことができます。

クエリパラメータを使う場合、PanacheQLのクエリはObjectパラメータ名を参照しますが、ネイティブのクエリはMongoDBのフィールド名を参照するので注意が必要です。

次のようなエンティティを想像してみてください。

public class Person extends PanacheMongoEntity {
    @BsonProperty("lastname")
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByNameWithPanacheQLQuery(String name){
        return find("name", name).firstResult();
    }

    public static Person findByNameWithNativeQuery(String name){
        return find("{'lastname': ?1}", name).firstResult();
    }
}

findByNameWithPanacheQLQuery()findByNameWithNativeQuery() はどちらも同じ結果を返しますが、PanacheQL で書かれたクエリはエンティティのフィールド名 name を使用し、ネイティブクエリは MongoDB のフィールド名 lastname を使用します。

クエリの射影レコード

クエリの射影は、 find() のメソッドが返す PanacheQuery オブジェクトに対して project(Class) のメソッドで行うことができます。

これを使って、データベースから返されるフィールドを制限することができます。IDフィールドは常に返されますが、これをプロジェクションクラス内に含めることは必須ではありません。

そのためには、投影されたフィールドのみを含むクラス(POJO)を作成する必要があります。このPOJOには、 @ProjectionFor(Entity.class) でアノテーションを付ける必要があります。 Entity はエンティティ・クラスの名前です。プロジェクション・クラスのフィールド名(ゲッター)は、データベースから読み込まれるプロパティを制限するために使用されます。

射影は、PanacheQLとネイティブクエリの両方で行うことができます。

import io.quarkus.mongodb.panache.common.ProjectionFor;
import org.bson.codecs.pojo.annotations.BsonProperty;

// using public fields
@ProjectionFor(Person.class)
public class PersonName {
    public String name;
}

// using getters
@ProjectionFor(Person.class)
public class PersonNameWithGetter {
    private String name;

    public String getName(){
        return name;
    }

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

// only 'name' will be loaded from the database
PanacheQuery<PersonName> shortQuery = Person.find("status ", Status.Alive).project(PersonName.class);
PanacheQuery<PersonName> query = Person.find("'status': ?1", Status.Alive).project(PersonNameWithGetter.class);
PanacheQuery<PersonName> nativeQuery = Person.find("{'status': 'ALIVE'}", Status.Alive).project(PersonName.class);
エンティティクラスのマッピングが使用されるため、カスタムカラムマッピングを定義するために @BsonProperty を使用する必要はありません。
射影クラスが他のクラスを継承している場合があります。この場合、親クラスも @ProjectionFor アノテーションを持つ必要があります。
Records are a good fit for projection classes.

クエリのデバッグ

MongoDB with Panacheではシンプルなクエリを書くことができますが、生成されたネイティブクエリをログに残しておくと、デバッグの際に便利なことがあります。

これは、 application.properties の中で以下のログカテゴリーを DEBUG に設定することで実現できます。

quarkus.log.category."io.quarkus.mongodb.panache.common.runtime".level=DEBUG

PojoCodecProvider: オブジェクトからBSONドキュメントへの変換を簡単に行うことができます。

MongoDB with Panache uses the PojoCodecProvider, with automatic POJO support, to automatically convert your object to a BSON document. This codec also supports Java records so you can use them for your entities or an attribute of your entities.

org.bson.codecs.configuration.CodecConfigurationException の例外が発生した場合、コーデックがオブジェクトを自動的に変換できないことを意味します。このコーデックは、Java Bean 標準に準拠しているため、パブリックフィールドまたはゲッター/セッターを使用する POJO を正常に変換します。 @BsonIgnore を使用して、フィールドまたはゲッター/セッターをコーデックで無視することができます。

クラスがこれらの規則に従わない場合(例えば、 get で始まるがセッターではないメソッドを含む場合)、そのクラスにカスタムコーデックを提供することができます。あなたのカスタム・コーデックは、自動的に検出され、コーデック・レジストリに登録されます。詳しくは、 BSONコーデックの使用をご覧ください。

トランザクション

MongoDBは、バージョン4.0からACIDトランザクションを提供しています。

MongoDB with Panacheでこれらを使うには、トランザクションを開始するメソッドに @Transactional アノテーションを付ける必要があります。

Inside methods annotated with @Transactional you can access the ClientSession with Panache.getClientSession() if needed.

MongoDBでは、トランザクションはレプリカセットでのみ可能です。幸運なことに、 MongoDBの開発サービス では、シングルノードのレプリカセットを設定するので、トランザクションと互換性があります。

カスタムID

ID はしばしば微妙な問題です。MongoDBでは、IDは通常、 ObjectId 型でデータベースによって自動生成されます。MongoDB with Panacheでは、IDは org.bson.types.ObjectId 型の id というフィールドで定義されていますが、もしカスタマイズしたいのであれば、私たちがサポートします。

PanacheMongoEntity の代わりに PanacheMongoEntityBase を拡張することで、独自のID戦略を指定することができます。そして、 @BsonId でアノテーションを付けて、好きなIDをパブリック・フィールドとして宣言します。

@MongoEntity
public class Person extends PanacheMongoEntityBase {

    @BsonId
    public Integer myId;

    //...
}

リポジトリを使用している場合は、 PanacheMongoRepository の代わりに PanacheMongoRepositoryBase を拡張し、ID 型を追加の型パラメータとして指定することになります。

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepositoryBase<Person,Integer> {
    //...
}

ObjectId を使う場合は、MongoDB が自動的に値を提供してくれますが、カスタムフィールド型を使う場合は、自分で値を提供する必要があります。

ObjectId can be difficult to use if you want to expose its value in your REST service. So we created Jackson and JSON-B providers to serialize/deserialize them as a String which are automatically registered if your project depends on either the Quarkus REST Jackson extension or the Quarkus REST JSON-B extension.

標準の ObjectId ID型を使用する場合、識別子がパスパラメータから来ているときは、新しい ObjectId を作成してエンティティを取得することを忘れないでください。例えば以下のように行います。

@GET
@Path("/{id}")
public Person findById(String id) {
    return Person.findById(new ObjectId(id));
}

Kotlin のデータクラスで作業する

Kotlinのデータクラスは、データキャリアクラスを定義する非常に便利な方法であり、エンティティクラスを定義するのに適しています。

しかし、このクラスの型はいくつかの制限があります。また、生成時に初期化される全てのフィールドは nullable としてマークされ、生成されたコンストラクタは、データクラスのすべてのフィールドをパラメータとして持つ必要があります。

MongoDB with Panache は PojoCodecProvider を使用します。これは MongoDB のコーデックで、パラメータなしのコンストラクタの存在を義務付けています。

そのため、データクラスをエンティティクラスとして使用したい場合は、Kotlinに空のコンストラクタを生成させる方法が必要です。そのためには、クラスのすべてのフィールドにデフォルト値を用意する必要があります。Kotlinのドキュメントの次の文章で説明しています。

JVMでは、生成されたクラスがパラメータレス・コンストラクタを持つ必要がある場合、すべてのプロパティのデフォルト値を指定する必要があります(「コンストラクタ」を参照)。

何らかの理由で前述の解決策が受け入れられないと判断された場合、代替手段があります。

まず、BSON Codecを作成すると、Quarkusに自動的に登録され、 PojoCodecProvider の代わりに使用されます。ドキュメントのこの部分を参照してください。 BSONコーデックの使用

もうひとつの方法は、 @BsonCreator アノテーションを使用して、 PojoCodecProvider に Kotlin データクラスのデフォルトコンストラクタを使用するように指示することです。この場合、すべてのコンストラクタパラメータは @BsonProperty でアノテーションする必要があります。引数なしのコンストラクタの無いPOJOのサポート を参照してください。

これは、エンティティが PanacheMongoEntity ではなく PanacheMongoEntityBase を拡張している場合にのみ機能します。なぜなら、ID フィールドもコンストラクタに含める必要があるからです。

Kotlinのデータクラスとして定義された Person クラスの例は次のようになります。

data class Person @BsonCreator constructor (
    @BsonId var id: ObjectId,
    @BsonProperty("name") var name: String,
    @BsonProperty("birth") var birth: LocalDate,
    @BsonProperty("status") var status: Status
): PanacheMongoEntityBase()

ここでは、 var を使用していますが、 val も使用できることに注意してください。

簡潔にするために @BsonProperty("_id") の代わりに @BsonId のアノテーションを使用していますが、どちらを使用しても構いません。

最後の方法は、 no-argcompiler plugin を使うことです。このプラグインにはアノテーションのリストが設定されており、最終的にはアノテーションが設定されている各クラスのno-argsコンストラクタが生成されます。

MongoDB with Panache では、データクラスに @MongoEntity アノテーションを使用することができます。

@MongoEntity
data class Person (
    var name: String,
    var birth: LocalDate,
    var status: Status
): PanacheMongoEntity()

リアクティブエンティティとレポジトリー

MongoDB with Panacheでは、エンティティとリポジトリの両方でリアクティブスタイルの実装を使うことができます。そのためには、エンティティを定義するときには ReactivePanacheMongoEntity または ReactivePanacheMongoEntityBase を、リポジトリを定義するときには ReactivePanacheMongoRepository または ReactivePanacheMongoRepositoryBase を、それぞれ Reactive バリアントとして使用する必要があります。

Mutiny

MongoDB with PanacheのリアクティブAPIは、Mutinyのリアクティブ型を使用しています。Mutinyに慣れていない方は、 MUTINYによる非同期入門 をご覧ください。

Person クラスのリアクティブ・バリアントは以下のようになります。

public class ReactivePerson extends ReactivePanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

bsonアノテーション、カスタムID、PanacheQLなど、 リアクティブ バリアント内で 命令型 のバリアントと同じ機能を利用できますが、エンティティやリポジトリのメソッドはすべてリアクティブ型を返します。

リアクティブ バリアントを持つ命令型の例から、同等のメソッドを見てみましょう。

// creating a person
ReactivePerson person = new ReactivePerson();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver,
// and accessible when uni1 will be resolved
Uni<ReactivePerson> uni1 = person.persist();

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
Uni<ReactivePerson> uni2 = person.update();

// delete it
Uni<Void> uni3 = person.delete();

// getting a list of all persons
Uni<List<ReactivePerson>> allPersons = ReactivePerson.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
Uni<ReactivePerson> personById = ReactivePerson.findById(personId);

// finding a specific person by ID via an Optional
Uni<Optional<ReactivePerson>> optional = ReactivePerson.findByIdOptional(personId);
personById = optional.map(o -> o.orElseThrow(() -> new NotFoundException()));

// finding all living persons
Uni<List<ReactivePerson>> livingPersons = ReactivePerson.list("status", Status.Alive);

// counting all persons
Uni<Long> countAll = ReactivePerson.count();

// counting all living persons
Uni<Long> countAlive = ReactivePerson.count("status", Status.Alive);

// delete all living persons
Uni<Long>  deleteCount = ReactivePerson.delete("status", Status.Alive);

// delete all persons
deleteCount = ReactivePerson.deleteAll();

// delete by id
Uni<Boolean> deleted = ReactivePerson.deleteById(personId);

// set the name of all living persons to 'Mortal'
Uni<Long> updated = ReactivePerson.update("name", "Mortal").where("status", Status.Alive);
If you use MongoDB with Panache in conjunction with Quarkus REST, you can directly return a reactive type inside your Jakarta REST resource endpoint.

リアクティブ型に対しても同様の問い合わせ機能がありますが、 stream() メソッドの動作は異なります。 Stream の代わりに Multi (リアクティブストリーム Publisher を実装したもの)を返します。

It allows more advanced reactive use cases, for example, you can use it to send server-sent events (SSE) via Quarkus REST:

import org.jboss.resteasy.reactive.RestStreamElementType;
import org.reactivestreams.Publisher;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<ReactivePerson> streamPersons() {
    return ReactivePerson.streamAll();
}
@RestStreamElementType(MediaType.APPLICATION_JSON) tells Quarkus REST to serialize the object in JSON.

Reactive transactions

MongoDBは、バージョン4.0からACIDトランザクションを提供しています。

To use them with reactive entities or repositories you need to use io.quarkus.mongodb.panache.common.reactive.Panache.withTransaction().

@POST
public Uni<Response> addPerson(ReactiveTransactionPerson person) {
    return Panache.withTransaction(() -> person.persist().map(v -> {
        //the ID is populated before sending it to the database
        String id = person.id.toString();
        return Response.created(URI.create("/reactive-transaction/" + id)).build();
    }));
}

MongoDBでは、トランザクションはレプリカセットでのみ可能です。幸運なことに、 MongoDBの開発サービス では、シングルノードのレプリカセットを設定するので、トランザクションと互換性があります。

Reactive transaction support inside MongoDB with Panache is still experimental.

モック

アクティブ・レコード・パターンの使用

active-recordパターンを使用している場合、Mockitoはスタティック・メソッドのモックをサポートしていないため、直接使用することはできませんが、 quarkus-panache-mock モジュールを使用することで、Mockitoを使用して、あなた自身のメソッドを含む、提供されたすべてのスタティック・メソッドをモックすることができます。

この依存関係を pom.xml に追加してください。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-panache-mock</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-panache-mock")

このシンプルなエンティティがある場合に

public class Person extends PanacheMongoEntity {

    public String name;

    public static List<Person> findOrdered() {
        return findAll(Sort.by("lastname", "firstname")).list();
    }
}

モッキングテストはこのように書くことができます。

@QuarkusTest
public class PanacheFunctionalityTest {

    @Test
    public void testPanacheMocking() {
        PanacheMock.mock(Person.class);

        // Mocked classes always return a default value
        Assertions.assertEquals(0, Person.count());

        // Now let's specify the return value
        Mockito.when(Person.count()).thenReturn(23L);
        Assertions.assertEquals(23, Person.count());

        // Now let's change the return value
        Mockito.when(Person.count()).thenReturn(42L);
        Assertions.assertEquals(42, Person.count());

        // Now let's call the original method
        Mockito.when(Person.count()).thenCallRealMethod();
        Assertions.assertEquals(0, Person.count());

        // Check that we called it 4 times
        PanacheMock.verify(Person.class, Mockito.times(4)).count();(1)

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(Person.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, Person.findById(12L));
        Assertions.assertNull(Person.findById(42L));

        // Mock throwing
        Mockito.when(Person.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12L));

        // We can even mock your custom methods
        Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
        Assertions.assertTrue(Person.findOrdered().isEmpty());

        PanacheMock.verify(Person.class).findOrdered();
        PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
        PanacheMock.verifyNoMoreInteractions(Person.class);
    }
}
1 verify のメソッドを Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないと、どのモックオブジェクトを渡せばいいのかわかりません。

リポジトリパターンの使用

リポジトリパターンを使用している場合は、 quarkus-junit5-mockito モジュールを使用して、Mockito を直接使用することができます。これにより、ビーンのモッキングが非常に簡単になります。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-junit5-mockito")

このシンプルなエンティティがある場合に

public class Person {

    @BsonId
    public Long id;

    public String name;
}

そしてこのリポジトリ。

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepository<Person> {
    public List<Person> findOrdered() {
        return findAll(Sort.by("lastname", "firstname")).list();
    }
}

モッキングテストはこのように書くことができます。

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @Test
    public void testPanacheRepositoryMocking() throws Throwable {
        // Mocked classes always return a default value
        Assertions.assertEquals(0, personRepository.count());

        // Now let's specify the return value
        Mockito.when(personRepository.count()).thenReturn(23L);
        Assertions.assertEquals(23, personRepository.count());

        // Now let's change the return value
        Mockito.when(personRepository.count()).thenReturn(42L);
        Assertions.assertEquals(42, personRepository.count());

        // Now let's call the original method
        Mockito.when(personRepository.count()).thenCallRealMethod();
        Assertions.assertEquals(0, personRepository.count());

        // Check that we called it 4 times
        Mockito.verify(personRepository, Mockito.times(4)).count();

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(personRepository.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, personRepository.findById(12L));
        Assertions.assertNull(personRepository.findById(42L));

        // Mock throwing
        Mockito.when(personRepository.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12L));

        Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
        Assertions.assertTrue(personRepository.findOrdered().isEmpty());

        // We can even mock your custom methods
        Mockito.verify(personRepository).findOrdered();
        Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
        Mockito.verifyNoMoreInteractions(personRepository);
    }
}

MongoDB の API をシンプルにする方法と理由

MongoDBのエンティティを書くときには、以下のように、ユーザーが不本意ながら慣れてしまっている厄介なことがたくさんあります。

  • IDロジックの重複:ほとんどのエンティティにはIDが必要ですが、モデルとはあまり関係がないため、ほとんどの人はIDの設定方法を気にしません。

  • ダサいゲッターとセッター:Javaは言語でプロパティをサポートしていないので、フィールドに対して読み書きを行わなかったとしてもフィールドを作成し、そのフィールドのためにゲッターとセッターを生成しなければなりません。

  • オブジェクト指向アーキテクチャの通常のオブジェクトでは、ステートとメソッドが同じクラスにないことはあり得ないのに、伝統的なEEパターンでは、エンティティの定義(モデル)とそれに対する操作(DAOやリポジトリ)を分けることが推奨されており、実際にはステートとその操作を不自然に分ける必要があります。さらに、エンティティごとに2つのクラスが必要になり、エンティティの操作を行う必要があるDAOやRepositoryをインジェクションする必要があるため、編集フローが崩れ、書いているコードから抜けてインジェクションポイントを設定してから戻って使用しなければなりません。

  • MongoDBのクエリは非常に強力ですが、一般的な操作では冗長すぎて、すべてのパーツが必要ではない場合でもクエリを書かなければなりません。

  • MongoDB のクエリは JSON ベースなので、文字列の操作や Document 型を使う必要があり、多くのボイラープレートコードが必要になります。

Panacheでは、これらの問題に対して、定見に基づいたアプローチをとりました。

  • エンティティは PanacheMongoEntity を拡張するようにしてください:自動生成されるIDフィールドがあります。カスタムのID戦略が必要な場合は、代わりに PanacheMongoEntityBase を拡張して、IDを自分で処理することができます。

  • パブリックフィールドを使ってください。無駄なゲッターとセッターを無くせます。フードの下では、不足しているすべてのゲッターとセッターを生成し、これらのフィールドへのすべてのアクセスを、アクセサ・メソッドを使用するように書き換えます。この方法では、必要なときに 便利な アクセサを書くことができ、エンティティ・ユーザーがフィールド・アクセスを使用していても、それが使用されます。

  • アクティブレコードパターンの使用: アクティブレコードパターンでは、すべてのエンティティロジックをエンティティクラスのスタティックメソッドに置き、DAOを作りません。エンティティスーパークラスには、非常に便利なスタティックメソッドがたくさん用意されていますし、エンティティクラスに独自のメソッドを追加することもできます。 Person ユーザーは、 Person と入力するだけで、すべての操作を一か所で完了させることができます。

  • 必要のない部分を書かないようにしましょう: Person.find("order by name")Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive) 、さらには Person.find("name", "Loïc") のように書きましょう。

以上となります: Panacheを使えば、MongoDBがこれほどまでに整然としたものになるのです。

外部プロジェクトや jar でエンティティーを定義する

MongoDB with Panache relies on compile-time bytecode enhancements to your entities. If you define your entities in the same project where you build your Quarkus application, everything will work fine.

エンティティーが外部のプロジェクトやジャーから来ている場合は、空の META-INF/beans.xml ファイルを追加することで、jarがQuarkusアプリケーションライブラリのように扱われるようにすることができます。

これにより、Quarkusは、エンティティが現在のプロジェクトの内部にあるかのようにインデックスを作成し、バイトコード強化をすることができます。

マルチテナンシー

"Multitenancy is a software architecture where a single software instance can serve multiple, distinct user groups. Software-as-a-service (SaaS) offerings are an example of multitenant architecture." (Red Hat).

MongoDB with Panache currently supports the database per tenant approach, it’s similar to schema per tenant approach when compared to SQL databases.

アプリケーションの記述

In order to resolve the tenant from incoming requests and map it to a specific database, you must create an implementation of the io.quarkus.mongodb.panache.common.MongoDatabaseResolver interface.

import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.vertx.ext.web.RoutingContext;

@RequestScoped (1)
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;

    @Override
    public String resolve() {
        return context.request().getHeader("X-Tenant");
    }

}
1 Beanは、テナントの解決が入ってくるリクエストに依存するため @RequestScoped にします。

The database selection priority order is as follow: @MongoEntity(database="mizain"), MongoDatabaseResolver, and then quarkus.mongodb.database property.

If you also use OIDC multitenancy, then if the OIDC tenantID and MongoDB database are the same and must be extracted from the Vert.x RoutingContext you can pass the tenant id from the OIDC TenantResolver to the MongoDB with Panache MongoDatabaseResolver as a RoutingContext attribute, for example:

import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.vertx.ext.web.RoutingContext;

@RequestScoped
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;
    ...
    @Override
    public String resolve() {
        // OIDC TenantResolver has already calculated the tenant id and saved it as a RoutingContext `tenantId` attribute:
        return context.get("tenantId");
    }
}

Given this entity:

import org.bson.codecs.pojo.annotations.BsonId;
import io.quarkus.mongodb.panache.common.MongoEntity;

@MongoEntity(collection = "persons")
public class Person extends PanacheMongoEntityBase {
    @BsonId
    public Long id;
    public String firstname;
    public String lastname;
}

And this resource:

import java.net.URI;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/persons")
public class PersonResource {

    @GET
    @Path("/{id}")
    public Person getById(Long id) {
        return Person.findById(id);
    }

    @POST
    public Response create(Person person) {
        Person.persist(person);
        return Response.created(URI.create(String.format("/persons/%d", person.id))).build();
    }
}

From the classes above, we have enough to persist and fetch persons from different databases, so it’s possible to see how it works.

アプリケーションの設定

The same mongo connection will be used for all tenants, so a database has to be created for every tenant.

quarkus.mongodb.connection-string=mongodb://login:pass@mongo:27017
# The default database
quarkus.mongodb.database=sanjoka

テスト

You can write your test like this:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Objects;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.Method;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;

@QuarkusTest
public class PanacheMongoMultiTenancyTest {

    public static final String TENANT_HEADER_NAME = "X-Tenant";
    private static final String TENANT_1 = "Tenant1";
    private static final String TENANT_2 = "Tenant2";

    @Test
    public void testMongoDatabaseResolverUsingPersonResource() {
        Person person1 = new Person();
        person1.id = 1L;
        person1.firstname = "Pedro";
        person1.lastname = "Pereira";

        Person person2 = new Person();
        person2.id = 2L;
        person2.firstname = "Tibé";
        person2.lastname = "Venâncio";

        String endpoint = "/persons";

        // creating person 1
        Response createPerson1Response = callCreatePersonEndpoint(endpoint, TENANT_1, person1);
        assertResponse(createPerson1Response, 201);

        // checking person 1 creation
        Response getPerson1ByIdResponse = callGetPersonByIdEndpoint(endpoint, person1.id, TENANT_1);
        assertResponse(getPerson1ByIdResponse, 200, person1);

        // creating person 2
        Response createPerson2Response = callCreatePersonEndpoint(endpoint, TENANT_2, person2);
        assertResponse(createPerson2Response, 201);

        // checking person 2 creation
        Response getPerson2ByIdResponse = callGetPersonByIdEndpoint(endpoint, person2.id, TENANT_2);
        assertResponse(getPerson2ByIdResponse, 200, person2);
    }

    protected Response callCreatePersonEndpoint(String endpoint, String tenant, Object person) {
        return RestAssured.given()
                .header("Content-Type", "application/json")
                .header(TENANT_HEADER_NAME, tenant)
                .body(person)
                .post(endpoint)
                .andReturn();
    }

    private Response callGetPersonByIdEndpoint(String endpoint, Long resourceId, String tenant) {
        RequestSpecification request = RestAssured.given()
                .header("Content-Type", "application/json");

        if (Objects.nonNull(tenant) && !tenant.isBlank()) {
            request.header(TENANT_HEADER_NAME, tenant);
        }

        return request.when()
                .request(Method.GET, endpoint.concat("/{id}"), resourceId)
                .andReturn();
    }

    private void assertResponse(Response response, Integer expectedStatusCode) {
        assertResponse(response, expectedStatusCode, null);
    }

    private void assertResponse(Response response, Integer expectedStatusCode, Object expectedResponseBody) {
        assertEquals(expectedStatusCode, response.statusCode());
        if (Objects.nonNull(expectedResponseBody)) {
            assertTrue(EqualsBuilder.reflectionEquals(response.as(expectedResponseBody.getClass()), expectedResponseBody));
        }
    }
}

関連コンテンツ