Quarkus で Panache を使ったアプリケーション作成

f:id:Naotsugu:20191009214703p:plain



はじめに

前回 Quarkus で JPA を使った簡単なアプリケーション作成を行いました。

blog1.mammb.com

今回は、Panache を使ったデータベースアクセスを見ていきます。



Panache とは

Panache は Quarkus に含まれ、簡易的なORMソリューションを提供します。

背後で Hibernate などの ORM を利用しますが、利用側からは ActiveRecord として扱えます。


Hibernate などの ORM は高度な機能を提供しますが、単純で一般的な利用用途でも定義が複雑化することがあります。

Panache は、このような単純な利用において、Entity の作成をより簡単にすることを目的としています。


Panache による Entity の定義は以下のようになります。

@Entity
public class Person extends PanacheEntity {
    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 deleteStefs(){
        delete("name", "Stef");
    }
}


  • Entity の ID は親クラスで定義されるため不要
    • カスタムID戦略が必要な場合は PanacheEntityBaseを拡張して定義する
  • パブリックフィールドを使うことで getter/setter が不要
    • getter / setter は自動生成され、呼び出し元も getter / setter コールに変換される
    • 独自の getter / setter を定義することも可能
  • スーパークラス PanacheEntity に定義された多くの静的メソッドで Entigy 操作が可能
  • Person.find("order by name")Person.find("name", "stef") のように必要のないクエリを記載する必要がない



Panache の利用準備

プロジェクトの作成や application.properties の定義などは前回記事を参照とします。

差分として build.gradle を以下のように変更します。

plugins {
    id 'java'
    id 'io.quarkus' version '0.23.2'
}

repositories {
    jcenter()
}

dependencies {
    implementation enforcedPlatform('io.quarkus:quarkus-bom:0.23.2')
    implementation 'io.quarkus:quarkus-resteasy-jsonb'
    implementation 'io.quarkus:quarkus-hibernate-orm-panache'
    implementation 'io.quarkus:quarkus-jdbc-postgresql'
}

io.quarkus:quarkus-hibernate-ormio.quarkus:quarkus-hibernate-orm-panache に変更するだけです。



Entity の定義

PanacheEntity を継承し、フィールドを public 宣言します。

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;
}


Entity は以下のように利用できます。

Person person = new Person();
person.name = "Stef";
person.persist();

person.name というフィールドへのアクセスは、コンパイル時に getter/setter へ変換されるため実行時にはカプセル化されます。


getter/setter は以下のように自身で定義すれば、そちらが利用されます。

@Entity
public class Person extends PanacheEntity {

    // ...

    public String getName(){
        return name.toUpperCase();
    }

    public void setName(String name){
        this.name = name.toLowerCase();
    }
}



Entity の操作

PanacheEntity に定義された静的メソッドにより Entity の操作ができます。


永続化
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

person.persist();


IDによる検索
person = Person.findById(personId);


一覧取得
List<Person> allPersons = Person.listAll();

List<Person> livingPersons = Person.list("status", Status.Alive);


ソート
List<Person> persons = Person.list(Sort.by("name").and("birth"));


件数取得
long countAll = Person.count();

long countAlive = Person.count("status", Status.Alive);


削除
if (person.isPersistent()) {
    person.delete();
}

Person.delete("status", Status.Alive);

Person.deleteAll();


Stream 操作
Stream<Person> persons = Person.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase())
    .filter(n -> ! "emmanuel".equals(n))
    .collect(Collectors.toList());



ページング

ページングを行うには find メソッドでクエリを作成し、このクエリを操作します。

PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);
livingPersons.page(Page.ofSize(25));

PanacheQuery を生成し、1ページの件数を定義します。


ページの取得
List<Person> firstPage = livingPersons.list();

List<Person> secondPage = livingPersons.nextPage().list();

List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();


件数の取得
int numberOfPages = livingPersons.pageCount();

int count = livingPersons.count();


メソッドチェーン
return Person.find("status", Status.Alive)
    .page(Page.ofSize(25))
    .nextPage()
    .stream();



クエリー

find メソッドで以下のようにクエリーを指定することができます。

Order.find("select distinct p from Person p left join fetch p.attributes");

from キーワードから始めることで HQL を使うこともできます。


クエリパラメータは以下のようなプレースホルダで指定することができます。

Person.find("name = ?1 and status = ?2", "stef", Status.Alive);


Parameters を指定してパラメータを構築して指定することができます。

Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive));


またはマップとして指定することもできます。

Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);



Entity メソッド

Panache では、Entity に対する操作は、対象の Entity に静的メソッドを追加して行うことを推奨しています。

