JAXBと org.hibernate.LazyInitializationException

この記事は、JAXBがHibernateを使用して作成されたオブジェクトをシリアル化するときに、LazyInitializationExceptionエラーを解決する方法を学ぶことに興味がある人に役立ちます。

記事の最後に、提案されたソリューションを実装するプロジェクトのソースコードへのリンクがあります-カスタムAccessorFactoryの使用。



比較のために、人気のあるJSONシリアライザーであるJacksonで同様の問題がどのように解決されたかを考えます。



1.実際、問題は何ですか?
抽象プロジェクトでは、リレーショナルDBMSの制御下にあるデータベースに、3つのテーブルに企業、サプライヤー、顧客に関するデータが格納されます。



画像



2つのRESTサービスを開発するとします。1つ目は会社とそのサプライヤーに関するデータを返し、2つ目は会社とその顧客に関するデータを返します。

(注:データを提供する会社。ID= 0、コンテンツタイプ-拡張子:/HLS/rest/company/suppliers.xmlでデータベースを簡素化するためにさらに決定します-XMLでサプライヤーデータを取得します。

HLS-テストアプリケーションのコンテキストパス:休止状態の遅延シリアル化。 もっと賢いものは思いつきませんでした。



顧客は、XMLおよびJSON形式のデータを受け取りたいと考えていました。 X、Y、Zの理由により、プロジェクトチームは、データにアクセスするためにORMを使用し、XMLを生成するためにJAXBを使用し、JSONを生成するためにJacksonを使用することにしました。



以上で、コーディングを開始します。



package ru.habr.zrd.hls.domain; ... @Entity @Table(name = "COMPANY") @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Company { @Id @GeneratedValue private Integer id; @Column(name = "S_NAME") private String name; @OneToMany @JoinColumn(name = "ID_COMPANY") @XmlElementWrapper //     @XmlElement(name = "supplier") private Set<Supplier> suppliers; @OneToMany @JoinColumn(name = "ID_COMPANY") @XmlElementWrapper //     @XmlElement(name = "customer") private Set<Customer> customers; // Getters/setters
      
      





Customer.java Supplier.javaのコードは提供しません。特別なものはありません。

package-info.javaで、2つのフェッチプロファイルを定義します。



 @FetchProfiles({ @FetchProfile(name = "companyWithSuppliers", fetchOverrides = { @FetchProfile.FetchOverride(entity = Company.class, association = "suppliers", mode = FetchMode.JOIN), }), @FetchProfile(name = "companyWithCustomers", fetchOverrides = { @FetchProfile.FetchOverride(entity = Company.class, association = "customers", mode = FetchMode.JOIN) }) }) package ru.habr.zrd.hls.domain;
      
      





「companyWithSuppliers」がデータベースからサプライヤをプルし、バイヤーを初期化しないままにすることは簡単にわかります。 2番目のプロファイルは反対のことを行います。

DAOでは、呼び出されるサービスに応じて、目的のフェッチプロファイルを設定します。



 ... public class CompanyDAO { public Company getCompany(String fetchProfile) { ... Session session = sessionFactory.getCurrentSession(); session.enableFetchProfile(fetchProfile); Company company = (Company) session.get(Company.class, 0); ... return company; } ...
      
      





JSONを始めましょう。 CompanyDAO.getCompany()メソッドによって返されたオブジェクトを標準のObjectMapper Jacksonでシリアル化しようとすると失敗します。



画像



残念ながら、非常に期待されています。 セッションが閉じられ、サプライヤコレクションをラップするHibernateプロキシはデータベースからデータをプルできません。 ジャクソンがそのような初期化されていないフィールドを特別な方法で処理できたら素晴らしいでしょう...



そして、そのような解決策があります: jackson-module-hibernate- 「Hibernate <...>データ型を処理するJackson JSONプロセッサ用のアドオンモジュール。 特に遅延読み込みの側面」。 必要なもの! ObjectMapperを修正しましょう:



 import org.codehaus.jackson.map.ObjectMapper; import com.fasterxml.jackson.module.hibernate.HibernateModule; public class JSONHibernateObjectMapper extends ObjectMapper { public JSONHibernateObjectMapper() { registerModule(new HibernateModule()); // ,  ,     //  -  property, .    . } }
      
      





CompanyDAO.getCompany()の結果を新しいマッパーでシリアル化します。



画像



まあ、それはうまくいきました-最終的なJSONでは、バイヤーだけがいてサプライヤはいません-初期化されていないコレクションは単純に無効にされます。 欠点のうち、Hibernate4のサポートがないことに注意する価値がありますが、GitHubの情報から判断すると、この機能は開発中です。 JAXBに渡します。



JAXB開発者は、子孫が何らかのHibernateレイジーローディングの友人ではないことを心配するにはグローバルすぎると考え、問題を解決するための定期的な手段を提供しませんでした。







どうする プロジェクトはほとんど失敗です。







そしてGoogleは言った:



2. LazyInitializationException:問題を解決するための一般的な方法


  1. 遅延コレクションを作成しないでください-FetchMode.JOIN(FetchType.EAGER)を使用してください。

    いいえ、このオプションは適切ではありません。両方のコレクション(サプライヤと顧客)を遅延させる必要があります。 次に、どのサービスを呼び出すかは問題ではないことがわかります:... / supplier.xmlまたは... / customers.xml-受信したXMLには、サプライヤーと顧客の両方に関するデータが同時に含まれます。
  2. 怠zyなコレクションを台無しにしないでください-@XmlTransientを使用してください(もちろん、このアノテーションの使用について一般的にお勧めする場合)。

    いいえ、このオプションは適切ではありません。コレクション(サプライヤと顧客)の両方に@XmlTransientのラベルを付ける必要があります。 次に、どのサービスを呼び出すかは問題ではないことがわかります:... / supplier.xmlまたは... / customers.xml-受信したXMLには、顧客またはサプライヤーに関するデータは含まれません。
  3. X、Y、Zトリックを使用してセッションを閉じないでください(たとえば、HibernateInterceptorまたはOpenSessionInViewFilter-SpringおよびHibernate3の場合)。

    いいえ、このオプションは適切ではありません。 不要なデータは閉じられていないセッションから取得され、パラグラフ1のように見えます。
  4. DTOを使用します-DAOとの中間層ですか? (この場合?-シリアライザー)、状況をどこで解決しますか?

    可能ですが、特定のケースごとに独自のDTOを作成する必要があります。 一般に、DTOは一種のアンチパターンであるため、DTOの使用はより正当化されるべきです。 データの重複を引き起こします。
  5. オブジェクトグラフを「手動で」またはXYZツール(たとえば、Springを使用している場合はHibernateレイジーチョッパー)を使用して、DAOからオブジェクトを取得した後、レイジーコレクションを処理します。

    このオプションは悪くなく、普遍的であると主張していますが、シリアル化の場合、1つの問題が残っています-オブジェクトグラフを2回通過する必要があります:遅延コレクションを排除するために最初に、シリアライズ時にシリアル化がこれを行います。


理想的には、ジャクソンがそうであるように、シリアライザーは初期化されていないコレクションをそれ自体で遮断する必要があるという結論に達します。



3.カスタムJAXB AccessorFactory
とりわけ、Googleは最後にアクセスした2つのリンクを発行しました。

forum.hibernateおよびblogs.oracle

Ctrl + C / Ctrl + Vに適したソリューションが存在しないことと、これらの記事で不必要なすべての不必要な恐怖による過度の過負荷。 そのため、記事の内容を創造的に仕上げ、修正する必要がありました。 結果を以下に示します。

したがって、言及されたソースから、私たちがする必要があることは明らかです:
  1. AccessorFactoryの実装を記述します(このタイプのクラスは、マーシャリング/アンマーシャリング時にオブジェクトのフィールド/プロパティにアクセスするためにJAXBによって使用されます)
  2. JAXBに、AccessorFactoryのカスタム実装を使用する必要があることを伝えます。
  3. この実装の場所をJAXBに伝えます。


ポイントに行きましょう:



 ... import com.sun.xml.bind.AccessorFactory; import com.sun.xml.bind.AccessorFactoryImpl; import com.sun.xml.bind.api.AccessorException; import com.sun.xml.bind.v2.runtime.reflect.Accessor; public class JAXBHibernateAccessorFactory implements AccessorFactory { //  AccessorFactory   - AccessorFactoryImpl.    public // ,      ,     //   wrapper. private final AccessorFactory accessorFactory = AccessorFactoryImpl.getInstance(); //     Accessor.      ,  //    private inner class,     . private static class JAXBHibernateAccessor<B, V> extends Accessor<B, V> { private final Accessor<B, V> accessor; public JAXBHibernateAccessor(Accessor<B, V> accessor) { super(accessor.getValueType()); this.accessor = accessor; } @Override public V get(B bean) throws AccessorException { V value = accessor.get(bean); //  !    -.  -   //   ,     ,  // .  Hibernate.isInitialized() c   //    Hibernate3,  Hibernate4. return Hibernate.isInitialized(value) ? value : null; } @Override public void set(B bean, V value) throws AccessorException { accessor.set(bean, value); } } //   ,    inner Accessor. @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly) throws JAXBException { return new JAXBHibernateAccessor(accessorFactory.createFieldAccessor(bean, field, readOnly)); } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Accessor createPropertyAccessor(Class bean, Method getter, Method setter) throws JAXBException { return new JAXBHibernateAccessor(accessorFactory.createPropertyAccessor(bean, getter, setter)); } }
      
      





JAXBがカスタム実装の使用を開始するには、JAXBContextが特別なプロパティ「com.sun.xml.bind.XmlAccessorFactory」= trueを設定する必要があります。 (別名JAXBRIContext.XMLACCESSORFACTORY_SUPPORT)。これには@XmlAccessorFactoryアノテーションのサポートが含まれます。 Springを使用する場合、これは直接ではなく、「jaxbContextProperties」プロパティでBean「org.springframework.oxm.jaxb.Jaxb2Marshaller」を設定するときに実行できます。



最後に、パッケージレベルのアノテーション@XmlAccessorFactoryを使用して実装のクラスを示します。



 ... @XmlAccessorFactory(JAXBHibernateAccessorFactory.class) package ru.habr.zrd.hls.domain; import com.sun.xml.bind.XmlAccessorFactory; ...
      
      





これらの操作を実行した後、当社のサービスを利用して、会社と顧客に関する情報を取得します。







すべてがOKです-バイヤーのみでサプライヤーはありません。 プロバイダーで初期化されていないコレクションは、AccessorFactoryによって無効化されるため、JAXBはそれをシリアル化しようとせず、LazyInitializationExceptionは発生しません。 その後、美しさをもたらすことができます-代理キーを発行から削除するなど。しかし、これは別の記事です。



最終的には、約束されているように、記事のトピックに関する(Spring Web MVCでの)実際の例のソースコードへのリンクです。 組み込みのH2を使用します。これは、プロジェクトの開始時に自動的に構成されるため、別個のDBMSをインストールする必要はありません。 Eclipse + STSプラグインを使用する場合、アーカイブにはEclipseとSTS用に構成された別個のバージョンがあります。



それだけです。この記事が誰かに役立つことを願っています。



All Articles