1. 序章

Kotlinの特徴の1つは、Javaライブラリとの相互運用性です。JPAは確かにその1つです。

このチュートリアルでは、KotlinクラスをJPAエンティティとして使用する方法について説明します。

2. 依存関係

物事を単純にするために、JPA実装としてHibernateを使用します。 Mavenプロジェクトに次の依存関係を追加する必要があります。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.2.15.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-testing</artifactId>
    <version>5.2.15.Final</version>
    <scope>test</scope>
</dependency>

また、H2組み込みデータベースを使用してテストを実行します。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
    <scope>test</scope>
</dependency>

Kotlinの場合、以下を使用します。

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    <version>1.2.30</version>
</dependency>

もちろん、 Hibernate H2 、およびKotlinの最新バージョンはMavenCentralにあります。

3. コンパイラプラグイン(jpa-plugin)

JPAを使用するには、エンティティークラスにパラメーターのないコンストラクターが必要です。

デフォルトでは、Kotlinクラスにはそれがありません。それらを生成するには、jpa-pluginを使用する必要があります。

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>1.2.30</version>
    <configuration>
        <compilerPlugins>
        <plugin>jpa</plugin>
        </compilerPlugins>
        <jvmTarget>1.8</jvmTarget>
    </configuration>
    <dependencies>
        <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-noarg</artifactId>
        <version>1.2.30</version>
        </dependency>
    </dependencies>
    <!--...-->
</plugin>

4. Kotlinクラスを使用したJPA

前のセットアップが完了すると、単純なクラスでJPAを使用する準備が整います。

次のように、nameidの2つの属性を持つPersonクラスの作成を開始しましょう。

@Entity
class Person(
    @Column(nullable = false)
    val name: String,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int?=null,
)

ご覧のとおり、 @Entity @Column @IdなどのJPAのアノテーションを自由に使用できます。

注:id属性はオプションであり、自動生成されるため、必ず最後の場所に配置してください。

エンティティの動作を確認するために、次のテストを作成します。

@Test
fun givenPerson_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person("John")
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave.name == personFound.name)
    })
}

ロギングを有効にしてテストを実行すると、次の結果が表示されます。

Hibernate: insert into Person (id, name) values (null, ?)
Hibernate: select person0_.id as id1_0_0_, person0_.name as name2_0_0_ from Person person0_ where person0_.id=?

これは、すべてが順調に進んでいることを示しています。

実行時にjpa-pluginを使用しない場合、デフォルトのコンストラクターがないため、InstantiationExceptionが発生することに注意してください:

javax.persistence.PersistenceException: org.hibernate.InstantiationException: No default constructor for entity: : com.baeldung.entity.Person

ここで、null値を使用して再度テストします。 これを行うには、Personエンティティを新しい属性email@OneToManyの関係で拡張しましょう。

    //...
    @Column(nullable = true)
    val email: String? = null,

    @Column(nullable = true)
    @OneToMany(cascade = [CascadeType.ALL])
    val phoneNumbers: List<PhoneNumber>? = null

emailおよびphoneNumbersフィールドはnull可能であるため、疑問符で宣言されていることもわかります。

PhoneNumber エンティティには、nameidの2つの属性があります。

@Entity
class PhoneNumber(   
    @Column(nullable = false)
    val number: String,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int?=null,
)

テストでこれを確認しましょう:

@Test
fun givenPersonWithNullFields_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person("John", null, null)
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave.name == personFound.name)
    })
}

今回は、1つの挿入ステートメントを取得します。

Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: select person0_.id as id1_0_1_, person0_.email as email2_0_1_, person0_.name as name3_0_1_, phonenumbe1_.Person_id as Person_i1_1_3_, phonenumbe2_.id as phoneNum2_1_3_, phonenumbe2_.id as id1_2_0_, phonenumbe2_.number as number2_2_0_ from Person person0_ left outer join Person_PhoneNumber phonenumbe1_ on person0_.id=phonenumbe1_.Person_id left outer join PhoneNumber phonenumbe2_ on phonenumbe1_.phoneNumbers_id=phonenumbe2_.id where person0_.id=?

もう一度テストしてみましょう。ただし、 null データを使用せずに、出力を確認します。

