SeamFramework.orgCommunity Documentation

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

13.1. はじめに
13.2. Homeオブジェクト
13.3. Queryオブジェクト
13.4. Controllerオブジェクト

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

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

We should emphasize that the framework is extremely simple, just a handful of simple classes that are easy to understand and extend. The "magic" is in Seam itself — the same magic you use when creating any Seam application even without using this framework.

Seam アプリケーションフレームワークの提供するコンポーネントは、 二つの使い方のいずれかで利用することができます。 第一の方法は、 他の Seam の組み込みコンポーネントで行っているように、 components.xml でコンポーネントのインスタンスをインストールし設定する方法です。 たとえば、 以下の components.xml 設定の一部では Person エンティティに対する基本的な CRUD 操作を実行できる 1 コンポーネントをインストールしています。


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

上記が「XML でのプログラミング」に偏重しているように思える場合は、 代りにコードを拡張して使用することもできます。

@Name("personHome")

public class PersonHome extends EntityHome<Person
> {
    
   @In EntityManager personDatabase;
    
   public EntityManager getEntityManager() {
      return personDatabase; 
   }
    
}

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

また、 必要に応じてクラスを EJB のステートフルセッション Bean にできるというメリットもあります。 (必ずEJBにする必要はなく、好みで、プレーンなJavaBeanとすることもできます。) JBoss AS を使用される場合、4.2.2.GAあるいはそれ以降のバージョンが必須です。

@Stateful

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

クラスをステートレスセッション Bean にすることもできます。 この場合、 その名前が entityManager であってもインジェクションを使って永続コンテキストを提供しなければなりません

@Stateless

@Name("personHome")
public class PersonHome extends EntityHome<Person
> implements LocalPersonHome {
    
   @In EntityManager entityManager;
    
   public EntityManager getPersistenceContext() { 
      entityManager; 
   }
    
}

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

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

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

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() の オペレーションを提供します。 remove()、あるいは update() を呼び出す前にまず setId() メソッドを用いて対象のオブジェクトの識別子をセットする必要があります。

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 エントリの nationality を初期化して作成しなければならない場合を考えてみましょう。これも簡単にできます。 構成ファイルを使う場合は;


<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();
    }
    
}

The Home object raises an org.jboss.seam.afterTransactionSuccess event when a transaction succeeds (a call to persist(), update() or remove() succeeds). By observing this event we can refresh our queries when the underlying entities are changed. If we only want to refresh certain queries when a particular entity is persisted, updated or removed we can observe the org.jboss.seam.afterTransactionSuccess.<name> event (where <name> is the simple name of the entity, e.g. an entity called "org.foo.myEntity" has "myEntity" as simple name).

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 createValueExpression("New person #{person.firstName} #{person.lastName} created"); }
    protected String getUpdatedMessage() { return createValueExpression("Person #{person.firstName} #{person.lastName} updated"); }
    protected String getDeletedMessage() { return createValueExpression("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>を使って、ページにバリデーション機能を追加することですが、これは皆さんへの宿題としておきましょう。

データベース中の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( concat(#{examplePerson.firstName},'%') )</value>
        <value
>lower(lastName) like lower( concat(#{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
>

基礎となるエンティティが変化する場合にリフレッシュするには org.jboss.seam.afterTransactionSuccess イベントを監視します。


<event type="org.jboss.seam.afterTransactionSuccess">
    <action execute="#{people.refresh}" />
</event
>

または、 PersonHome で person エンティティの永続化、 更新、 あるいは削除が行われた場合にクエリーをリフレッシュするだけなら次のようにします。


<event type="org.jboss.seam.afterTransactionSuccess.Person">
    <action execute="#{people.refresh}" />
    </event
>

Unfortunately Query objects don't work well with join fetch queries - the use of pagination with these queries is not recommended, and you'll have to implement your own method of calculating the total number of results (by overriding getCountEjbql().

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

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;
      }
   }
}

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