第10章 Seamアプリケーションフレームワーク

Seamは特別のインターフェースやスーパークラスを拡張することなく、純粋なJavaクラスにアノテーションを付記することにより簡単にアプリケーションを作成することが出来ます。 しかし、components.xmlの設定 (簡単な場合には) や機能の拡張により再利用する事が出来る既成のコンポーネントを提供することにより、お決まりのプログラムについて更に簡単に作成できるようにすることが出来ます。

SeamアプリケーションフレームワークはJPAやHibernateを使ったデータベースへのアクセスに関わる基本的なプログラムのコード量を削減することが出来ます。

このフレームワークは理解も容易で、拡張し易く簡単ないくつかのクラスから構成されています。 コード量を減らしたり、コードの再利用を可能にする「マジック」はSeamそのものにあります — このフレームワークを使わなくても Seam アプリケーションを作成するときには同様のマジックを使用しているのです。

10.1. イントロダクション

このフレームワークの提供するコンポーネントは、二つの使い方のいずれかで利用することが出来ます。 一つは、他のSeamの組み込みコンポーネントで行っているように、components.xmlでインスタンスを設定して使用する方法です。 下のcomponents.xmlの設定 (部分) ではContactエンティティに対する基本的なCRUD操作をインストールしています。

<framework:entity-home name="personHome" 
                       entity-class="eg.Person" 
                       entity-manager="#{personDatabase}">
    <framework:id>#{param.personId}</framework:id>
</framework:entity-home>

もし、これが「XMLプログラミング」に偏重しているように思えて、好みに合わなければ、代りに機能を拡張して行うことも出来ます。

@Stateful
@Name("personHome")
public class PersonHome extends EntityHome<Person> implements LocalPersonHome {
    @RequestParameter String personId;
    @In EntityManager personDatabase;
    
    public Object getId() { return personId; }
    public EntityManager getEntityManager() { return personDatabase; }
    
}

第二の方法 (機能の拡張を使う) は大きなメリットとして、簡単に拡張したり、内蔵された機能をオーバーライドすることが出来ます。 (このフレームワークの提供するクラスは、拡張や、カスタム化に対応できるよう、注意深く作成されています。)

また、第2のメリットとして、クラスをEJBのステートフルセッションビーンとすることが出来ます。 (必ずEJBにする必要はなく、好みで、プレーンなJavaビーンとすることも出来ます。)

現時点で、Seamアプリケーションフレームワークは、CRUD 用にEntityHomeHibernateEntityHome、それにQueryの為のEntityQueryHibernateEntityQueryの4つのコンポーネントを提供しています。

HomeとQueryはセッション、イベント、それに対話スコープで機能するように作成されています。 どのスコープを使用するかは、アプリケーションのステートモデルに依存します。

SeamアプリケーションフレームワークはSeamが管理している永続性コンテキストでのみ動作します。 デフォルトで、entityManagerという名前の永続性コンテキストを探します。

10.2. Homeオブジェクト

Homeオブジェクトは、特定のエンティティクラスに対する永続性操作を提供します。Personクラスについて考えてみましょう。

@Entity
public class Person {
    @Id private Long id;
    private String firstName;
    private String lastName;
    private Country nationality;
    
    //getters and setters...
}

コンフィギュレーションで、下のようにpersonHomeコンポーネントを定義することが出来ます。

<framework:entity-home name="personHome" entity-class="eg.Person" />

また、機能を拡張して下のように、同様のことが出来ます。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {}

Homeオブジェクトはpersist()remove()update() それに getInstance() の 4 つの操作を提供します。

HomeはJSFページから、下のように直接利用することが出来ます。

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{personHome.instance.firstName}"/></div>
    <div>Last name: <h:inputText value="#{personHome.instance.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

通常、Personpersonで参照出来た方が便利ですので、components.xmlに下のように一行加えて、そのようにしましょう。

<factory name="person" 
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome" 
                       entity-class="eg.Person" />

