Skip to content

Commit 2d4d378

Browse files
committed
hibernate native alternatives testing
1 parent 233a4a6 commit 2d4d378

File tree

9 files changed

+872
-0
lines changed

9 files changed

+872
-0
lines changed

testing/testing-hibernate-native-alternatives/GEMINI_DEEP_RESEARCH.md

Lines changed: 234 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Native Hibernate Alternatives to `framefork/typed-ids`
2+
3+
This module provides concrete implementations and integration tests demonstrating native JPA/Hibernate approaches for strongly-typed identifiers, specifically focusing on the limitations of using `@EmbeddedId` and `@IdClass` with database-generated keys.
4+
5+
## Overview
6+
7+
This module demonstrates the **actual runtime behavior** of native alternatives when attempting to use database-generated identifiers, providing empirical evidence of their limitations compared to the `framefork/typed-ids` library.
8+
9+
## What `framefork/typed-ids` Library Solves
10+
11+
The `framefork/typed-ids` library provides a comprehensive solution for strongly-typed identifiers that addresses the following challenges:
12+
13+
### Core Features
14+
- **Compile-time Type Safety** - Prevents accidental ID misuse (e.g., passing `OrderId` where `UserId` expected)
15+
- **Minimal Boilerplate** - Simple inheritance from base classes with automatic registration
16+
- **Application-side ID Generation** - Built-in UUIDv7 and TSID generation for optimal database performance
17+
- **Database-side ID Generation** - Full `@GeneratedValue` support (IDENTITY, SEQUENCE, AUTO) for `ObjectBigIntId`
18+
- **Transparent Querying** - Direct JPQL/HQL usage: `WHERE entity.id = :id` (no property dereferencing)
19+
- **Clean Serialization** - Automatic JSON serialization to primitive values via auto-discoverable Jackson modules
20+
- **Proper OpenAPI Schemas** - Generates primitive type schemas (string/integer) in API documentation
21+
- **Database Optimization** - Intelligent column type selection per database dialect (e.g., native UUID on PostgreSQL)
22+
- **Ecosystem Integration** - Out-of-box support for Jackson, Gson, Kotlinx Serialization, SpringDoc
23+
24+
### Advanced Capabilities
25+
- **Automatic Type Registration** - Compile-time indexing eliminates manual `@Type` annotations
26+
- **Custom ID Generators** - Seamless integration with Hibernate's internal generator system
27+
- **Multiple Hibernate Versions** - Dedicated modules for different Hibernate versions
28+
- **Zero Configuration** - ServiceLoader-based auto-discovery for all integrations
29+
30+
## Alternative Approaches Analysis
31+
32+
### 1. @EmbeddedId with Java Records
33+
34+
**What it solves:**
35+
- ✅ Compile-time Type Safety
36+
- ✅ Minimal Boilerplate (record syntax)
37+
- ✅ JPA Standard Compliance
38+
- ✅ Transparent Querying (supports both `entity.id = :idObject` and `entity.id.value = :primitiveValue`)
39+
40+
**What it doesn't solve:**
41+
- ❌ Database-side ID Generation (`@GeneratedValue` fails with composite ID error)
42+
- ❌ Clean Serialization (produces nested JSON objects by default)
43+
- ❌ Proper OpenAPI Schemas (generates complex object schemas)
44+
- ❌ Ecosystem Integration (manual serializers required)
45+
- ❌ Application-side ID Generation (no built-in generators)
46+
- ❌ Database Optimization (no dialect-specific column types)
47+
- ❌ Automatic Type Registration (N/A)
48+
49+
**Test Results in this module:**
50+
- ✅ Schema Generation: Creates `auto_increment` column
51+
- ❌ Runtime: `IdentifierGenerationException: Identity generation isn't supported for composite ids`
52+
- ✅ JPQL Querying: Both `WHERE e.id = :embeddableObject` and `WHERE e.id.value = :primitiveValue` work
53+
- ✅ SELECT NEW Constructor: Supports both direct embedded object mapping and inline constructor calls
54+
55+
### 2. @IdClass with Java Records
56+
57+
**What it solves:**
58+
- ✅ Compile-time Type Safety
59+
- ✅ JPA Standard Compliance
60+
- ✅ Transparent Querying (direct field access in JPQL)
61+
62+
**What it doesn't solve:**
63+
- ❌ Database-side ID Generation (same composite ID limitation)
64+
- ❌ Minimal Boilerplate (requires both record and entity field mapping)
65+
- ❌ Clean Serialization (entity structure depends on implementation)
66+
- ❌ Proper OpenAPI Schemas (depends on entity serialization)
67+
- ❌ Ecosystem Integration (manual configuration required)
68+
- ❌ Application-side ID Generation (no built-in generators)
69+
- ❌ Database Optimization (no dialect-specific optimizations)
70+
- ❌ Automatic Type Registration (N/A)
71+
72+
**Test Results in this module:**
73+
- ✅ Schema Generation: Creates `auto_increment` column
74+
- ❌ Runtime: `IdentifierGenerationException: Identity generation isn't supported for composite ids`
75+
- ✅ JPQL Querying: Direct field access works (`WHERE e.value = :primitiveValue`) but object comparison fails
76+
- ✅ SELECT NEW Constructor: Supports primitive value mapping and inline IdClass object construction, but cannot auto-convert primitives to IdClass objects
77+
78+
### 3. JPA AttributeConverter (Not Implemented)
79+
80+
**What it solves:**
81+
- ✅ Compile-time Type Safety
82+
- ✅ Transparent Querying
83+
- ✅ Clean Serialization (primitive field exposure)
84+
85+
**What it doesn't solve:**
86+
- ❌ JPA Standard Compliance (explicitly forbidden for `@Id` fields)
87+
- ❌ Database-side ID Generation (workarounds required)
88+
- ❌ Minimal Boilerplate (requires converter per ID type)
89+
- ❌ Proper OpenAPI Schemas (depends on workaround implementation)
90+
- ❌ Ecosystem Integration (manual configuration required)
91+
- ❌ Application-side ID Generation (no built-in generators)
92+
- ❌ Database Optimization (no dialect-specific optimizations)
93+
- ❌ Automatic Type Registration (N/A)
94+
95+
**Why not implemented:** JPA specification explicitly prohibits `@Convert` on `@Id` fields. While some implementations like modern Hibernate may not reject this at startup, the behavior is undefined and non-portable across JPA providers.
96+
97+
## Empirical Test Results
98+
99+
### Common Failure Pattern
100+
Both `@EmbeddedId` and `@IdClass` approaches exhibit the same failure pattern:
101+
102+
1. **Schema Generation Succeeds** - Hibernate creates proper `auto_increment` columns
103+
2. **Entity Compilation Succeeds** - No compile-time errors or warnings
104+
3. **Runtime Failure** - `IdentifierGenerationException` during `persist()` and `flush()`
105+
106+
### Root Cause
107+
Hibernate treats both approaches as **composite identifiers** even when using single fields:
108+
- `@EmbeddedId`: Embedded object is inherently composite from Hibernate's perspective
109+
- `@IdClass`: Any use of `@IdClass` signals composite identity to Hibernate
110+
111+
The error message is identical: `"Identity generation isn't supported for composite ids"`
112+
113+
## Conclusion
114+
115+
This empirical analysis demonstrates that **native JPA/Hibernate approaches fail** when combining strongly-typed identifiers with database-generated keys.
116+
While schema generation succeeds, runtime failures occur due to Hibernate's treatment of these patterns as composite identifiers.
117+
118+
**Key Findings:**
119+
1. Native approaches work only with **application-generated identifiers**
120+
2. Database-generated keys require significant compromises in other areas
121+
3. No native approach provides the comprehensive feature set of `framefork/typed-ids`
122+
4. Integration testing reveals gaps between theoretical capabilities and practical limitations
123+
124+
**Recommendation**: For production applications requiring both type safety and database-generated keys, `framefork/typed-ids` library provides the only viable comprehensive solution.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id("framefork.java")
3+
}
4+
5+
dependencies {
6+
// Need Hibernate for JPA annotations, but not the typed-ids implementations
7+
implementation(libs.hibernate.orm.v66)
8+
compileOnly(libs.jetbrains.annotations)
9+
10+
// Only the testing infrastructure, not the actual typed-ids implementations
11+
testImplementation(project(":typed-ids-hibernate-63-testing"))
12+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.framefork.typedIds.embeddable;
2+
3+
import jakarta.persistence.Embeddable;
4+
import jakarta.persistence.GeneratedValue;
5+
import jakarta.persistence.GenerationType;
6+
7+
import java.io.Serializable;
8+
9+
@Embeddable
10+
public record EmbeddableBigIntWithGenerated(
11+
@GeneratedValue(strategy = GenerationType.IDENTITY)
12+
Long value
13+
) implements Serializable
14+
{
15+
16+
public static EmbeddableBigIntWithGenerated from(long value)
17+
{
18+
return new EmbeddableBigIntWithGenerated(value);
19+
}
20+
21+
public static EmbeddableBigIntWithGenerated from(String value)
22+
{
23+
return new EmbeddableBigIntWithGenerated(Long.parseLong(value));
24+
}
25+
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.framefork.typedIds.embeddable;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.EmbeddedId;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.Table;
7+
import org.jspecify.annotations.Nullable;
8+
9+
@Entity
10+
@Table(name = EntityEmbeddableBigIntGeneratedId.TABLE_NAME)
11+
public class EntityEmbeddableBigIntGeneratedId
12+
{
13+
14+
public static final String TABLE_NAME = "embeddable_generated_id";
15+
16+
@EmbeddedId
17+
@Nullable
18+
private EmbeddableBigIntWithGenerated id;
19+
20+
@Column(nullable = false)
21+
private String name;
22+
23+
public EntityEmbeddableBigIntGeneratedId(String name)
24+
{
25+
this.name = name;
26+
// Note: id should be generated by Hibernate, but will it work with @EmbeddedId?
27+
}
28+
29+
@SuppressWarnings("NullAway")
30+
protected EntityEmbeddableBigIntGeneratedId()
31+
{
32+
}
33+
34+
@Nullable
35+
public EmbeddableBigIntWithGenerated getId()
36+
{
37+
return id;
38+
}
39+
40+
public String getName()
41+
{
42+
return name;
43+
}
44+
45+
public void setName(String name)
46+
{
47+
this.name = name;
48+
}
49+
50+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.framefork.typedIds.idclass;
2+
3+
import java.io.Serializable;
4+
5+
public record IdClassBigInt(
6+
Long value
7+
) implements Serializable
8+
{
9+
10+
public static IdClassBigInt from(long value)
11+
{
12+
return new IdClassBigInt(value);
13+
}
14+
15+
public static IdClassBigInt from(String value)
16+
{
17+
return new IdClassBigInt(Long.parseLong(value));
18+
}
19+
20+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.framefork.typedIds.idclass;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.IdClass;
9+
import jakarta.persistence.Table;
10+
import org.jspecify.annotations.Nullable;
11+
12+
@Entity
13+
@Table(name = IdClassBigIntEntityGeneratedId.TABLE_NAME)
14+
@IdClass(IdClassBigInt.class)
15+
public class IdClassBigIntEntityGeneratedId
16+
{
17+
18+
public static final String TABLE_NAME = "id_class_generated_id";
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
@Column(name = "value", nullable = false)
23+
@Nullable
24+
private Long value;
25+
26+
@Column(nullable = false)
27+
private String name;
28+
29+
public IdClassBigIntEntityGeneratedId(String name)
30+
{
31+
this.name = name;
32+
}
33+
34+
@SuppressWarnings("NullAway")
35+
protected IdClassBigIntEntityGeneratedId()
36+
{
37+
}
38+
39+
@Nullable
40+
public IdClassBigInt getId()
41+
{
42+
return value != null ? new IdClassBigInt(value) : null;
43+
}
44+
45+
@Nullable
46+
public Long getValue()
47+
{
48+
return value;
49+
}
50+
51+
public String getName()
52+
{
53+
return name;
54+
}
55+
56+
public void setName(String name)
57+
{
58+
this.name = name;
59+
}
60+
61+
}

0 commit comments

Comments
 (0)