Search code examples
spring-bootkotlinspring-data-jpadomain-driven-design

Avoid primitive obsession in Hibernate / Spring JPA


In our Spring Boot 3 (Kotlin) project, we apply domain-driven design and want to avoid primitive obsession in entities and repositories (especially IDs, which often are of type GUID and can easily be confused). Is it a good idea to use @Embeddable / @EmbeddedId to wrap the primitive ID type in a class? Like so:

@Embeddable
data class OrderId(var orderId: String = "") : Serializable

@Entity
data class Order(
    @EmbeddedId
    var id: OrderId = OrderId(),
)

(We've seen this approach only for composite keys.) Or is some other mechanism the recommended way to do this? We are fearful of subtle traps, e.g. that it may not be fully compatible with JPQL native queries.


Solution

  • My project uses a custom UUID (a Kotlin derivation of https://github.com/ulid/spec) and we are using this as the identifier on all entities. Actually, we don't go as far as using different UUIDs per entity, but this would be easy to achieve.

    We do not use @EmbeddedId rather just a something configured into the Spring Data serialization process.

    This means entity classes do no need to be annotated with anything in normal circumstances:

    data class Channel(
        val id: RULID,
        ...
    }
    

    since, with Mongo at least, id is the expected primary key (which is translated as _id when it hits the Mongo DB).

    Here are the relevant pieces of code (given RULID is our custom ID) - as you can see we serialize this into Strings on the database side.

    @org.springframework.data.convert.ReadingConverter
    class DBObjectToRULIDConverter : Converter<String, RULID> {
        override fun convert(source: String): RULID {
            if (!RULID.isValid(source)) {
                return RULID.INVALID
            }
            return RULID.from(source)
        }
    }
    
    @org.springframework.data.convert.WritingConverter
    class RULIDToDBObjectConverter : Converter<RULID, String> {
        override fun convert(source: RULID): String {
            return source.toString()
        }
    }
    

    And given we are using Mongo we declare the converters like this:

    @Configuration
    @EnableMongoRepositories(basePackages = ["xxxxxxx.yyyyyy"])
    class MongoConfig {
    
        @Bean
        fun customConversions(): MongoCustomConversions {
            return MongoCustomConversions(
                listOf(
                    DBObjectToRULIDConverter(),
                    RULIDToDBObjectConverter(),
                    // others...
                )
            )
        }
    }
    

    Spring Repository interface magic works without any configuration in many cases:

    @Repository
    interface ProfileRepository : MongoRepository<Profile, RULID>
    

    with nothing extra so that findById(id: RULID): Profile works with no further configuration.

    And custom queries, again work without any further serialisation handling:

        @Query("{'member._id': ?0}")
        fun findChannelsByUserId(memberId: RULID?): List<Channel>
    

    or writing lower-level query syntax:

    val criteria = Criteria.where("_id").`is`(channel.id)