(コンフィギュレーションを使用している場合、) PersonHome@Factory を追加します。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {
    
    @Factory("person")
    public Person initPerson() { return getInstance(); }
    
}

(機能を拡張している場合) これで、下のように JSF ページの記述が簡単になります。

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

これで、Personの新しいエントリーを作成することが出来るようになります。 はい、これで全てです。 次に、表示、更新、それに削除機能を既存のデータ−ベースの Personエントリー操作に追加するためには、PersonHomeに対象のエントリーを特定する識別子を伝える必要があります。 下のように、ページパラメータを使って、これを行います。

<pages>
    <page view-id="/editPerson.jsp">
        <param name="personId" value="#{personHome.id}"/>
    </page>
</pages>

これで、JSFページにこれらの機能を追加することが出来ます。

<h1>
    <h:outputText rendered="#{!personHome.managed}" value="Create Person"/>
    <h:outputText rendered="#{personHome.managed}" value="Edit Person"/>
</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}" rendered="#{!personHome.managed}"/>
        <h:commandButton value="Update Person" action="#{personHome.update}" rendered="#{personHome.managed}"/>
        <h:commandButton value="Delete Person" action="#{personHome.remove}" rendered="#{personHome.managed}"/>
    </div>
</h:form>

リクエストパラメータ無しでページにリンクした場合、「Person作成」としてページが表示され、personId をリクエストパラメータとして渡した場合には、「Person編集」としてページが表示されます。

Personをその国籍を初期化して作成しなければならない場合を考えてみましょう。これも簡単に出来ます。 コンフィギュレーションを使う場合は;

<factory name="person" 
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome" 
                       entity-class="eg.Person" 
                       new-instance="#{newPerson}"/>

<component name="newPerson" 
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

また、機能を拡張して行う場合は下の様になります。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {
    
    @In Country country;
    
    @Factory("person")
    public Person initPerson() { return getInstance(); }
    
    protected Person createInstance() {
        return new Person(country);
    }
    
}

勿論、Countryオブジェクトは、例えばCountryHomeというHomeオブジェクトの管理下のオブジェクトとすることも出来ます。

アソシエーションの管理など、 より洗練された操作を実現するのも PersonHome にメソッドを追加するだけで出来るようになります。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {
    
    @In Country country;
    
    @Factory("person")
    public Person initPerson() { return getInstance(); }
    
    protected Person createInstance() {
        return new Person(country);
    }
    
    public void migrate()
    {
        getInstance().setCountry(country);
        update();
    }
    
}

Homeオブジェクトは操作が成功したときに自動的にフェースメッセージを表示します。 これを、カスタマイズするには、下のようにコンフィギュレーションを設定します。

<factory name="person" 
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       new-instance="#{newPerson}">
    <framework:created-message>New person #{person.firstName} #{person.lastName} created</framework:created-message>
    <framework:deleted-message>Person #{person.firstName} #{person.lastName} deleted</framework:deleted-message>
    <framework:updated-message>Person #{person.firstName} #{person.lastName} updated</framework:updated-message>
</framework:entity-home>

<component name="newPerson" 
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

あるいは、機能を拡張して下のようにすることも出来ます。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {
    
    @In Country country;
    
    @Factory("person")
    public Person initPerson() { return getInstance(); }
    
    protected Person createInstance() {
        return new Person(country);
    }
    
    protected String getCreatedMessage() { return "New person #{person.firstName} #{person.lastName} created"; }
    protected String getUpdatedMessage() { return "Person #{person.firstName} #{person.lastName} updated"; }
    protected String getDeletedMessage() { return "Person #{person.firstName} #{person.lastName} deleted"; }
    
}

しかし、メッセージ定義における最良の方法は (デフォルトで messages という名前の) Seam に対して既知のリソースバンドルに定義することでしょう。

