Работа с Kotlin и JPA

1. Введение

Одной из характеристик Kotlin является совместимость с библиотеками Java, и JPA, безусловно, является одной из них.

В этом руководстве мы рассмотрим, как использовать классы Kotlin в качестве объектов JPA.

1. Введение  Одной из характеристик Kotlin является совместимость с библиотеками Java, и JPA, безусловно, является одной из них.

2. Зависимости

Чтобы упростить задачу, мы будем использовать Hibernate в качестве нашей реализации JPA; нам нужно будет добавить следующие зависимости в наш проект 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</artifactId>
<version>1.8.0</version>
</dependency>

3. Плагины для компилятора (jpa-plugin)

Чтобы использовать JPA, классам сущностей нужен конструктор без параметров.

По умолчанию в классах Kotlin его нет, и для их создания нам потребуется использовать jpa-плагин:

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

4. JPA с классами Kotlin

После выполнения предыдущей настройки мы готовы использовать JPA с простыми классами.

Давайте начнем создавать класс Person с двумя атрибутами – name и id, вот так:

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

Как мы видим, мы можем свободно использовать аннотации из JPA, такие как @Entity, @Column и @Id.

Примечание: не забудьте поместить атрибут 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-плагин во время выполнения, мы получим исключение InstantiationException из-за отсутствия конструктора по умолчанию:

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

Теперь мы снова проведем тестирование с нулевыми значениями. Для этого давайте расширим нашу сущность Person новым атрибутом email и отношением @OneToMany:

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

Мы также можем видеть, что поля email и phone Numbers имеют значение null, поэтому они объявлены со знаком вопроса.

Объект PhoneNumber имеет два атрибута – name и id:

@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)
})
}

На этот раз мы получим одну инструкцию insert:

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=?

Давайте протестируем еще раз, но без нулевых данных, чтобы проверить результат:

@Test
fun givenPersonWithFullData_whenSaved_thenFound() {
doInHibernate(({ this.sessionFactory() }), { session ->
val personToSave = Person(
"John",
"jhon@test.com",
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)
})
}

И, как мы можем видеть, теперь мы получаем три инструкции insert:

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. Методы equals и hashCode

Давайте начнем с реализаций equals и hashCode. Большинство объектов JPA содержат по крайней мере одно сгенерированное значение — например, автоматически сгенерированные идентификаторы. Это означает, что некоторые свойства генерируются только после того, как мы сохраняем их в базе данных.

Таким образом, вычисляемые значения equals и хэш-код отличаются до и после сохранения, поскольку некоторые свойства, используемые при вычислении equals и хэш-кода, генерируются после сохранения. Поэтому мы должны быть осторожны при использовании объектов 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 }
}
}

В приведенном выше примере, несмотря на то, что мы добавили адрес в набор, мы не можем найти его в наборе после сохранения в базе данных. Это происходит потому, что значение хэш-кода меняется после сохранения адреса.

В дополнение к этому, реализации simple 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. Заключение

В этой краткой статье мы рассмотрели пример того, как интегрировать классы Kotlin с JPA с помощью jpa-плагина и Hibernate. Кроме того, мы поняли, почему мы должны быть осмотрительны при использовании классов данных в качестве объектов JPA.

Оригинал статьи: https://www.baeldung.com/kotlin/jpa