以下のように静的メソッドを追加します。

@Entity
public class Person extends PanacheEntity {

    // ...

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }
}



トランザクション

RESTエンドポイントコントローラーのようなアプリケーションエントリポイントの境界でトランザクションを定義することが推奨されています。

@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {

    @POST
    @Path("/")
    @Transactional
    public Long create(Person person) {
        person.persist();
        return person.id;
    }
}


アプリケーションサービスを定義する場合は以下のようになるでしょう。

@ApplicationScoped
public class PersonService {

    @Transactional
    public Long create(String name) {
        Person person = new Person();
        person.name = name;
        person.persist();
        return person.id;
    }
}



ロック

Panache では直接データベースのロックをサポートしませんが、Panache.getEntityManager() で取得した EntityManager を使ってロックを行うことができます。

@Entity
public class Person extends PanacheEntity {

    // ...

    public static Person findByIdForUpdate(Long id){
        EntityManager entityManager = Panache.getEntityManager();
        Person person = findById(id);
        entityManager.lock(person, LockModeType.PESSIMISTIC_WRITE);
        return person;
    }
}



カスタムIDの利用

PanacheEntity は以下のような定義になっています。

@MappedSuperclass
public abstract class PanacheEntity extends PanacheEntityBase {
    @Id
    @GeneratedValue
    public Long id;
}


Entity 固有のカスタムIDを定義するには PanacheEntity の代わりに PanacheEntityBase を継承します。

@Entity
public class Person extends PanacheEntityBase {

    @Id
    @SequenceGenerator(
            name = "personSequence",
            sequenceName = "person_id_seq",
            allocationSize = 1,
            initialValue = 4)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
    public Integer id;

    //...
}



リポジトリとしての利用

Panache では推奨していませんが、PanacheRepository を実装することで Repository を利用することもできます。

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   public Person findByName(String name){
       return find("name", name).firstResult();
   }

   public List<Person> findAlive(){
       return list("status", Status.Alive);
   }

   public void deleteStefs(){
       delete("name", "Stef");
  }
}

Spring Data 風ですね。


以下のように EntityManager を使った処理もできます。

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

    @Inject
    EntityManager entityManager;

    public Person findByIdForUpdate(Long id){
        Person person = findById(id);
        entityManager.lock(person, LockModeType.PESSIMISTIC_WRITE);
        return person;
    }
}



PanacheEntity の静的メソッド一覧


Persist 系

メソッド
void persist()
void persistAndFlush()
void persist(Iterable<?> entities)
void persist(Stream<?> entities)
void persist(Object firstEntity, Object... entities)


Delete 系

メソッド
void delete()
long deleteAll()
long delete(String query, Object... params)
long delete(String query, Map<String, Object> params)
long delete(String query, Parameters params)


PanacheQuery 系

メソッド
PanacheQuery findAll()
PanacheQuery findAll(Sort sort)
PanacheQuery find(String query, Object... params)
PanacheQuery find(String query, Sort sort, Object... params)
PanacheQuery find(String query, Map<String, Object> params)
PanacheQuery find(String query, Sort sort, Map<String, Object> params)
PanacheQuery find(String query, Parameters params)
PanacheQuery find(String query, Sort sort, Parameters params)


List系

メソッド
List listAll()
List listAll(Sort sort)
List list(String query, Object... params)
List list(String query, Sort sort, Object... params)
List list(String query, Map<String, Object> params)
List list(String query, Sort sort, Map<String, Object> params)
List list(String query, Parameters params)
List list(String query, Sort sort, Parameters params)


Stream系

メソッド
Stream streamAll()
Stream streamAll(Sort sort)
Stream stream(String query, Object... params)
Stream stream(String query, Sort sort, Object... params)
Stream stream(String query, Map<String, Object> params)
Stream stream(String query, Sort sort, Map<String, Object> params)
Stream stream(String query, Parameters params)
Stream stream(String query, Sort sort, Parameters params)


Count 系

メソッド
long count()
long count(String query, Object... params)
long count(String query, Map<String, Object> params)
long count(String query, Parameters params)

その他

メソッド
T findById(Object id)
void flush()
boolean isPersistent()



マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

  • 作者:Sam Newman
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2016/02/26
  • メディア: 単行本(ソフトカバー)

クラウドネイティブ、マイクロサービス対応 最新システム構築実践ガイド

クラウドネイティブ、マイクロサービス対応 最新システム構築実践ガイド

  • 作者:
  • 出版社/メーカー: 日経BP
  • 発売日: 2018/06/19
  • メディア: Kindle版