Simplified 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を使った場合と比べて、コードがどれだけコンパクトで読みやすくなったかお気づきでしょうか?これは面白いと思いませんか?読んでみてください。
the list() method might be surprising at first. It takes fragments of PanacheQL queries (subset of JPQL) and contextualizes the rest.
That makes for very concise but yet readable code.
MongoDB native queries are also supported.
|
what was described above is essentially the active record pattern, sometimes just called the entity pattern.
MongoDB with Panache also allows for the use of the more classical repository pattern via 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")
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プロパティーまたは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;
}
Annotating with @MongoEntity is optional. Here the entity will be stored in the ThePerson collection instead of the default Person collection.
|
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 persons by their ID
ObjectId personId1 = new ObjectId(id1AsString);
ObjectId personId2 = new ObjectId(id2AsString);
List<Person> person = Person.findByIds(List.of(personId1, personId2);
// 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());
A persistOrUpdate() method persists or updates an entity in the database, it uses the upsert capability of MongoDB to do it in a single query.
|
エンティティメソッドの追加
エンティティに対するカスタムクエリを、エンティティ自体の中に追加できます。そうすることで、自分や同僚が簡単に見つけることができ、クエリは操作するオブジェクトと一緒に配置されます。エンティティクラスにスタティックメソッドとして追加するのが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;
}
Annotating with @MongoEntity is optional. Here the entity will be stored in the ThePerson collection instead of the default Person collection.
|
MongoDB with Panacheでは、PojoCodecProvider を使用してエンティティーを MongoDB Document に変換します。
このマッピングをカスタマイズするために、以下のアノテーションを使用することができます。
-
@BsonId: IDフィールドをカスタマイズすることができます。Custom IDs を参照してください。 -
@BsonProperty: フィールドのシリアル化された名前をカスタマイズします。 -
@BsonIgnore: シリアル化の際にフィールドを無視することができます。
You can use public fields or private fields with getters/setters.
If you don’t want to manage the ID by yourself, you can make your entity extends 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 persons by their ID
ObjectId personId1 = new ObjectId(id1AsString);
ObjectId personId2 = new ObjectId(id2AsString);
List<Person> person = personRepository.findByIds(List.of(personId1, personId2);
// 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());
A persistOrUpdate() method persists or updates an entity in the database, it uses the upsert capability of MongoDB to do it in a single query.
|
| The rest of the documentation show usages based on the active record pattern only, but keep in mind that they can be performed with the repository pattern as well. The repository pattern examples have been omitted for brevity. |
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 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). |
Prior to Quarkus 3.16, when using $in with a list, you had to write your query with {'status':{$in: [?1]}}. Starting with Quarkus 3.16, make sure you use {'status':{$in: ?1}} instead. The list will be properly expanded with surrounding square brackets.
|
また、基本的な日付型の変換も行います。 Date, LocalDate, LocalDateTime もしくは Instant 型のすべてのフィールドは、 ISODate 型 (UTC datetime) を使って BSON Dateにマッピングされます。MongoDB POJO コーデックは 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 = Map.of("name", "Loïc", "status", Status.Alive);
Person.find("name = :name and status = :status", params);
Person.find("{'name': :name, 'status', :status}", params);
すべてのクエリー操作は、インデックス (Object…) または名前 (Map<String,Object>) でパラメーターを渡すことができます。
クエリパラメータを使う場合、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);
Using @BsonProperty is not needed to define custom column mappings, as the mappings from the entity class will be used.
|
You can have your projection class extends from another class. In this case, the parent class also needs to have use @ProjectionFor annotation.
|
| Records are a good fit for projection classes. |
クエリのデバッグ
MongoDB with Panacheではシンプルなクエリを書くことができますが、生成されたネイティブクエリをログに残しておくと、デバッグの際に便利なことがあります。
これは、 application.properties の中で以下のログカテゴリーを DEBUG に設定することで実現できます。
quarkus.log.category."io.quarkus.mongodb.panache.common.runtime".level=DEBUG
PojoCodecProvider: オブジェクトからBSONドキュメントへの変換を簡単に行うことができます。
MongoDB with Panache は、自動 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 persons by their ID
ObjectId personId1 = new ObjectId(id1AsString);
ObjectId personId2 = new ObjectId(id2AsString);
Uni<List<Person>> person = ReactivePerson.findByIds(List.of(personId1, personId2);
// 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 を実装したもの)を返します。
これにより、より高度なリアクティブユースケースが可能になります。たとえば、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) tells Quarkus REST to serialize the object in 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の開発サービス では、シングルノードのレプリカセットを設定するので、トランザクションと互換性があります。
| 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-junit-mockito モジュールを使用して Mockito を直接使用できます。これにより、Bean のモックがはるかに簡単になります。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit-mockito</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-junit-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 アプリケーションをビルドするのと同じプロジェクトであれば、すべて正常に動作します。
エンティティーが外部のプロジェクトやジャーから来ている場合は、空の 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));
}
}
}