シンプルになった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 プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=mongodb-panache-quickstart"
このコマンドは、Quarkus REST (旧称 RESTEasy Reactive) Jackson と MongoDB with Panache エクステンションをインポートする Maven 構造を生成します。
これで、 quarkus-mongodb-panache
エクステンションがビルドファイルに追加されました。
新しいプロジェクトを生成したくない場合は、ビルドファイルに依存関係を追加してください。
<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
のアノテーションでは次の設定が可能です。
-
マルチテナントアプリケーション用のクライアントの名前を指定することもできます。 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();
範囲とページを混在させることはできません。範囲を使用した場合、現在のページを持っていることに依存するすべてのメソッドは |
ソート
クエリ文字列を受け付けるすべてのメソッドは、オプションで 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 コーデックは 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}
は、更新ドキュメント{'$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> {
//...
}
|
REST サービスでその値を公開する場合、 ObjectId
の使用が困難になる可能性があります。
そこで、Quarkus REST Jackson エクステンションまたは Quarkus REST JSON-B エクステンションのいずれかに依存している場合は自動登録される String
としてシリアル化/逆シリアル化するための Jackson および JSON-B プロバイダーを作成しました。
標準の
|
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()
@BsonCreator アプローチとは異なり、ここでは |
リアクティブエンティティとレポジトリー
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
に追加してください。
<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 は、コンパイル時のバイトコードエクステンションによってエンティティーを拡張します。 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 にします。 |
データベース選択の優先度は、 |
OIDC マルチテナンシー も使用している場合、OIDC のテナント ID と MongoDB
データベースが同じであれば、次の例のように
|
このエンティティーの場合:
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));
}
}
}