Одной из характеристик Kotlin является совместимость с библиотеками Java. JPA является одной из них.
В этом руководстве рассмотрим, как использовать классы Kotlin в качестве сущностей JPA.
Зависимости
Для простоты будем использовать 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-jdk8</artifactId>
<version>1.2.30</version>
</dependency>
Самые последние версии Hibernate, H2 и Kotlin можно найти в Maven Central.
Плагины компилятора (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>
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-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 имеет два атрибута – 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, ?)
Классы Data
Классы Data Kotlin – это обычные классы с дополнительными функциями, которые делают их подходящими в качестве держателей данных. Среди этих дополнительных функций есть реализации по умолчанию для методов equals, hashCode и toString.
Можно возразить, что можно использовать классы Data Kotlin в качестве сущностей JPA. В отличие от того, что здесь происходит естественным образом, использование классов Data в качестве сущностей JPA обычно не рекомендуется. Это в основном из-за сложных взаимодействий между миром JPA и теми реализациями по умолчанию, которые компилятор Kotlin предоставляет для каждого класса Data.
Для демонстрации будем использовать эту сущность:
@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,
)
Методы equals и hashCode
Начнем с реализации equals и hashCode. Большинство сущностей JPA содержат как минимум одно сгенерированное значение – например, автоматически сгенерированные идентификаторы. Это означает, что некоторые свойства генерируются только после того, как сохраняем их в базе данных.
Таким образом, вычисляемые equals и hashCode отличаются до и после сохранения, поскольку некоторые свойства, используемые во время вычислений equals и hashCode, генерируются после сохранения. Поэтому нужно быть осторожными при использовании сущностей JPA класса Data с коллекциями на основе хэшей:
@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.
Нежелательное получение lazy-связей
Компилятор Kotlin генерирует реализации методов по умолчанию на основе всех свойств класса Data. Когда используем классы Data для объектов JPA, некоторые из этих свойств могут быть lazy-связью с целевым объектом. Учитывая все это, иногда безобидный вызов toString, equals или hashCode может вызвать еще несколько запросов для загрузки lazy-связи. Это может повредить производительности, особенно если не нужно извлекать эти связи:
@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=?
Заключение
В этой статье рассмотрели пример интеграции классов Kotlin с JPA с помощью jpa-plugin и Hibernate. Более того, увидели, почему следует быть осмотрительными при использовании классов Data в качестве сущностей JPA.
Исходный код доступен на GitHub.