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 リンク (ドキュメント) として表現する必要があるため、生の 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 clone https://github.com/quarkusio/quarkus-quickstarts.git で Git リポジトリーをクローンします。または、https://github.com/quarkusio/quarkus-quickstarts/archive/main.zip[アーカイブ] をダウンロードします。

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

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.19.4: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"

このコマンドは、Quarkus REST (旧称 RESTEasy Reactive) Jackson と MongoDB with Panache エクステンションをインポートする Maven 構造を生成します。 これで、 quarkus-mongodb-panache エクステンションがビルドファイルに追加されました。

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

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 のアノテーションでは次の設定が可能です。

  • マルチテナントアプリケーション用のクライアントの名前を指定することもできます。 Multiple MongoDB Clients を参照してください。それ以外の場合は、デフォルトのクライアントを使います。

  • データベースの名前、それ以外の場合は quarkus.mongodb.database プロパティーまたは MongoDatabaseResolver 実装が使用されます。

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

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フィールドをカスタマイズすることができます。Custom IDs を参照してください。

  • @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フィールドをカスタマイズすることができます。Custom IDs を参照してください。

  • @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リソースの書き方

まず、RESTEasy エクステンションの 1 つを組み込み、Jakarta REST エンドポイントを有効にします。たとえば、Jakarta REST および JSON サポートの場合は io.quarkus:quarkus-rest-jackson 依存関係を追加します。

そして、次のようなリソースを作成することで、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 ネイティブクエリと呼んでいます。

JPQL (または HQL) のサブセットとも言える PanacheQL もサポートしており、簡単にクエリーを表現できます。 MongoDB with Panacheは、それをMongoDBのネイティブクエリーにマッピングします。

{ で始まらないクエリは、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{'status':{$in: ?1}} にマッピングされます。

MongoDBのクエリは、有効なJSONドキュメントでなければなりません。同じフィールドをクエリ内で複数回使用することは、無効なJSONを生成することになるため、PanacheQLでは許可されません ( GitHub のこの問題を参照)。
Quarkus 3.16 より前のバージョンでは、リストのある $in を使用する場合、クエリーを {'status':{$in: [?1]}} のように記述する必要がありました。Quarkus 3.16 以降では、代わりに {'status':{$in: ?1}} を使用してください。リストは、適切に角括弧で囲まれて展開されます。

また、基本的な日付型の変換も行います。 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} は、更新ドキュメント {'$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 アノテーションを持つ必要があります。
レコードは射影クラスに適しています。

クエリのデバッグ

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

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

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

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

MongoDB with Panache は、自動 POJO サポート のある PojoCodecProvider を使用して、 オブジェクトを BSON ドキュメントに自動的に変換します。 このコーデックは Java レコードもサポートしているため、エンティティーまたはエンティティーの属性に使用できます。

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

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

トランザクション

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

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

@Transactional アノテーションが付けられたメソッド内では、必要に応じて Panache.getClientSession() を使用して ClientSession にアクセスできます。

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 が自動的に値を提供してくれますが、カスタムフィールド型を使う場合は、自分で値を提供する必要があります。

REST サービスでその値を公開する場合、 ObjectId の使用が困難になる可能性があります。 そこで、Quarkus REST Jackson エクステンションまたは Quarkus REST JSON-B エクステンションのいずれかに依存している場合は自動登録される String としてシリアル化/逆シリアル化するための Jackson および JSON-B プロバイダーを作成しました。

標準の 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()

@BsonCreator アプローチとは異なり、ここでは val は使用できません。 プロパティは var として定義する必要があります。そうしないと、システムはすべてのプロパティに対して null 値を持つオブジェクトを作成します。

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

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);
MongoDB を Panache とともに Quarkus REST と組み合わせて使用すると、Jakarta REST リソースエンドポイント内でリアクティブ型を直接返すことができます。

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

これにより、より高度なリアクティブユースケースが可能になります。たとえば、Quarkus REST 経由でサーバー送信イベント (SSE) を送信できます。

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) は、Quarkus REST にオブジェクトを JSON でシリアル化するように指示します。

リアクティブトランザクション

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

これらをリアクティブエンティティーまたはリポジトリーで使用するには、 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の開発サービス では、シングルノードのレプリカセットを設定するので、トランザクションと互換性があります。

Panache を使用した MongoDB 内のリアクティブトランザクションサポートはまだ実験段階です。

モック

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

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 は、コンパイル時のバイトコードエクステンションによってエンティティーを拡張します。 Quarkus アプリケーションをビルドするのと同じプロジェクトであれば、すべて正常に動作します。

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

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

マルチテナンシー

マルチテナンシーとは、単一のソフトウェアインスタンスが複数の異なるユーザーグループにサービスを提供できるソフトウェアアーキテクチャーです。サービスとしてのソフトウェア (SaaS) これらはマルチテナントアーキテクチャーの一例です " (Red Hat)。

MongoDB with Panache は現在、テナントごとのデータベースアプローチをサポートしています。これは、SQL データベースと比較すると、テナントごとのスキーマアプローチに似ています。

アプリケーションの記述

受信リクエストからテナントを解決し、特定のデータベースにマップするには、 io.quarkus.mongodb.panache.common.MongoDatabaseResolver インターフェイスの実装を作成する必要があります。

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 にします。

データベース選択の優先度は、 @MongoEntity(database="mizain")MongoDatabaseResolverquarkus.mongodb.database プロパティーの順です。

OIDC マルチテナンシー も使用している場合、OIDC のテナント ID と MongoDB データベースが同じであれば、次の例のように RoutingContext 属性から OIDC テナント ID にアクセスできます。

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

@RequestScoped
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;
    ...
    @Override
    public String resolve() {
        // OIDC has saved the tenant id as the RoutingContext attribute:
        return context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
    }
}

このエンティティーの場合:

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;
}

およびこのリソースの場合:

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();
    }
}

上記のクラスから、さまざまなデータベースから persons を永続化して取得するのに十分な情報が得られるため、どのように機能するかを確認できます。

アプリケーションの設定

すべてのテナントに同じ mongo 接続が使用されるため、テナントごとにデータベースを作成する必要があります。

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

テスト

テストは次のように記述できます。

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

関連コンテンツ