40 リポジトリAPIの使用

CoherenceリポジトリAPIは、Coherenceで管理されているデータにアクセスするための、より高レベルでDDDに適した方法を提供します。

CoherenceリポジトリAPIを使用すると、Coherenceをデータ・ストアとして使用するアプリケーションの実装に使用するフレームワークに関係なく、アプリケーション内のデータ・アクセス・レイヤーの実装が容易になります。プレーンなJavaアプリケーションや、CDIを使用するアプリケーションの場合も同様に機能し、独自のリポジトリ実装を簡単に作成できます。

また、Micronaut Data (Micronaut Data with Coherenceを参照)およびSpring Dataリポジトリ実装の基盤でもあります。この章で説明するすべての機能は、これらのフレームワークを使用する際にも使用できます。唯一の違いは、独自のリポジトリを定義する方法です。これはフレームワーク固有であり、個別に文書化されています。

リポジトリAPIは、NamedMap APIの上に実装され、Coherenceがキーと値のデータ・ストアとして使用される多くの一般的なユース・ケースで簡単に使用できる多くの機能を提供します。

この章の内容は次のとおりです。

機能および利点

基本的なCRUD (作成、読取り、更新、削除)機能に加えて、リポジトリAPIには一般的なデータ管理タスクを簡素化する多くの機能があります。

その機能の包括的なリストを次に示します。
  • 強力な予測機能
  • 柔軟なインプレース・エンティティ更新
  • 高水準のデータ集計のサポート
  • ストリームAPIのサポート
  • 非同期APIのサポート
  • イベント・リスナーのサポート
  • 宣言的高速化および索引作成
  • CDI (Contexts and Dependency Injection)のサポート

リポジトリの実装

Coherenceには、抽象ベース・クラスcom.oracle.coherence.repository.AbstractRepositoryが用意されています。これは、カスタム・リポジトリ実装で3つの抽象メソッドを拡張および実装する必要があるためです。

    /**
     * Return the identifier of the specified entity instance.
     *
     * @param entity  the entity to get the identifier from
     *
     * @return the identifier of the specified entity instance
     */
    protected abstract ID getId(T entity);

    /**
     * Return the type of entities in this repository.
     *
     * @return the type of entities in this repository
     */
    protected abstract Class<? extends T> getEntityType();

    /**
     * Return the map that is used as the underlying entity store.
     *
     * @return the map that is used as the underlying entity store
     */
    protected abstract M getMap();
たとえば、String識別子を使用してPersonエンティティを格納するために使用できるリポジトリ実装は、次のように単純にすることができます。
public class PeopleRepository
        extends AbstractRepository<String, Person>
    {
    private NamedMap<String, Person> people;

    public PeopleRepository(NamedMap<String, Person> people)
        {
        this.people = people;
        }

    protected NamedMap<String, Person> getMap()                    
        {
        return people;
        }

    protected String getId(Person person)                              
        {
        return person.getSsn();
        }

    protected Class<? extends Person> getEntityType()        
        {
        return Person.class;
        }
    }
ここで:
  • getMapメソッドは、リポジトリのバッキング・データ・ストアとして使用する必要があるNamedMapを返します。この場合、コンストラクタ引数を使用して指定します。ただし、CDI (Contexts and Dependency Injection)を使用して簡単に注入できます。
  • getIdメソッドは、指定されたエンティティの識別子を返します。
  • getEntityTypeメソッドは、リポジトリに格納されているエンティティのクラスを返します。

前述の例のような簡単なリポジトリ実装では、拡張したAbstractRepositoryクラスによって提供されるすべてのリポジトリAPI機能にアクセスできます。

ただし、前述のリポジトリ・クラスにビジネス・メソッドを追加すれば、アプリケーション内で使いやすくなります。このようなメソッドの最も一般的な例は、アプリケーションに必要な様々な「ファインダ」メソッドです。たとえば、アプリケーションで名前に基づいて個人を見つけるために、頻繁にリポジトリに問い合せる必要がある場合、次に示すように、その目的のためのメソッドを追加できます。
public Collection<Person> findByName(String name)
        {
        Filter<Person> filter = Filters.like(Person::getFirstName, name)
                                    .or(Filters.like(Person::getLastName, name));
        return getAll(filter);
        }
