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アーカイブ をダウンロードします。

ソリューションは mongodb-panache-quickstart directory にあります。

Mavenプロジェクトの作成

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

CLI
quarkus create app org.acme:mongodb-panache-quickstart \
    --extension=resteasy-reactive-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:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=mongodb-panache-quickstart \
    -Dextensions="resteasy-reactive-jackson,mongodb-panache" \
    -DnoCode
cd mongodb-panache-quickstart

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

このコマンドは、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アノテーションプロセッサーを追加する必要があります:

pom.xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>${maven.compiler.parameters}</parameters>
        <annotationProcessorPaths>
            <!-- Your existing annotation processor(s)... -->
            <path>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-panache-common</artifactId>
                <version>${quarkus.platform.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
build.gradle
annotationProcessor("io.quarkus:quarkus-panache-common")

MongoDB with Panacheのセットアップと設定

始めるには

  • 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 プロパティが使用されます。

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

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 {

    @JsonProperty
    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 を拡張したエンティティを作ることができます。

リポジトリの定義

When using Repositories, you can get the exact same convenient methods as with the active record pattern, injected in your Repository, by making them implements PanacheMongoRepository:

@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_機能を使用して単一のクエリでそれを行います。
残りのドキュメントでは、アクティブレコードパターンに基づく使用法のみを示していますが、リポジトリパターンでも実行できることを覚えておいてください。リポジトリパターンの例は簡潔にするために省略しています。

JAX-RSリソースの記述

まず、JAX-RSエンドポイントを有効にするために、RESTEasyのエクステンションの1つを含めます。例えば、JAX-RSとJSONのサポートのために、 io.quarkus:quarkus-resteasy-reactive-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 queries must be valid JSON documents, using the same field multiple times in a query is not allowed using PanacheQL as it would generate an invalid JSON (see this issue on GitHub).

また、基本的な日付型の変換も行います。 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 offers operations to update multiple documents based on an update document and a query : Person.update("foo = ?1 and bar = ?2", fooName, barName).where("name = ?1", name).

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

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

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

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

  • {'firstname' : ?1 and 'status' : ?2} will be mapped to the update document {'$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}} will be used as-is

クエリパラメーター

ネイティブクエリと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.runtime".level=DEBUG

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

MongoDB with Panacheは、automatic POJO supportPojoCodecProviderを使用して、オブジェクトをBSONドキュメントに自動変換します。

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

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

トランザクション

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

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

MongoDB with Panache内部のトランザクションサポートはまだ実験的なものです。

カスタム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 は、REST サービスでその値を公開したい場合、使用するのが難しいことがあります。そこで、RESTEasy Jackson extensionまたはRESTEasy JSON-B extensionに依存するプロジェクトであれば自動的に登録される、 String としてシリアライズ/デシリアライズするJacksonおよびJSON-Bプロバイダを作成しました。

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

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

Kotlin Dataクラスの使用

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

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

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

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

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

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

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

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

これは、エンティティが 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-arg compiler 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 - an intuitive reactive programming libraryをご覧ください。

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);
RESTEasyでMongoDB with Panacheを併用している場合、 quarkus-resteasy-mutiny エクステンションを含めれば、JAX-RSリソースエンドポイント内でリアクティブ型を直接返すことができます。

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

これにより、より高度なリアクティブなユースケースが可能となり、例えば、RESTEasyを介してSSE(Server-Sent Event)を送信するために使用することができます。

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

@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<ReactivePerson> streamPersons() {
    return ReactivePerson.streamAll();
}
@SseElementType(MediaType.APPLICATION_JSON) がRESTEasyにJSONでオブジェクトをシリアライズするように指示します。
トランザクションは、Reactive エンティティおよびRepositoryではサポートされていません。

モック

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

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

  • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires an unnatural split between the state and its operations even though we would never do something like that for regular objects in the Object-Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’re writing to set up an injection point before coming back to use it.

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

  • MongoDB queries are JSON based, so you will need some String manipulation or using the Document type, and it will need a lot of boilerplate code.

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は、エンティティに対してコンパイル時にバイトコードを強化します。

これは、マーカーファイル META-INF/panache-archive.marker の存在によって、Panache エンティティを持つアーカイブ(および Panache エンティティのコンシューマー)を識別しようとするものです。Panacheには、Panacheに(間接的にでも)依存しているアーカイブでこのファイルを自動的に作成するアノテーションプロセッサが含まれています。アノテーションプロセッサを無効にしている場合、場合によってはこのファイルを手動で作成する必要があります。