シンプルになった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 プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
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.
新しいプロジェクトを生成したくない場合は、ビルドファイルに依存関係を追加してください。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
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 aMongoDatabaseResolver
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();
範囲とページを混在させることはできません。範囲を使用した場合、現在のページを持っていることに依存するすべてのメソッドは |
ソート
クエリ文字列を受け付けるすべてのメソッドは、オプションで 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
は{'status':{$in: [?1]}}
にマッピングされます。
MongoDBのクエリは、有効なJSONドキュメントでなければなりません。同じフィールドをクエリ内で複数回使用することは、無効なJSONを生成することになるため、PanacheQLでは許可されません ( GitHub のこの問題を参照)。 |
また、基本的な日付型の変換も行います。 Date
, LocalDate
, LocalDateTime
もしくは Instant
型のすべてのフィールドは、 ISODate
型 (UTC datetime) を使って BSON Dateにマッピングされます。MongoDB POJO コーデックは ZonedDateTime
と OffsetDateTime
をサポートしていないので、使う前に変換しておく必要があります。
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 アノテーションを持つ必要があります。
|
Java 17+を実行する場合、レコードは投影クラスと相性が良いです。 |
クエリのデバッグ
MongoDB with Panacheではシンプルなクエリを書くことができますが、生成されたネイティブクエリをログに残しておくと、デバッグの際に便利なことがあります。
これは、 application.properties
の中で以下のログカテゴリーを DEBUG に設定することで実現できます。
quarkus.log.category."io.quarkus.mongodb.panache.common.runtime".level=DEBUG
PojoCodecProvider: オブジェクトからBSONドキュメントへの変換を簡単に行うことができます。
MongoDB with Panache は PojoCodecProvider と automatic POJO support で、オブジェクトから BSON ドキュメントに自動変換しています。
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
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.
標準の
|
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()
ここでは、 簡潔にするために |
最後の方法は、 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
に追加してください。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
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 を直接使用することができます。これにより、ビーンのモッキングが非常に簡単になります。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
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: |
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
|
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));
}
}
}