Person_created=New person #{person.firstName} #{person.lastName} created
Person_deleted=Person #{person.firstName} #{person.lastName} deleted
Person_updated=Person #{person.firstName} #{person.lastName} updated

この方法を使えば、国際化に対応することが出来ますし、コードやコンフィギュレーションファイルとプレゼンテーション層とを切り離すことが出来ます。

最後のステップは<s:validateAll><s:decorate>を使って、ページにバリデーション機能を追加することですが、これは皆さんへの宿題としておきましょう。

10.3. Queryオブジェクト

データベース中のPersonの全てのインスタンスのリストが必要な場合、Queryオブジェクトを使って、下のようにすることが出来ます。

<framework:entity-query name="people" 
                        ejbql="select p from Person p"/>

また、これ (この結果) をJSFページから使うことが出来ます。

<h1>List of people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

量の多いページを処理する為にページングも必要でしょう。

<framework:entity-query name="people" 
                        ejbql="select p from Person p" 
                        order="lastName" 
                        max-results="20"/>

表示するページを決める為にページパラメータを使います。

<pages>
    <page view-id="/searchPerson.jsp">
        <param name="firstResult" value="#{people.firstResult}"/>
    </page>
</pages>

ページングを管理するJSFのコードは若干繁雑ですが、許容範囲内です。

<h1>Search for people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="First Page">
    <f:param name="firstResult" value="0"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="Previous Page">
    <f:param name="firstResult" value="#{people.previousFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Next Page">
    <f:param name="firstResult" value="#{people.nextFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Last Page">
    <f:param name="firstResult" value="#{people.lastFirstResult}"/>
</s:link>

実用的な検索スクリーンでは、絞りこんだ検索結果を得るために、多くの検索のクライテリアをユーザに入力してもらう必要があります。 この重要なユースケースをサポートする為に、Queryオブジェクトはオプションとして制約を設定することが出来ます。

<component name="examplePerson" class="Person"/>
        
<framework:entity-query name="people" 
                        ejbql="select p from Person p" 
                        order="lastName" 
                        max-results="20">
    <framework:restrictions>
        <value>lower(firstName) like lower( #{examplePerson.firstName} + '%' )</value>
        <value>lower(lastName) like lower( #{examplePerson.lastName} + '%' )</value>
    </framework:restrictions>
</framework:entity-query>

上記の例ではexampleオブジェクトの使用について留意してください。

<h1>Search for people</h1>
<h:form>
    <div>First name: <h:inputText value="#{examplePerson.firstName}"/></div>
    <div>Last name: <h:inputText value="#{examplePerson.lastName}"/></div>
    <div><h:commandButton value="Search" action="/search.jsp"/></div>
</h:form>

<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

ここの例ではコンフィギュレーションによる再利用を示していますが、Queryオブジェクトの再利用は機能を拡張して行う事も同様に可能です。

10.4. Controllerオブジェクト

Seamアプリケーションフレームワークのオプショナルなクラスとして、Controllerと、そのサブクラスとして、 EntityControllerHibernateEntityControllerBusinessProcessControllerがあります。 これらのクラスは、一般に利用されるコンポーネントのメソッドに対する、少し便利なアクセス方法を提供しています。 これらは、新しいユーザがSeamに組み込まれた豊富な機能を探検するための出発点を提供し、また若干のコード量の削減に貢献します。

例として、SeamのRegistrationの例のRegisterActionをSeamアプリケーションフレームワークで書き直すと以下のようになります。

@Stateless
@Name("register")
public class RegisterAction extends EntityController implements Register
{

   @In private User user;
   
   public String register()
   {
      List existing = createQuery("select u.username from User u where u.username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();
      
      if ( existing.size()==0 )
      {
         persist(user);
         info("Registered new user #{user.username}");
         return "/registered.jspx";
      }
      else
      {
         addFacesMessage("User #{user.username} already exists");
         return null;
      }
   }

}

ご覧のように、びっくりするような改善にはなりません、、、。