@Test
fun givenPersonWithFullData_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person(          
          "John", 
          "[email protected]", 
          Arrays.asList(PhoneNumber("202-555-0171"), PhoneNumber("202-555-0102")))
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave.name == personFound.name)
    })
}

そして、ご覧のとおり、3つの挿入ステートメントがあります。

Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)

5. データクラス

Kotlinデータクラスは、データホルダーとして適した追加機能を備えた通常のクラスです。 これらの追加関数の中には、 equals hashCode 、およびtoStringメソッドのデフォルトの実装があります。

当然、KotlinデータクラスをJPAエンティティとして使用できると主張するかもしれません。 ここで自然に発生することとは対照的に、JPAエンティティとしてデータクラスを使用することは一般的に推奨されていません。 これは主に、JPAの世界と、データクラスごとにKotlinコンパイラによって提供されるデフォルトの実装との間の複雑な相互作用によるものです。

デモンストレーションのために、このエンティティを使用します。

@Entity
data class Address(    
    val name: String,
    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
    val phoneNumbers: List<PhoneNumber>,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int? = null,
)

5.1. およびhashCodeメソッドに等しい

hashCodeの実装に等しいことから始めましょう。 ほとんどのJPAエンティティには、自動生成された識別子など、少なくとも1つの生成された値が含まれています。 これは、一部のプロパティは、データベースに永続化した後にのみ生成されることを意味します。

したがって、equalsおよびhashCodeの計算中に使用される一部のプロパティは永続化後に生成されるため、計算されたequalsとhashCodeは永続化の前後で異なります。 したがって、ハッシュベースのコレクションでデータクラスJPAエンティティを使用する場合は注意が必要です。

@Test
fun givenAddressWithDefaultEquals_whenAddedToSet_thenNotFound() {
    doInHibernate({ sessionFactory() }) { session ->
        val addresses = mutableSetOf<Address>()
        val address = Address(name = "Berlin", phones = listOf(PhoneNumber("42")))
        addresses.add(address)

        assertTrue(address in addresses)
        session.persist(address)
        assertFalse { address in addresses }
    }
 }

上記の例では、アドレスをセットに追加しましたが、データベースに永続化した後、セット内でアドレスを見つけることができません。これは、アドレスを永続化した後にハッシュコード値が変更されるために発生します。

これに加えて、単純なequalsおよびhashCode実装は、JPAエンティティで使用するには十分ではありません。

5.2. 怠惰な関連付けの不要なフェッチ

Kotlinコンパイラは、データクラスのすべてのプロパティに基づいてデフォルトのメソッド実装を生成します。 JPAエンティティにデータクラスを使用している場合、これらのプロパティの一部は、ターゲットエンティティとの遅延関連付けである可能性があります。

これらすべてを考慮すると、 toString equals 、または hashCode を無害に呼び出すと、レイジーアソシエーションをロードするためにさらにいくつかのクエリが発行される場合があります。 これは、特にこれらの関連付けをフェッチする必要がない場合に、パフォーマンスを低下させる可能性があります。

@Test
fun givenAddress_whenLogging_thenFetchesLazyAssociations() {
    doInHibernate({ this.sessionFactory() }) { session ->
        val addressToSave = Address(name = "Berlin", phoneNumbers = listOf(PhoneNumber("42")))
        session.persist(addressToSave)
        session.clear()

        val addressFound = session.find(Address::class.java, addressToSave.id)
            
        assertFalse { Hibernate.isInitialized(addressFound.phoneNumbers) }
        logger.info("found the entity {}", addressFound) // initializes the lazy collection
        assertTrue(Hibernate.isInitialized(addressFound.phoneNumbers))
    }
}

上記の例では、一見無害に見えるログステートメントが追加のクエリをトリガーします。

Hibernate: select * from Address address0_ where address0_.id=?
Hibernate: select * from Address_PhoneNumber phonenumbe0_ inner join PhoneNumber phonenumbe1_ on phonenumbe0_.phoneNumbers_id=phonenumbe1_.id where phonenumbe0_.Address_id=?

6. 結論

この簡単な記事では、jpa-pluginとHibernateを使用してKotlinクラスをJPAと統合する方法の例を見ました。 さらに、データクラスをJPAエンティティとして使用するときに慎重にならなければならない理由もわかりました。

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