As Java recently celebrates its 30th birthday this year, I'm excited to announce Kapper 1.5 with first-class support for Java Record classes!
While Kapper was designed with Kotlin in mind and provides a Kotlin-idiomatic API, support for other JVM languages was always an important objective.
The addition of Record support emphasizes this commitment to the broader JVM ecosystem.
This release also brings new customization options that make database mapping even more powerful and flexible, while maintaining the blazing-fast performance Kapper is known for.
🚀 What's New in Kapper 1.5
Java Record Support - Fast and Modern
The headline feature of Kapper 1.5 is native support for auto-mapping to Java Record classes.
Records, introduced in Java 14, provide a concise way to model immutable data - perfect for database entities.
Here's how simple it is to use Records with Kapper:
// Define your record
public record SuperHero(UUID id, String name, String email, int age) {}
// Query a list of heroes
try (Connection conn = dataSource.getConnection()) {
List<SuperHero> heroes = kapper.query(SuperHero.class, conn,
"SELECT * FROM super_heroes", Map.of());
}
// Find a single hero by ID
try (Connection conn = dataSource.getConnection()) {
SuperHero hero = kapper.querySingle(SuperHero.class, conn,
"SELECT * FROM super_heroes WHERE id = :id",
Map.of("id", heroId));
}
Custom Mappers for Ultimate Flexibility
When introducing support for Record mappers, I made a change to how mappers are registered.
This opened the door to custom mappers, allowing you to define exactly how your data should be mapped from the database to your objects, which is especially useful for performance critical use cases, or where complex queries or mapping logic is required.
By using custom mappers, you avoid the reflection overhead of auto-mapping, making Kapper even faster than other ORMs in these configurations.
Custom mappers are just code, nothing magical, so they are easy to understand, maintain and test.
You can register a custom mapper class:
data class SuperHero(val id: UUID, val name: String, val email: String? = null, val age: Int? = null)
// Custom mapper class
class SuperHeroMapper : Mapper<SuperHero> {
override fun createInstance(
resultSet: ResultSet,
fields: Map<String, Field>,
) = SuperHero(
id = UUID.fromString(resultSet.getString("id")),
name = resultSet.getString("name"),
email = resultSet.getString("email"),
age = resultSet.getInt("age"),
)
}
Kapper.mapperRegistry.registerIfAbsent<SuperHero>(SuperHeroMapper())
val heroes = connection.query<SuperHero>("SELECT * FROM super_heroes")
Or use a mapper lambda for inline customization:
val heroes = connection.query<SuperHero>(
"SELECT * FROM super_heroes",
{ resultSet, _ ->
SuperHero(
id = UUID.fromString(resultSet.getString("id")),
name = resultSet.getString("name"),
email = resultSet.getString("email"),
age = resultSet.getInt("age"),
)
}
)
📊 Performance That Impresses
Benchmark results show that Java Record mapping isn't just convenient - it's fast.
When loading and mapping 100 rows:
Library | Time (μs) |
---|---|
Raw JDBC | 73.7 |
Kapper Record | 106.9 |
Hibernate | 127.3 |
Kapper Data Class | 220.5 |
Ktorm | 685.6 |
Key Performance Insights
- Kapper Record auto-mapping outperforms Hibernate entity class mapping (106.9μs vs 127.3μs)
- Maintains competitive performance while giving you complete SQL control
- Continues to significantly outperform Ktorm by a substantial margin (685.6μs)
Note: Record mapping is currently faster than Kotlin data class mapping (220.5μs), though we plan to optimize data class performance in future releases.
The performance advantage of Records over Kotlin data classes likely stems from Java vs Kotlin reflection differences - an area I'm actively investigating for future optimizations.
_The benchmark was run on a M3 MacBook Air. Full details are available in the Kapper benchmark results repo, and the benchmark code can be found here.
🏆 Why Records + Kapper = Perfect Match
Hibernate's Record Limitations
While Hibernate is working on Record support, it's still limited and doesn't provide the seamless experience you get with Kapper.
With Kapper, Records work out of the box with zero configuration.
The Kapper Philosophy
Kapper doesn't try to replace SQL - it embraces it. You write the queries you need, and Kapper handles the mapping efficiently:
// Custom joins? No problem!
try (Connection conn = dataSource.getConnection()) {
List<SuperHeroBattle> battles = kapper.query(SuperHeroBattle.class, conn,
"""
SELECT s.name as superhero, v.name as villain, b.battle_date as date
FROM super_heroes s
INNER JOIN battles b ON s.id = b.super_hero_id
INNER JOIN villains v ON v.id = b.villain_id
WHERE s.name = :name
""",
Map.of("name", heroName));
}
🛠 Getting Started
Add Kapper 1.5 to your project:
Maven
<dependency>
<groupId>net.samyn</groupId>
<artifactId>kapper</artifactId>
<version>1.5.0</version>
</dependency>
Gradle
dependencies {
implementation("net.samyn:kapper:1.5.0")
}
🔗 Cross-Language Support
While we're celebrating Java Records, Kapper continues to excel with Kotlin data classes, offering a consistent API across both languages:
// Kotlin - still works beautifully
data class SuperHero(val id: UUID, val name: String, val email: String, val age: Int)
fun findHeroes(): List<SuperHero> = dataSource.connection.use {
it.query<SuperHero>("SELECT * FROM super_heroes")
}
Head to Kapper Examples for more code samples and documentation.
🎉 Happy 30th Birthday, Java!
Java's journey from Oak to the modern platform powering millions of applications worldwide has been remarkable.
Features like Records show Java's commitment to evolving while maintaining its core strengths.
I am a huge fan of the Kotlin language but wanted to show my appreciation for the Java ecosystem but making Java database programming more enjoyable and performant through Kapper 1.5.
Ready to try Kapper 1.5?
Check out the full release notes and examples to get started.
What do you think about Java Records? Have you tried them with database mapping? Let us know in the comments!
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.