次に、アプリケーション内でfindByNameメソッドを直接呼び出して、姓または名が文字'A'で始まるすべての人を検索できます。次に例を示します。
for (Person p : people.findByName("A%"))
    {
    // processing
    }

基本的なCRUD操作の実行

基本的な操作を使用して、リポジトリの追加、削除、更新および問合せを行うことができます

リポジトリに新しいエンティティを追加したり、既存のエンティティを置き換えるために、saveまたはsaveAllメソッドを使用できます。

saveメソッドは、単一のエンティティを引数として受け取り、それをバッキングNamedMapに格納します。
people.save(new Person("555-11-2222", "Aleks", 46));
saveAllメソッドを使用すると、エンティティのコレクションまたはストリームを引数として渡すことで、エンティティのバッチを格納できます。一部のエンティティがリポジトリに格納された後、getおよびgetAllメソッドを使用してリポジトリに問い合せることができます。
// gets a single person by identifier
Person person = people.get("555-11-2222");                                                        
assert person.getName().equals("Aleks");
assert person.getAge() == 46;

// fetches all the people from the repository
Collection<Person> allPeople = people.getAll();

// fetches all the people from the repository, that are aged 18 years or above.                                         
Collection<Person> allAdults = people.getAll(Filters.greaterOrEqual(Person::getAge, 18));

ソートされた結果を取得するには、getAllOrderedByメソッドをコールし、メソッド参照を使用してComparableプロパティを指定します。次の例の結果には、リポジトリにあるすべての個人の名前が、最年少から最年長まで年齢でソートして表示されます。

