StrutsとHibernateの間に橋を架ける--zt

13934 ワード

要約Hibernateとstrutsは、現在市販されているいくつかの最も流行しているオープンソースのライブラリの一つです.これらは効率的で、プログラマーがJavaエンタープライズアプリケーションを開発し、競合するライブラリをいくつか選択する第一選択です.それらはよく一緒に応用されていますが、Hibernateの設計目標はStrutsと一緒に使用するのではなく、StrutsはHibernateが誕生する数年前に発表されました.一緒に仕事をさせるためには、まだ多くの挑戦があります.この文章はStrutsとHibernateの間のいくつかのギャップを明らかにし,特にオブジェクト向けモデリングに関係している.両者の間に橋を架ける方法も述べ,拡張Strutsに基づく解決策を与えた.StrutsとHibernateに基づいて構築されたすべてのWebアプリケーションは、この汎用的な拡張から利益を得ることができます.Hibernate in Action(Manning,2004 10月)という本では,著者Christian BauerとGavin Kingがオブジェクト世界向けのモデルと関係データモデルを明らかにし,両世界の例は一致しなかった.Hibernateは両方を記憶層(persistence Layer)に接着することに非常に成功した.しかし、領域モデル(domain model)(すなわちModel-View-Controllerのmodel layer)とHTMLページ(MVCのView Layer)は依然として一致していない.この論文では,この不一致を検討し,解決策を探索する.例が一致しない再発見は、productとcategoryという古典的なparent-child関係の例を見てみましょう.Categoryクラスは、longのタイプの識別子idとStringのタイプの属性nameを定義します.Productクラスにはlongのタイプの識別子idとCategoryのタイプのプロパティcategoryもあり、多対一の関係(つまり多くのproductがCategoryに属することができる)

						/**
* @hibernate.class table="CATEGORY"
*/
public class Category {
   private Long id;

   private String name;

   /**
    * @hibernate.id generator-class="native" column="CATEGORY_ID"
    */
   public Long getId() {
      return id;
   }

   public void setId(Long id) {
      this.id = id;
   }

   /**
    * @hibernate.property column="NAME"
    */
   public String getName() {
      return name;
   }

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

/**
* @hibernate.class table="PRODUCT"
*/
public class Product {
   private Long id;
   private Category category;

   /**
    * @hibernate.id generator-class="native" column="PRODUCT_ID"
    */
   public Long getId() {
      return id;
   }

   public void setId(Long id) {
      this.id = id;
   }

   /**
    * @hibernate.many-to-one
    * column="CATEGORY_ID"
    * class="Category"
    * cascade="none"
    * not-null="false"
    */
   public Category getCategory() {
      return category;
   }

   public void setCategory(Category category) {
      this.category = category;
   }
}
を表しています.私たちはproductがcategoryを変更することを望んでいます.そのため、私たちのHTMLはドロップダウンボックスを提供してすべてのCategoryをリストします.

						<select name="categoryId">
   <option value="">No Category</option>
   <option value="1">Category 1</option>
   <option value="2">Category 2</option>
   <option value="3">Category 3</option>
</select>
ここでは、ProductレルムオブジェクトではcategoryプロパティがCategoryタイプですが、ProductFormにはlongのcategoryIdタイプが1つしかありません.この不一致は、不一致を増加させるだけでなく、primitive typeの識別子と対応するオブジェクトとの間の変換に不要なコードをもたらす.このような不一致部分はHTML Form自身によるものである:それは1つの関係モデルのみを表し、オブジェクト向けのモデルを表すことはできない.オブジェクトとリレーションシップモデルの不一致は、ストレージレイヤでオブジェクトリレーションシップマッピング(O/RM)によって解決される.しかし、同様の問題は、表示レイヤ(view layer)に依然として存在する.解決の鍵は彼らを一緒にシームレスに仕事をさせることだ.Strutsの機能と限界幸いなことに、Strutsは埋め込まれたオブジェクト属性を生成し、解釈することができます.Categoryドロップダウン・ボックスでは、Struts page-construction(html)tag library:

