1. 概要

このチュートリアルでは、Hibernateプロキシを実際のエンティティオブジェクトに変換する方法を学習します。 その前に、Hibernateがいつプロキシオブジェクトを作成するかを理解します。 次に、Hibernateプロキシが役立つ理由について説明します。 最後に、オブジェクトのプロキシを解除する必要があるシナリオをシミュレートします。

2. Hibernateはいつプロキシオブジェクトを作成しますか?

Hibernateはプロキシオブジェクトを使用して遅延読み込みを許可します。シナリオをより適切に視覚化するために、PaymentReceiptおよびPaymentエンティティを見てみましょう。

@Entity
public class PaymentReceipt {
    ...
    @OneToOne(fetch = FetchType.LAZY)
    private Payment payment;
    ...
}
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    protected WebUser webUser;
    ...
}

たとえば、これらのエンティティのいずれかをロードすると、 FetchType.LAZYを使用して関連付けられたフィールドのプロキシオブジェクトをHibernateで作成します。

実例を示すために、統合テストを作成して実行してみましょう。

@Test
public void givenPaymentReceipt_whenAccessingPayment_thenVerifyType() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    Assert.assertTrue(paymentReceipt.getPayment() instanceof HibernateProxy);
}

テストから、 PaymentReceipt をロードし、支払いオブジェクトがCreditCardPayment のインスタンスではないことを確認しました—それはHibernateProxyオブジェクトです。

対照的に、遅延読み込みがないと、返される支払いオブジェクトは CreditCardPayment のインスタンスであるため、前のテストは失敗します。

さらに、Hibernateがバイトコードinstrumentationを使用してプロキシオブジェクトを作成していることにも言及する価値があります。

これを確認するために、統合テストのアサーションステートメントの行にブレークポイントを追加して、デバッグモードで実行できます。 次に、デバッガーが何を表示するかを見てみましょう。

paymentReceipt = {PaymentReceipt@5042} 
 payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
  $$_hibernate_interceptor = {ByteBuddyInterceptor@5053} 

デバッガーから、Hibernateが Byte Buddy を使用していることがわかります。これは、実行時にJavaクラスを動的に生成するためのライブラリです。

3. Hibernateプロキシが役立つのはなぜですか?

3.1. 遅延読み込み用のHibernateプロキシ

これについては以前に少し学びました。 さらに重要な意味を持たせるために、PaymentReceiptエンティティとPaymentエンティティの両方から遅延読み込みメカニズムを削除してみましょう。

public class PaymentReceipt {
    ...
    @OneToOne
    private Payment payment;
    ...
}
public abstract class Payment {
    ...
    @ManyToOne
    protected WebUser webUser;
    ...
}

それでは、 PaymentReceipt をすばやく取得して、ログから生成されたSQLを確認しましょう。

select
    paymentrec0_.id as id1_2_0_,
    paymentrec0_.payment_id as payment_3_2_0_,
    paymentrec0_.transactionNumber as transact2_2_0_,
    payment1_.id as id1_1_1_,
    payment1_.amount as amount2_1_1_,
    payment1_.webUser_id as webuser_3_1_1_,
    payment1_.cardNumber as cardnumb1_0_1_,
    payment1_.clazz_ as clazz_1_,
    webuser2_.id as id1_3_2_,
    webuser2_.name as name2_3_2_ 
from
    PaymentReceipt paymentrec0_ 
left outer join
    (
        select
            id,
            amount,
            webUser_id,
            cardNumber,
            1 as clazz_ 
        from
            CreditCardPayment 
    ) payment1_ 
        on paymentrec0_.payment_id=payment1_.id 
left outer join
    WebUser webuser2_ 
        on payment1_.webUser_id=webuser2_.id 
where
    paymentrec0_.id=?

ログからわかるように、 のクエリ お支払いの領収書 複数のjoinステートメントが含まれています。 

それでは、遅延読み込みを適切に実行して実行しましょう。

select
    paymentrec0_.id as id1_2_0_,
    paymentrec0_.payment_id as payment_3_2_0_,
    paymentrec0_.transactionNumber as transact2_2_0_ 
from
    PaymentReceipt paymentrec0_ 