Collection<Person> peopleOrderedByAge = people.getAllOrderedBy(Person::getAge)
より複雑なユース・ケースの場合は、かわりに使用するComparatorを指定できます。たとえば、以前に使用したfindByNameメソッドの結果をソートする場合(リポジトリの実装を参照)、最初に姓、次に名でソートすると、次のように再実装できます。
public Collection<Person> findByName(String name)
        {
        Filter<Person> filter = Filters.like(Person::getFirstName, name)
                                    .or(Filters.like(Person::getLastName, name));
        return getAllOrderedBy(filter,
                               Remote.comparator(Person::getLastName)
                                     .thenComparing(Person::getFirstName));

この例では、標準のJava ComparatorのかわりにCoherence Remote.comparatorを使用して、指定したコンパレータがシリアライズ可能であり、リモート・クラスタ・メンバーに送信できるようにします。

最後に、リポジトリからエンティティを削除するには、いくつかのremoveメソッドのいずれかを使用できます。
// removes specified entity from the repository 
boolean fRemoved = people.remove(person);
// removes entity with the specified identifier from the repository
boolean fRemoved = people.removeById("111-22-3333");

前述のどちらの例でも、結果は、エンティティが実際にバッキングNamedMapから削除されたかどうかを示すブール値になり、エンティティがリポジトリに存在しなかった場合はfalseになることがあります。

削除した値自体に関心がある場合は、前述のメソッドのオーバーロードを使用して、それを表すことができます。
// removes specified entity from the repository and returns it as the result
Person removed = people.remove(person, true); 

// removes entity with the specified identifier from the repository and returns it as the result             
Person removed = people.removeById("111-22-3333", true);

ノート:

この削除によって、追加のネットワーク・トラフィックが発生します。したがって、削除したエンティティが本当に必要でないかぎり、そのエンティティを要求しないことをお薦めします。
前述の例は、リポジトリから単一のエンティティを削除する場合に便利です。単一のネットワーク呼出しの一部として複数のエンティティを削除する場合は、かわりにremoveAllメソッドのいずれかを使用する必要があります。これらのメソッドでは、識別子を明示的に指定するか、Filterを使用して削除する基準を指定することで、エンティティのセットを削除できます。
// removes all men from the repository and returns 'true' if any entity has been removed
boolean fChanged = people.removeAll(Filters.equal(Person::getGender, Gender.MALE));
// removes entities with the specified identifiers from the repository and returns 
// 'true' if any entity has been removed
boolean fChanged = people.removeAllById(Set.of("111-22-3333", "222-33-4444"));
単一エンティティの削除操作の場合と同様に、削除されたエンティティを結果として返すことができるオーバーロードを使用することもできます。
// removes the names of all men from the repository and returns the map of removed 
// entities, keyed by the identifier
Map<String, Person> mapRemoved =
        people.removeAll(Filters.equal(Person::getGender, Gender.MALE), true);
// removes the entities with the specified identifiers from the repository and returns
// the map of removed entities, keyed by the identifier
Map<String, Person> mapRemoved =
        people.removeAllById(Set.of("111-22-3333", "222-33-4444"), true);

サーバー側の予測の実行

いくつかの基準を満たすエンティティのコレクションについてリポジトリに問い合せることは確かに一般的で便利な操作ですが、エンティティ内のすべての属性を必要としない場合もあります。たとえば、個人名のみが必要な場合、Personインスタンスに含まれるすべての情報を問い合せて破棄することは不要であり、時間の無駄です。
これは、リレーショナル・データベースに対して次を実行することと同等です。
SELECT * FROM PEOPLE
次の単純なコマンドで十分です。
SELECT name FROM PEOPLE
CoherenceリポジトリAPIを使用すると、対象のエンティティ属性のサーバー側の予測を実行することによって収集されるデータの量を制限できます。たとえば、個人の名前のみが必要な場合、名前のみを取得できます。
// returns the name of the person with a specified identifier
String name  = people.get("111-22-3333", Person::getName); 

// returns the map of names of all the people younger than 18, keyed by the person’s identifier                
Map<String, String> mapNames =
        people.getAll(Filters.less(Person::getAge, 18), Person::getName);
明らかに、エンティティ全体を返すか、エンティティから1つの属性を返すかは両極端であり、多くの場合、その間に何かが必要です。たとえば、個人の名前と年齢が必要な場合があります。このような場合、Coherenceではフラグメントを使用できます。
// returns a fragment containing the name and age of the person with a specified identifier
Fragment<Person> fragment = people.get("111-22-3333",
                                       Extractors.fragment(Person::getName, Person::getAge));  

// retrieves the person’s name from a fragment
String name = fragment.get(Person::getName);

// retrieves the person’s age from a fragment
int age = fragment.get(Person::getAge);
getAllメソッドのいずれかを使用して、複数のエンティティ間で同じ予測を実行できます。
//returns a map of fragments containing the name and age of all the people younger than 18, keyed by the person’s identifier
Map<String, Fragment<Person>> fragments = people.getAll(
        Filters.less(Person::getAge, 18),
        Extractors.fragment(Person::getName, Person::getAge));
表の各行に一連の列を含むリレーショナル・データベースとは異なり、Coherenceは各エンティティを完全なオブジェクト・グラフとして格納します。つまり、属性は他のオブジェクト・グラフにすることができ、任意のレベルにネストできます。つまり、ネストされたオブジェクトの属性を予測することもできます。たとえば、Personクラスには、ネストされたAddressオブジェクトが属性として存在し、その属性にはstreetcityおよびcountry属性があります。リポジトリ内の個人の名前および国を取得する場合は、次の例を参照してください。
// returns a fragment containing the name and the 'Address' fragment of the person with a specified identifier       
Fragment<Person> person = people.get(
        "111-22-3333",
        Extractors.fragment(Person::getName,
                            Extractors.fragment(Person::getAddress, Address::getCountry)));  

// retrieves the person’s name from the 'Person' fragment
String name = person.get(Person::getName);

// retrievea the 'Address' fragment from the 'Person' fragment           
Fragment<Address> address = person.getFragment(Person::getAddress);

// retrieves the person’s country from the 'Address' fragment     
String country = address.get(Address::getCountry);

インプレース更新の実行

最新のアプリケーションでデータを更新するための最も一般的なアプローチは、読取り/変更/書込みパターンです。
たとえば、Personの属性を更新する一般的なコードは次のようになります。
Person person = people.get("111-22-3333");
person.setAge(55);
people.save(person);

これは、基礎となるデータ・ストアが、より適切で効率的なデータ更新方法を提供するかどうかに関係なく当てはまります。たとえば、RDBMSは、その目的のためにストアド・プロシージャを提供しますが、これは使いづらく、JPA、Spring Data、Micronaut Dataなどの一般的なアプリケーション・フレームワークに適合しないため、使用する開発者はほとんどいません。また、コード・ベースをある程度断片化し、ビジネス・ロジックをアプリケーションおよびデータ・ストアに分割し、いくつかのアプリケーション・コードをSQLで記述する必要があります。

ただし、前述の方法は、次の理由で最適ではありません。
  • アプリケーションがデータ・ストアに対して行うネットワーク呼出しの数が2倍になり、操作の全体的な待機時間が増加します。
  • 必要以上に多くの(大量になる場合がある)データをネットワーク上で移動します。
  • 単一属性の非常に単純な更新操作を実行するために、複雑なエンティティのコストがかかる構築が必要になる場合があります(これは特にJPAおよびRDBMSバックエンドの場合に当てはまります)。
  • データ・ストアに不要な負荷が加えられます。これは通常、アプリケーションのスケーリングが最も難しいコンポーネントです。
  • 同時実行性の問題(つまり、データ・ストア内のエンティティが初期読取りと後続の書込みの間に変更された場合の処理)が発生し、通常は読取りと書込みの両方が同じトランザクション内で発生することが必要になります。

更新を実行するためのはるかに優れた効率的な方法は、更新関数をデータ・ストアに送信し、データ・ストア自体の中でローカルに実行することです(ストアド・プロシージャはほとんどそのためにあります)。

Coherenceは、エントリ・プロセッサを介して常にこれらのタイプの更新をサポートしてきましたが、リポジトリAPIを使用すると、より簡単に行うことができます。たとえば、前述のコードを次のように書き換えることができます。
people.update("111-22-3333", Person::setAge, 55);

前述のコードは、引数として数値55を使用したsetAgeメソッドをコールして、指定された識別子でPersonインスタンスを更新するようにCoherenceに指示しています。このコードは非常に効率的であるだけでなく、より短く、読み書きも簡単になっています。

特定の識別子を持つPersonインスタンスがクラスタ内のどこにあるかを知ることは、重要ではありません。Coherenceは、指定されたIDを持つエンティティに対してプライマリ所有者でsetAgeメソッドを呼び出し、フォルト・トレランスのために変更されたエンティティのバックアップを自動的に作成することを保証します。

また、前述のアプローチでは、RDBMSでストアド・プロシージャが実行するのと同じメリットが得られますが、デメリットが無い、つまりJavaですべてのコードを記述し、同じ場所に保持するという点を指摘しておきます。このアプローチにより、データに対してリッチ・ドメイン・モデルを実装し、エンティティに対するビジネス・ロジックをリモートで実行できます。これは、DDDアプリケーションと非常にうまく連携しています。

エンティティに対するセッターをリモートで呼び出すことは、すべてのデータ変更のニーズには十分ではありません。たとえば、従来のJavaBeanセッターではvoidが返されますが、更新後のエンティティ値が何かを知りたいことがよくあります。その問題の解決は簡単です。Coherenceは指定されたメソッド呼出しの結果を返すため、setAgeメソッドを変更して適切なAPIを実装するだけで済みます。
public Person setAge(int age)
    {
    this.age = age;
    return this;
    }
update呼出しの結果として、変更されたPersonインスタンスが取得されます。
Person person = people.update("111-22-3333", Person::setAge, 55);
assert person.getAge() == 55;
より複雑な更新を実行したり、複数の属性を同時に更新する必要がある場合があります。複数の更新呼出しを行うことでこれらを実現できますが、各更新によって個別のネットワーク呼出しが発生するため、非効率的です。かわりに、その状況で実行する関数を指定できるupdateオーバーロードを使用できます。
Person person = people.update("111-22-3333", p ->
    {
    p.setAge(55);
    p.setGender(Gender.MALE);
    return p;
    });

assert person.getAge() == 55;
assert person.getGender() == Gender.MALE;

このようにして、実行される更新ロジックおよび戻り値を完全に制御できます。

リポジトリに存在しないエンティティを更新したい場合があります。この場合、新しいインスタンスを作成する必要があります。たとえば、顧客が最初のアイテムをカートに追加するときに、顧客のショッピング・カート・エンティティを作成できます。特定の顧客のCartが存在するかどうかを確認し、カートが存在しない場合は新しいものを作成するコードを実装できますが、Cart::addItemコールの一部としてCartインスタンスを作成するだけで回避できるネットワーク呼出しが結果として発生します。リポジトリAPIでは、オプションのEntityFactory引数を使用してこれを実行できます。

carts.update(customerId,          // the cart/customer identifier
             Cart::addItem,       // the method to invoke on a target 'Cart' instance   
             item,                // the 'CartItem' to add to the cart
             Cart::new            // the 'EntityFactory' to use to create a new 'Cart' instance if the 
                                  // cart with the specified identifier does not exist
             );
EntityFactoryインタフェースは単純です。
@FunctionalInterface
public interface EntityFactory<ID, T>
        extends Serializable
    {
    /**
     * Create an entity instance with the specified identity.
     *
     * @param id identifier to create entity instance with
     *
     * @return a created entity instance
     */
    T create(ID id);
    }
エンティティ識別子を受け入れ、指定された識別子を持つエンティティの新しいインスタンスを返す単一のcreateメソッドがあります。前述の例では、Cartクラスに次のようなコンストラクタがあることを示しています。
public Cart(Long cartId)
    {
    this.cartId = cartId;
    }
予測およびその他の操作と同様に、単一のエンティティの変更に使用できるupdateメソッドに加えて、1回の呼出しで複数のエンティティを変更するために使用できるupdateAllメソッドも多数あります。これが役立つ例としては、株式分割を実行する場合のように、まったく同じ関数を複数のエンティティに適用する場合です。
positions.updateAll(
        Filters.equal(Position::getSymbol, "AAPL"),       
        Position::split, 5);
前述のコード例で:
  • 最初の引数は、更新するポジションのセットを決定するために使用されるFilterです。
  • 2番目の引数は、各ポジションに適用する関数です。この場合、split(5)は、AAPL記号を持つ各Positionエンティティでコールされます。

単一エンティティの更新と同様に、各関数呼出しの結果がクライアントに返され、今回は、処理されたエンティティの識別子をキーとして含むMapの形式で、そのエンティティに適用された関数の結果が値として返されます。

ストリームAPIおよびデータ集計APIの使用

リポジトリに問い合せると、getAllメソッドおよびFilterを使用して、エンティティのサブセットを取得できますが、場合によっては、エンティティ自体ではなく、リポジトリ内のエンティティのサブセットに適用された一部の計算の結果が必要になることがあります。たとえば、部門内のすべての従業員の平均給与や、ポートフォリオ内のすべての株式ポジションの合計値を計算することが必要な場合があります。

処理する必要のあるエンティティについてリポジトリに問合せを実行し、クライアント上で処理自体を実行することは確かにできますが、これは、ネットワークを介して大量のデータを移動し、クライアント側の処理後に破棄するだけになる可能性があるため、タスクを達成するには非常に非効率な方法です。

Coherenceには、様々なタイプの分散処理を効率的に実行できる多数の機能があります。インプレース更新でCoherenceエントリ・プロセッサAPIを利用してデータを格納するクラスタ・メンバーでデータ変更を実行するのと同様に、リポジトリAPIのデータ集計のサポートでは、Coherenceリモート・ストリームAPIおよび集計APIを利用して、読取り専用分散計算を効率的に実行します。この機能を使用すると、処理をデータに移動したり(その逆ではなく)、クライアントに少数の(または多くの場合は1つのみの)コアではなく、クラスタに存在する数のCPUコアでパラレルに計算を実行できます。

最初のオプションは、ストリームAPIを使用することです。これは、Java 8で導入された標準のJava APIであるため、おそらくすでによく知られています。たとえば、すべての従業員の平均給与は次のように計算できます。
double avgSalary = employees.stream()
         .collect(RemoteCollectors.averagingDouble(Employee::getSalary));
特定の部門の従業員のみの平均給与を計算する場合は、処理する従業員の名前をフィルタできます。
double avgSalary = employees.stream()
         .filter(e -> e.getDepartmentId == departmentId)
         .collect(RemoteCollectors.averagingDouble(Employee::getSalary));

ただし、前述のコードは機能しますが、指定した部門に属するかどうかを判断するために、リポジトリ内のすべての従業員の名前を処理し、デシリアライズする可能性もあるため、理想的ではありません。

同じタスクを実行するためのより適切な方法は、Coherence固有のstreamメソッドのオーバーロードを使用することです。これにより、Filterを指定して次に基づいてストリームを作成できます。
double avgSalary = employees.stream(Filters.equal(Employee::getDepartmentId, departmentId))
         .collect(RemoteCollectors.averagingDouble(Employee::getSalary));

その違いは微妙ですが、重要です。前の例とは異なり、この例では、Coherenceはストリームを作成する前に問合せを実行し、プロセス内の索引を利用できます。これにより、大量のデータ・セットを処理する際のオーバーヘッドを大幅に削減できます。

ただし、同じことを実現するより簡単な方法もあります。
double avgSalary = employees.average(Employee::getSalary);
特定の部門の場合:
double avgSalary = employees.average(
        Filters.equal(Employee::getDepartmentId, departmentId),
        Employee::getSalary);

リポジトリ集計メソッドを直接使用する場合の例を次に示します。これにより、エンティティ属性のminmaxaverageおよびsumの検索などの一般的なタスクが単純になります。

groupBytopなど、より高度な集計もあります。
Map<Gender, Set<Person>> peopleByGender = people.groupBy(Person::getGender);

Map<Long, Double> avgSalaryByDept =
    employees.groupBy(Employee::getDepartmentId, averagingDouble(Employee::getSalary));

List<Double> top5salaries = employees.top(Employee::getSalary, 5);

単純なものは、countおよびdistinctです。

多くの場合、属性のminmaxまたはtop値だけでなく、これらの値が属するエンティティも必要になります。このような状況では、minBymaxByおよびtopByメソッドを使用して、属性の最小値、最大値および上位値を含むエンティティをそれぞれ返します。
Optional<Person> oldestPerson   = people.maxBy(Person::getAge);
Optional<Person> youngestPerson = people.minBy(Person::getAge);

List<Employee> highestPaidEmployees = employees.topBy(Employee::getSalary, 5);

宣言的高速化および索引作成の使用

Coherenceは、索引を使用して問合せおよび集計を最適化します。索引を使用すると、クラスタ全体に格納されているエンティティのデシリアライズを回避できます。これは、複雑なエンティティ・クラスを持つ大規模なデータ・セットがある場合、コストのかかる操作です。索引自体もソートできます。これは、lessgreaterbetweenなどの範囲ベースの問合せを実行する場合に役立ちます。

索引を作成する標準的な方法は、NamedMap.addIndexメソッドをコールすることです。ただし、リポジトリAPIでは、よりシンプルで宣言的な索引作成方法が導入されています。

索引を定義するには、索引を作成するエンティティ属性のアクセサに@Indexed注釈を付けます。
public class Person
    {
    //defines an unordered index on 'Person::getName', which is suitable for filters such as 'equal', 'like', and 'regex'
    @Indexed                                                  
    public String getName()
        {
        return name;
        }
    //defines an ordered index on 'Person::getAge', which is better suited for filters such as 'less', 'greater', and 'between'
    @Indexed(ordered = true)                                  
    public int getAge()
        {
        return age;
        }
    }

リポジトリが作成されると、@Indexed注釈のエンティティ・クラスがイントロスペクトされ、この注釈を持つ各属性の索引が自動的に作成されます。作成された索引は、その属性が問合せ式内で参照されるたびに使用されます。

場合によっては、デシリアライズされたエンティティ・インスタンスを破棄するのではなく、保持したい場合があります。インスタンスを保持すると、問合せや集計を頻繁に行ったり、ストリームAPIを使用したり、インプレース更新や予測を行う場合に役立ちます。これは、すべての属性で個々の索引をメンテナンスするコストが、デシリアライズされたエンティティ・インスタンスを保持するコストよりも大きくなる可能性があるためです。

このような状況のために、Coherenceは特別な索引タイプDeserializationAcceleratorを提供します。ただし、リポジトリAPIを使用している場合は、より簡単に構成できます。@Accelerated注釈を使用して、エンティティ・クラスまたはリポジトリ・クラス自体に注釈を付けます。
@Accelerated
public class Person
    {
    }

すべてのエンティティのシリアライズ済コピーとデシリアライズ済コピーの両方を格納するために、クラスタに追加の記憶域容量が必要になりますが、状況によっては、パフォーマンス上の利点がコストを大幅に上回る場合があります。言い換えれば、高速化は時間と空間のトレードオフの典型的な例であり、それをいつ使用するのが適切であるかを決めるのは完全にあなた次第です。

イベント・リスナーの作成

Coherenceでは、データ・エンティティを効率的に格納、変更、問合せおよび集計できるだけでなく、リポジトリ内のエンティティが変更されるたびにイベント通知を受信するように登録することもできます。

エンティティが挿入、更新または削除されるたびに通知されるリスナーを作成して登録できます。
public static class PeopleListener
            implements PeopleRepository.Listener<Person>
        {
        public void onInserted(Person personNew)
            {
            // handle INSERT event
            }

        public void onUpdated(Person personOld, Person personNew)
            {
            // handle UPDATE event
            }

        public void onRemoved(Person personOld)
            {
            // handle REMOVE event
            }
        }
people.addListener(new PeopleListener());                                   
people.addListener("111-22-3333", new PeopleListener());          
people.addListener(Filters.greater(Person::getAge, 17), new PeopleListener());
ここで:
  • people.addListenerは、リポジトリ内のエンティティが挿入、更新または削除されるたびに通知されるリスナーを登録します。
  • 2行目は、指定した識別子を持つエンティティが挿入、更新または削除されたときに通知されるリスナーを登録します。
  • 3行目は、17より古いPersonが挿入、更新または削除されたときに通知されるリスナーを登録します。

前述の例に示すように、関心のあるイベントのみを登録することで、受信するイベントの数とネットワーク経由で送信されるデータの量を減らす方法がいくつかあります。

ノート:

前述の例で使用されているすべてのリスナー・メソッドには、デフォルトのno-op実装があります。したがって、実際に処理したいものだけを実装すれば済みます。
ただし、リスナーを登録するたびに別のクラスを実装する必要があるのは少し煩雑であるため、リポジトリAPIにはデフォルトのリスナー実装と、タスクを容易にするための適切なビルダーも用意されています。
people.addListener(
        people.listener()
              .onInsert(personNew -> { /* handle INSERT event */ })
              .onUpdate((personOld, personNew) -> { /* handle UPDATE event with old value */ })
              .onUpdate(personNew -> { /* handle UPDATE event without old value */ })
              .onRemove(personOld -> { /* handle REMOVE event */ })
              .build()
);

ノート:

リスナー・ビルダーAPIを使用する場合、onUpdateイベント・ハンドラ引数リストから古いエンティティ値を省略するオプションがあります。同じイベント・タイプに複数のハンドラを指定することもできます。その場合は、指定した順序で構成され、呼び出されます。
イベント・タイプに関係なく、すべてのイベントを受信する単一のイベント・ハンドラを提供するオプションもあります。
people.addListener(
        people.listener()
              .onEvent(person -> { /* handle all events */ })
              .build()
);

リスナー・クラスを明示的に実装する場合と同様に、エンティティ識別子またはフィルタを最初の引数としてaddListenerメソッドに渡して、受信するイベントのスコープを制限できます。

非同期リポジトリAPIの使用

同期リポジトリAbstractRepository<ID, T>に加えて、非同期バージョンAbstractAsyncRepository<ID, T>があります。リポジトリの実装で説明したものと同じ抽象メソッドを実装する必要があります。

2つのAPIの主な違いは、非同期APIが戻り型のjava.util.CompletableFutureを返すことです。たとえば、ブロッキング・バージョンのCollection<T> getAll()は、リポジトリAPIの非同期バージョンではCompletableFuture<Collection<T>>になります。

非同期APIは、返される前に結果をコレクションにバッファリングするのではなく、操作の結果が使用可能になると渡されるコールバックも提供します。この機能を使用すると、すべての結果をメモリーに蓄積するコストを支払うことなく、非常に大きな結果セットをストリーミングおよび処理できますが、これはブロッキングAPIでは不可能です。

例40-1 AbstractAsyncRepositoryの例

public class AsyncPeopleRepository
        extends AbstractAsyncRepository<String, Person>
    {
    private AsyncNamedMap<String, Person> people;

    public AsyncPeopleRepository(AsyncNamedMap<String, Person> people)
        {
        this.people = people;
        }

    protected AsyncNamedMap<String, Person> getMap()          
        {
        return people;
        }

    protected String getId(Person entity)                     
        {
        return entity.getSsn();
        }

    protected Class<? extends Person> getEntityType()         
        {
        return Person.class;
        }
    }
  • getMapメソッドは、リポジトリのバッキング・データ・ストアとして使用する必要があるAsyncNamedMapを返します。この場合、コンストラクタ引数によって指定します。ただし、CDI (Contexts and Dependency Injection)を使用して簡単に注入することもできます。
  • getIdメソッドは、指定されたエンティティの識別子を返します。
  • getEntityTypeメソッドは、リポジトリに格納されているエンティティのクラスを返します。
次の例では、AsyncPersonRepositoryを使用してエンティティの単純な問合せを作成します。
String upercaseName = asyncPeople.get("111-22-3333")                          
                                 .thenApply(Person::getName)          
                                 .thenApply(String::toUpperCase) 
                                  .get()
ここで:
  • 最初の行は、IDに基づいてCompletableFuture<Personを取得します。
  • futureが完了すると、2行目はPersonインスタンスから個人の名前を取得します。
  • 3行目は、名前を大文字に変換します。
  • 3行目は、大文字の名前をブロックして返します。

この使用パターンは、CompletableFutureを返すすべてのメソッドで類似しています。

非同期コールバックの使用

結果に対して実現されるコレクション全体を処理するかわりに、結果が使用可能になると呼び出されるコールバックを定義できます。これらのAPIは、すべての結果が処理されたことを知らせるCompletableFuture<Void>を返します。

たとえば、クライアントに結果セットが蓄積されることなく、サーバーからストリーミングして戻される人の名前を出力する場合は、次のようにします。
asyncPeople.getAll(person -> System.out.println(person.getName())) 
    .thenApply(done -> System.out.println("DONE!"))
前述のコード例で:
  • コードの最初の行には、リポジトリ内の各Personの名前が出力されます。
  • 2行目では、すべての人の名前が処理されるとDONE!が出力されます。
最初の引数としてValueExtractorを指定して、name属性を抽出することもできます。この場合、ネットワーク上で移動するデータを減らすために、前述の例のコードを次のように書き換えることができます。
asyncPeople.getAll(Person::getName, (id, name) -> System.out.println(name)) 
     .thenApply(done -> System.out.println("DONE!"))
ここで:
  • コードの最初の行には、リポジトリ内の各Personの名前が出力されます。
  • 2行目では、すべての人の名前が処理されるとDONE!が出力されます。

前述の例では、コールバックは、エンティティ識別子および抽出された値を引数として受け取るBiConsumerとして実装されています。フラグメント・エクストラクタを前述のgetAllメソッドの最初の引数として使用することもできます。この場合、コールバックの2番目の引数はname属性のみでなくFragment<Person>になります。