						<html:select property="category.id">
   <option value="">No Category</option>
   <html:options collection="categories" property="id" labelProperty="name"/>
</html:select>
を使用できます.categoriesはCategoryオブジェクトのリストであると仮定します.そこで、ProductFormをより「オブジェクト向け」に変更し、ProductFormのcategoryIdをCategoryのcategoryタイプに変更します.この変更により、ProductとProductFormの間でプロパティをコピーする作業が煩雑になります.両方に同じプロパティがあるためです.

						public class ProductForm extends ActionForm {
     private Long id;
     private Category category;
     ...
}
残りのStruts Action,configuration,validator,jsp,hibernateレイヤを完了した後、テストを開始し、すぐにProductFormにアクセスします.category.idでNullPointerExceptionに遭遇しました.これは予想通りです!ProductForm.Categoryはまだ設定されていません.また、Hibernateは、複数対の関連オブジェクト参照を空に設定します(database fieldが空の場合).Strutsは、すべてのオブジェクトが表示(HTML Formの生成)および伝播(HTML FORMのコミット)の前に確立されることを要求する.ActionFormの使い方を見てみましょうreset()は橋を架ける.(そうではない)悪名高いStruts ActionFormが私の最初の週にStrutsに接触したとき、私の最大の疑問は、なぜ私がProperties、getterメソッド、setterメソッドのためにほぼ同じ2つのcopyを維持しなければならないのか、1つはActionForm Beanで、1つはDomainObjectである.この煩雑な手順はStrutsコミュニティの最も主要な愚痴の一つになった.私の観点では、ActionFormには理由があります.まず、Domain Objectとは異なる役割を果たしているため、Domain Objectと区別できます.MVCモードでは、Domain ObjectはModel層の一部であり、ActionFormはView層である.WebpageのFieldとDatabaseのFieldが異なる可能性があるため、いくつかの特製の変換は一般的です.第二に、ActionForm.validate()メソッドは、非常に使いやすい検証ルールを定義できます.第三に、他の特定のView行為があるかもしれないが、domain layerで実現したくない.特にpersistence frameworkがdomain objectを管理するとき.FormをコミットしてActionForm内のメソッド-reset()を使用してviewとmodelの不一致を解決します.このreset()メソッドは、ActionFormがStruts Controllerサーブレットによってrequestが処理されたときにActionFormプロパティをコピーする前に呼び出されます.この方法の最も一般的な使用は、checkboxが選択されていないcheckboxが正しく認識されるようにfalseに明示的に設定されなければならないことである.Reset()はview rendingオブジェクトを初期化するのに適した場所でもある.コードは、

						public class ProductForm extends ActionForm {
     private Long id;
     private Category category;
     ...
     public void reset(ActionMapping mapping, HttpServletRequest request)
     {
        super.reset( mapping, request );
        if ( category == null ) { category = new Category(); }
     }
}
Strutsがユーザーが発行した値を使用してProductFormに記入する前に、Strutsがreset()を呼び出し、categoryプロパティが初期化されます.categoryがnullかどうかを確認しなければなりません.後でこれについて議論します.編集Formはこれまでform提出時の問題を解決してきました.しかし、formページを生成するときは?Html:select tagも空でない参照を望んでいるので、formがページを生成する前にreset()を呼び出します.私たちはactionクラスに1行を加えました:

