|
| 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. |
0 commit comments