where
    paymentrec0_.id=?

明らかに、生成されたSQLは、不要な結合ステートメントをすべて省略することで単純化されています。

3.2. データを書き込むためのHibernateプロキシ

説明のために、 Payment を作成し、それにWebUserを割り当てるために使用してみましょう。 プロキシを使用しない場合、2つのSQLステートメントが生成されます。[X106X]WebUserを取得するSELECTステートメントと、PaymentINSERTステートメントです。作成。

プロキシを使用してテストを作成しましょう。

@Test
public void givenWebUserProxy_whenCreatingPayment_thenExecuteSingleStatement() {
    entityManager.getTransaction().begin();

    WebUser webUser = entityManager.getReference(WebUser.class, 1L);
    Payment payment = new CreditCardPayment(new BigDecimal(100), webUser, "CN-1234");
    entityManager.persist(payment);

    entityManager.getTransaction().commit();
    Assert.assertTrue(webUser instanceof HibernateProxy);
}

entityManager.getReference(…)を使用してプロキシオブジェクトを取得していることを強調する価値があります。

次に、テストを実行してログを確認しましょう。

insert 
into
    CreditCardPayment
    (amount, webUser_id, cardNumber, id) 
values
    (?, ?, ?, ?)

ここで、プロキシを使用する場合、Hibernateが実行したステートメントは1つだけであることがわかります。 入れる の声明 支払い 作成。

4. シナリオ:プロキシ解除の必要性

ドメインモデルを前提として、 PaymentReceiptを取得していると仮定します。すでに知っているように、継承戦略がTable-per-のPaymentエンティティに関連付けられています。クラスとレイジーフェッチタイプ

この場合、入力されたデータに基づいて、PaymentReceiptの関連付けられたPaymentはタイプCreditCardPaymentです。ただし、遅延読み込みを使用しているため、プロキシオブジェクトになります。

それでは、 クレジットカードでの支払い 実在物:

@Entity
public class CreditCardPayment extends Payment {
    
    private String cardNumber;
    ...
}

確かに、cを取得することはできません ardNumber からのフィールドクレジットカードでの支払いプロキシを解除せずにクラス支払い物体。 とにかく、キャストしてみましょう支払いオブジェクトにクレジットカードでの支払い何が起こるか見てみましょう

@Test
public void givenPaymentReceipt_whenCastingPaymentToConcreteClass_thenThrowClassCastException() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    assertThrows(ClassCastException.class, () -> {
        CreditCardPayment creditCardPayment = (CreditCardPayment) paymentReceipt.getPayment();
    });
}

テストから、キャストする必要があることがわかりました 支払い オブジェクトに クレジットカードでの支払い。 ただし、なぜなら the 支払い オブジェクトはまだHibernateプロキシオブジェクトです。 ClassCastException.

5. エンティティオブジェクトへのHibernateプロキシ

Hibernate 5.2.10以降、組み込みの静的メソッドを使用してHibernateエンティティのプロキシを解除できます。

Hibernate.unproxy(paymentReceipt.getPayment());

このアプローチを使用して、最終的な統合テストを作成しましょう。

@Test
public void givenPaymentReceipt_whenPaymentIsUnproxied_thenReturnRealEntityObject() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    Assert.assertTrue(Hibernate.unproxy(paymentReceipt.getPayment()) instanceof CreditCardPayment);
}

テストから、Hibernateプロキシを実際のエンティティオブジェクトに正常に変換したことがわかります。

一方、Hibernate5.2.10より前のソリューションは次のとおりです。

HibernateProxy hibernateProxy = (HibernateProxy) paymentReceipt.getPayment();
LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
CreditCardPayment unproxiedEntity = (CreditCardPayment) initializer.getImplementation();

6. 結論

このチュートリアルでは、Hibernateプロキシを実際のエンティティオブジェクトに変換する方法を学びました。 それに加えて、Hibernateプロキシがどのように機能し、なぜそれが役立つのかについても説明しました。 次に、オブジェクトのプロキシを解除する必要がある状況をシミュレートしました。

最後に、いくつかの統合テストを実行して、例を示し、ソリューションを検証しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。