						public class EditProductAction extends Action {
     public final ActionForward execute( ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response ) throws Exception
     {
        ...
        Product product = createOrLoadProduct();
        ProductForm productForm = (ProductForm)form;
        PropertyUtils.copyProperties( productForm, product );
        productForm.reset( mapping, request );
        ...
     }
}
読者はactionクラスとJakarta commons Beanutilsパッケージによく知っていると仮定します.CreateOrLoadProduct()は、このactionがProductを確立または変更したことに応じて、新しいProductインスタンスを確立またはデータベースから既存のインスタンスをロードします.ProductFormが割り当てられた後(つまり、PropertyUtils.copyPropertiesが呼び出された後)、productForm.CategoryはすでにProductからCategoryがコピーされ(訳者:実際にはcategoryオブジェクトリファレンスをコピーしただけで、オーバーヘッドはありません)、ProductFormがページを生成するために使用されます.Hibernateにロードされたオブジェクトを上書きしないことを保証する必要があります.nullであるかどうかを確認する必要があります.reset()メソッドはActionFormで定義されているため、上記のコードをCommonEditActionなどのsuperclassに入れて処理することができます.

						      Product product = createOrLoadProduct();
        PropertyUtils.copyProperties( form, product );
        form.reset( mapping, request );
読み取り専用のFormが必要な場合は、2つの選択肢があります.最初に関連するjspオブジェクトがnullであるかどうかを確認します.2番目にdomainオブジェクトをActionFormにコピーした後、Reset()を呼び出してdomainオブジェクトを保存します.私たちはFormのコミットとFormページの生成の問題を解決したので、Strutsは満足できます.でもHibernateは?ユーザがnull ID optionを選択すると、我々の例では「no category」option-form、productFormがコミットされる.categoryは、idがnullの新しいhibernateオブジェクトを指します.Category属性がProductFormからHibernate制御のProductオブジェクトにコピーする格納されると、Hibernateはproductに文句を言う.categoryは一時オブジェクトであり、Productが格納される前に格納する必要があります.もちろん、Nullであり、格納される必要はないことを知っています.だからproductをCategoryをNullに設定すると、HibernateはProductを格納できます(この場合、データベースproduct.categoryは空の値に設定されます).Hibernateの働き方も変えたくないので、Domainオブジェクトにコピーする前に一時オブジェクトをクリーンアップする方法を選びました.

						public class ProductForm extends ActionForm {
     private Long id;
     private Category category;
     ...
     public void reset(ActionMapping mapping, HttpServletRequest request) {
        super.reset( mapping, request );
        if ( category == null ) { category = new Category(); }
     }

     public void cleanupEmptyObjects() {
        if ( category.getId() == null ) { category = null; }
     }
}
copyPropertiesの前に一時オブジェクトをクリーンアップする方法をProductFormに追加しました.CategoryはNullを入れるだけでProductFormになります.CategoryをNullに設定します.それからDomainオブジェクトのcategoryもnullに設定されます:

						public class SaveProductAction extends Action {
     public final ActionForward execute( ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response ) throws Exception
     {
        ...
        Product product = new Product();
        ((ProductForm)form).cleanupEmptyObjects();
        PropertyUtils.copyProperties( product, form );
        SaveProduct( product );
        ...
     }
}
一対の多関係私はまだCategoryからProductまでの一対の多関係を解決していません.CategoryのMetadataに追加しました:

						public class Category {
     ...
     private Set products;
     ...

     /**
      * @hibernate.set
      * table="PRODUCT"
      * lazy="true"
      * outer-join="auto"
      * inverse="true"
      * cascade="all-delete-orphan"
      *
      * @hibernate.collection-key
      * column="CATEGORY_ID"
      *
      * @hibernate.collection-one-to-many
      * class="Product"
      */
     public Set getProducts() {
        return products;
     }

     public void setProducts(Set products) {
        this.products = products;
     }
}
注意:Hibernateのcascadeプロパティはall-delete-orphanです.Hibernateは、含まれるCategoryオブジェクトを格納するときにProductオブジェクトを自動的に格納する必要があることを示しています.parentオブジェクトと一緒にchildオブジェクトを格納する場合は一般的ではありません.一般的には、childの格納とparentの格納をそれぞれ制御します.私たちの例では、ユーザーが同じhtml pageでCategoryとProductSを編集できるようにすれば、簡単にできます.Productsをsetで表すのは非常に直感的です.

