はじめに
今日、ソフトウェア開発のプロフェッショナルは、途方に暮れるほど多くのテクノロジの選択肢に囲まれています。どのようなビジネス問題であっても、どのテクノロジを採用するのが最も適切かを決めることは必ずしも容易ではありません。最近、企業向けJava開発の領域では、SpringフレームワークとEJB 3.0仕様に関する議論が盛んに行われています。そのため、多種多様な意見の中から明確かつ率直なアドバイスを見つけ出すのは難しいことかもしれません。
両者の相対的な長所と短所についての質問は、私のコンサルティング業務においても頻繁に遭遇するものの1つです。しかし、SpringとEJB 3.0に関する情報が数多く存在する一方で、両者を比較する客観的な情報は非常に少ないのが現状です。本稿では、読者の皆さんが類似点と相違点を理解するための助けとなるように、両テクノロジの機能を相互に関連付けながら詳しく見ていくことにします。
SpringとEJBは同じものではありません。非常に重要なことですが、Springは実装であり、EJB 3.0は仕様です。しかし、両者の間には、例えばJavaアプリケーションにミドルウェアサービスを提供する仕組みが用意されている点など、オーバーラップする部分もあります。SpringはEJBに対抗して開発されたため、両者を比較することは自然な成り行きと言えます。とりわけ、EJBの新しいバージョンが利用できるようになった今は、EJB 3.0が以前のバージョンの欠点をどのように解決しているかの再評価にふさわしい時期でしょう。また、その解決が首尾よく行われているとして、Springが提供し続けるEJBにはない機能とはどのようなものでしょうか?
本稿は2回シリーズの1回目であり、各回ではSpringとEJB 3.0についての種類の異なる特性を取り上げます。企業向けアプリケーションの開発という状況において、特に重要な特性を選んであります。このシリーズでは、航空券予約のサンプルアプリケーションを使ってそれぞれの特性を探っていきます。このサンプルアプリケーションには内容豊かなドメインモデルが含まれ、同時実行性、会話性、トランザクション性、メッセージング、スケジューリングといった企業の課題が示されています。また、SpringとEJB 3.0が同等の機能を提供しているかどうかを判断するために単体テストを利用します。
定義
細かい比較に入る前に、SpringとEJBが自らをそれぞれどのように説明しているのかを少し見てみましょう。SpringはそのWebサイトにおいて「階層化されたJava/J2EEアプリケーションフレームワーク」と定義されています。一方のEJBは「オブジェクト指向、分散型、企業レベルの各アプリケーションの開発および配備のためのコンポーネント・アーキテクチャ」であると記されています(J2EE用語集)。
まず、これらの定義から、SpringとEJBは共に企業向けJava開発の領域での運用を想定していることが分かります。しかし、SpringとEJBは、それぞれアプリケーションフレームワークとアーキテクチャであると主張しています。アーキテクチャの意味は場合によって大きく異なりますが、変更が困難な決定内容、またはソフトウェアシステムの構造というのが一般的な定義と言えます。アーキテクチャが暗に決定を意味しているのに対し、フレームワークはサポートを意味しています。今回の比較にあたっては、こうしたニュアンスを知っておくとよいでしょう。また、EJBが分散コンポーネントの開発に明確に対応している一方で、Springの定義ではこの点が強調されていないことも付け加えておきます。
持続性
持続性は、どんな企業向けアプリケーションでも重大な要素です。そのため、当然のことながらSpringとEJB 3.0でも持続性をしっかりとサポートしています。Springは、その設計思想を忠実に守り、持続性のフレームワークを再実装するのではなく、むしろJDBC、Hibernate、JDO、iBatis、(Spring 2.0の時点で)Java Persistence API(JPA)などの定評あるフレームワークを数多く統合しています。一方のEJB 3.0の仕様では、エンティティBeanがJPAで置き換えられています。
JPAは、簡素化された軽量なオブジェクト関係マッピングのフレームワークを提供することを目指したものです。JPAの仕様(EJB 3.0仕様の範囲内にある別のドキュメント)は、持続性プロバイダとのインタラクション、およびリレーショナルデータベースへのエンティティのマッピングのための各インターフェイスを定義しています。
持続性 - 機能の比較
SpringとEJB 3.0との間でサポートの比較が可能であることを説明するために、図1に示す航空券予約アプリケーションのドメインモデルを考えてみましょう。
以下に示す単体テストは、チケットを作成して、既存のフライトおよび割り当てられた座席との関連付けを行い、データベースに保存できることを検証するものです。
public void testSave() { Ticket ticket = TicketMother.create(); Flight outboundFlight = flightDAO.findById(1); Flight returnFlight = flightDAO.findById(2); ticket.setOutboundFlight(outboundFlight); ticket.setReturnFlight(returnFlight); ticket.getPassenger().setFlightDetails(outboundFlight, new PassengerFlightDetails(getSeatForFlight( outboundFlight, "2A"))); ticket.getPassenger().setFlightDetails(returnFlight, new PassengerFlightDetails(getSeatForFlight( returnFlight, "2B"))); ticketDAO.save(ticket); ticket = ticketDAO.findById(ticket.getId()); assertEquals("John", ticket.getPassenger().getFirstName()); assertEquals("2A", ticket.getPassenger().getDetailsForFlight( ticket.getOutboundFlight()).getSeat().getNumber()); }
このテストはSpringとEJB 3.0のどちらの実装でも正しく実行することができ、この2つのテクノロジが機能的に等価な形で基本的なORMの持続性をサポートしていることを示しています。両者の実装もまた非常によく似ています。ここでは、Hibernateを用いたSpringの実装を例として示します。HibernateはSpringと併用される最も一般的なORMプロバイダであり、Springの現行のプロダクションリリースではJPAがサポートされていないからです。Spring/HibernateによるTicketDAOは、次のようなクラスとして実装されています。
public class TicketSpringDAO extends HibernateDaoSupport implements TicketDAO { public Ticket save(Ticket ticket) { getHibernateTemplate().saveOrUpdate(ticket); return ticket; } ... }
これに相当するEJB 3.0の実装は次のようになります。
@Stateless public class TicketEJBDAO implements TicketDAO { @PersistenceContext(unitName = "flightdb") private EntityManager em; public Ticket save(Ticket ticket) { em.persist(ticket); return ticket; } ... }
ご覧の通り、両者の実装は非常によく似ています。Springの実装では、Hibernateとの連携の詳細がHibernateDaoSupport
クラスによって抽象化されており、このクラスはHibernateのプラミング(plumbing)を提供してSpringのHibernateテンプレートおよび依存関係との連携を支援するものになっています。EJB 3.0の実装では、コンテナによってエンティティマネージャが自動的に注入されています。
HibernateのSessionとJPAのEntity Managerはおおよそ同じものですが、注意すべき重要な違いが2つあります。Hibernateのセッションはエンティティキャッシュであると同時にORMエンジンへのインターフェイスでもありますが、JPAではこれら2つの概念が分離されています。また、持続性コンテキストがキャッシュとして機能しているのに対し、エンティティマネージャはORMエンジンへのインターフェイスにおいて機能します。
持続性エンティティをそれぞれの関係上等価なものにマッピングする、種類の異なるアプローチに注目するのも面白いでしょう。Spring/Hibernateのマッピングは、次のXMLマッピングファイルで表現されています。
<hibernate-mapping package="org.jug.flight.domain" default-lazy="false"> <class name="Ticket"> <id name="id" column="id"> <generator class="native" /> </id> <property name="status" type="Status" /> <many-to-one name="outboundFlight" column="outbound_flight" /> <many-to-one name="returnFlight" column="return_flight" /> <many-to-one name="passenger" column="passenger_id" cascade="all" /> </class> </hibernate-mapping>
JPAアプリケーションでは注釈を使ってこのマッピングを表現することが多いのですが、それはそうすることで必要な設定が減り、変更対象のオブジェクトに近い場所にマッピングデータを置くことで可視性が高まるからです。このアプローチによる実装を以下に示します。
@Entity public class Ticket implements Serializable { @Id @GeneratedValue private long id; private Status status; private double price; @ManyToOne(cascade = CascadeType.PERSIST) private Passenger passenger; private Flight outboundFlight; private Flight returnFlight; ... }
このように、Spring/HibernateとEJB 3.0/JPAが提供するORMによる持続性のサポートは、機能的にはほぼ等価であることが分かります。
持続性 - 機能以外の比較
持続性のサポートにおいてSpringとEJB 3.0の大きな違いが最初に現れるのは、各種の持続性フレームワークとの互換性かもしれません。SpringはさまざまなORMの実装をサポートしていますが、EJBが明示的にサポートしているのはJPAだけです。しかし、JPAは仕様に過ぎないので、Hibernate、Kodo、TopLinkといったさまざまなプロバイダがJPAをサポートしていることは憶えておく必要があります。表1に、よく知られたEJB 3.0アプリケーションサーバーにおけるデフォルトのJPAプロバイダを示します。
アプリケーションサーバー | JPA実装 |
JBoss | Hibernate |
BEA | Kodo(OpenJPA) |
Oracle | TopLink |
Glassfish | TopLink |
この意味では、どちらのテクノロジにも持続性プロバイダの選択肢が用意されていることになります。JPA仕様に含まれていないプロバイダ固有の機能を使う必要がある場合、EJBアプリケーション内でそうすることは確かに可能ですが、プラットフォームの相互互換性を損なう可能性があります。
ORMツールを扱う場合に重要な別の概念として、キャッシュ処理があります。SpringやEJB 3.0を検討する際にキャッシュが重要なのは、複数のコンポーネントを1つの作業単位上で協調させるためにはコンポーネント間でキャッシュを共有しなければならないからです。Springでは通常、スレッドローカル変数を用いてセッション(およびトランザクション)をスレッドにアタッチするアプリケーションによってこの処理が実現されます。Springにはこの実装を容易に行うクラスが用意されていますが、それでもやはり開発チームによる対処が必要になります。これに対し、EJB 3.0では持続性コンテキストがトランザクションに自動的に伝搬されます。
多くの場合、オブジェクトモデルは内容に富んだ複雑なグラフです。そのため、持続性エンティティをデータベースから取得するたびにその関係をすべて自動的にフェッチするのは非効率的です。この問題を解決するためにORMプロバイダは多くの場合、実際にデータが必要になるまで特定のデータベース操作を保留する遅延初期化(lazy initialization=怠惰な初期化)のような機能を提供しています。これにより、不必要かつコストのかかる多くのデータベース呼び出しが回避されるものの、アプリケーションのロジックがかなり複雑になる可能性があります。コレクションへのアクセス時に、遅延初期化がなされたコレクションに関連するキャッシュがクローズされていると、例外がスローされます。
Spring/Hibernateがとっているアプローチは、ビューの作成に応じてキャッシュのオープンおよびクローズを行うものです。Springにはこれを簡単化するOpenSessionInViewFilter(およびインターセプタ)が用意されていますが、これには設定が必要です。EJB 3.0仕様では、持続性コンテキストに興味深いスコープが追加されており、ステートフルセッションBeanの存続期間を上限としてキャッシュの存続期間を制限することが可能です。これは拡張持続性コンテキスト(extended persistence context)と呼ばれ、その指定はステートフルセッションBeanにおいて持続性コンテキストに注釈を付けることによって行います。この拡張持続性コンテキストにより、長いアプリケーショントランザクションや怠惰な読み込みが行われたコレクションの処理を単純化できます。
持続性 - まとめ
JPAは差し込み可能(pluggable)な持続性プロバイダをサポートしており、Springはこうしたプロバイダを直接サポートしているため、EJBとSpringのアプローチは非常に類似していると言えます。表2に主な比較のポイントをまとめています。
機能 | Spring | EJB 3.0 |
簡単なORMの持続性 | √ | √ |
実装 | Hibernate、JPA、JDO、TopLink、iBatis | JPA(Hibernate、Kodo、TopLinkを含む各種プロバイダ) |
JDBCサポート | √ | -- |
マッピング | XML、注釈 | 注釈、XML |
キャッシュの伝播 | スレッドローカル | トランザクション |
拡張キャッシュスコーピング | ビューにおけるオープンセッション | 拡張持続性コンテキスト |
標準 | √(JPAを使用する場合) | √ |