						public class CategoryForm extends ActionForm {
     private Set productForms;
     ...
     public void reset(ActionMapping mapping, HttpServletRequest request) {
        super.reset( mapping, request );

        for ( int i = 0; i < MAX_PRODUCT_NUM_ON_PAGE; i++ ) {
           ProductForm productForm = new ProductForm();
           productForm.reset( mapping, request );
           productForms.add( productForm );
        }
     }

     public void cleanupEmptyObjects() {
        for ( Iterator i = productForms.iterator(); i.hasNext(); ) {
           ProductForm productForm = (ProductForm) i.next();
           productForm.cleanupEmptyObjects();
        }
     }
}
さらに、formsを参照、編集、コミットし、関連するobjectsを格納することができますが、すべてのActionFormクラスに対してCleanupEmptyObjects()とreset()メソッドを定義するのは厄介です.抽象的なActionFormを使用して、これらの作業の完了に協力します.一般的な実装として,すべてのHibernateが管理するdomainオブジェクトを遍歴し,identifierを発見し,id値をテストしなければならない.幸いなことにhibernate.Metadataパッケージには、domainオブジェクトのメタデータを取り出すUtilityクラスが2つあります.このobjectがHibernateによって管理されているかどうかをClassMetadataクラスで確認します.もしそうなら、私たちはそれらのid Valueを取り出します.Jakarta Commons Beanutilsパッケージを使用してJavaBeanメタデータの操作を支援しました.

						import java.beans.PropertyDescriptor;
import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.metadata.ClassMetadata;

public abstract class AbstractForm extends ActionForm {
   public void reset(ActionMapping mapping, HttpServletRequest request) {
      super.reset( mapping, request );

      // Get PropertyDescriptor of all bean properties
      PropertyDescriptor descriptors[] =
         PropertyUtils.getPropertyDescriptors( this );

      for ( int i = 0; i < descriptors.length; i++ ) {
         Class propClass = descriptors.getPropertyType();

         ClassMetadata classMetadata = HibernateUtil.getSessionFactory()
            .getClassMetadata( propClass );

         if ( classMetadata != null ) {   // This is a Hibernate object
            String propName = descriptors.getName();
            Object propValue = PropertyUtils.getProperty( this, propName );

            // Evaluate property, create new instance if it is null
            if ( propValue == null ) {
               PropertyUtils.setProperty( this, propName, propClass.newInstance() );
            }
         }
      }
   }

   public void cleanupEmptyObjects() {
      // Get PropertyDescriptor of all bean properties
      PropertyDescriptor descriptors[] =
      PropertyUtils.getPropertyDescriptors( this );

      for ( int i = 0; i < descriptors.length; i++ ) {
         Class propClass = descriptors.getPropertyType();
         ClassMetadata classMetadata = HibernateUtil.getSessionFactory()
            .getClassMetadata( propClass );

         if ( classMetadata != null ) {   // This is a Hibernate object
            Serializable id = classMetadata.getIdentifier( this, EntityMode.POJO );

            // If the object id has not been set, release the object.
            // Define application specific rules of not-set id here,
            // e.g. id == null, id == 0, etc.
            if ( id == null ) {
               String propName = descriptors.getName();
               PropertyUtils.setProperty( this, propName, null );
            }


         }
      }
   }
}
コードを読めるようにExceptionの処理コードを省略しました.我々の新しいAbstractFormクラスはStrutsのActionFormクラスから継承され、resetとcleanupの複数対の関連オブジェクトを提供します.この関係が逆の場合(すなわち一対の多関係)、Abstractクラスで実現するように例ごとに異なります.まとめStrutsとHibernateは非常に流行し、強力なフレームワークであり、domainモデルとMVCビュー(view)の違いを補うことができます.この論文ではStruts/Hibernate Projectを解決するための一般的なスキームを議論し,既存のコードを大量に修正する必要はない.