Skip to content

A completely custom JPA repository, without BaseJpaRepository or JpaRepositoryImplementation #3621

@fprochazka

Description

@fprochazka

Hi,
upon reading the docs about Spring supporting custom repositories, I've gone down the rabbit hole of wanting a completely custom base repository impl + interface, because I wanted to remove and replace parts of the repository. I wanted to remove all mutability and have only a repository interface, which will only provide read methods. And I wanted to modify those read methods, so that they're better suited for usage with Kotlin. For java, having a Optional<T> findById(ID id) makes perfect sense, but for kotlin, a fun findById(id: ID): T? is much better, because I can then use native language constructs to handle the nullable value, without having to first unpack it from Optional.

ReadOnlyJpaRepository
@NoRepositoryBean
interface ReadOnlyJpaRepository<EntityType, EntityIdType> : Repository<EntityType, EntityIdType> {

    /**
     * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is implemented
     * this is very likely to always return an instance and throw an [jakarta.persistence.EntityNotFoundException] on first access.
     * Some of them will reject invalid identifiers immediately.
     *
     * @see EntityManager.getReference(Class, Object) for details on when an exception is thrown.
     */
    fun getReference(id: EntityIdType): EntityType;

    /**
     * Retrieves an entity by its id.
     */
    fun findById(id: EntityIdType): EntityType?

    /**
     * Returns a single entity matching the given [Specification] or null if none found.
     */
    fun findOne(spec: Specification<EntityType>): EntityType?

    /**
     * Returns entities matching the given [Specification] applying the {@code queryFunction} that defines the query and its result type.
     */
    fun <S : EntityType, ResultType> findBy(spec: Specification<EntityType>, queryFunction: (FluentQuery.FetchableFluentQuery<S>) -> ResultType): ResultType

    /**
     * Returns all instances of the type [EntityType] with the given IDs.
     * <p>
     * If some or all ids are not found, no entities are returned for these IDs.
     * <p>
     * Note that the order of elements in the result is not guaranteed.
     */
    fun findAllById(ids: Iterable<EntityIdType>): List<EntityType>

    /**
     * Returns all entities matching the given [Specification].
     */
    fun findAll(spec: Specification<EntityType>, pageable: Pageable = Pageable.unpaged(), sort: Sort = Sort.unsorted()): List<EntityType>

    /**
     * Returns a page of entities matching the given [Specification].
     */
    fun findPage(spec: Specification<EntityType>, pageable: Pageable = Pageable.unpaged(), sort: Sort = Sort.unsorted()): Page<EntityType>

    /**
     * Returns a window of entities matching the given [Specification].
     */
    fun scroll(spec: Specification<EntityType>, scroll: ScrollPosition, sort: Sort = Sort.unsorted(), windowLimit: Int = 1000): Window<EntityType>

    /**
     * Returns the number of instances that the given [Specification] will return.
     */
    fun count(spec: Specification<EntityType>): Long

    /**
     * Checks whether the data store contains elements that match the given [Specification].
     */
    fun exists(spec: Specification<EntityType>): Boolean

    /**
     * Lock the entity with the provided identifier.
     */
    fun lockById(id: EntityIdType, lockMode: LockModeType): EntityType?

    /**
     * Lock a single entity matching the given [Specification] or null if none found.
     */
    fun lockOne(spec: Specification<EntityType>, lockMode: LockModeType): EntityType?

    /**
     * Lock all entities matching the given [Specification].
     */
    fun lockAll(spec: Specification<EntityType>, lockMode: LockModeType): Iterable<EntityType>

}

I also have corresponding ReadOnlyJpaRepositoryImpl but that is not interesting.

Anyway... I've discovered a major roadblock, because the org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository() hardcodes a org.springframework.data.jpa.repository.support.JpaRepositoryImplementation return type. Which means that any repository that wants to use the default system is forced to implement the JpaRepositoryImplementation, therefore making the removal and change of signature for some methods impossible. To bypass this problem, I had to override/extend a bunch of classes and even had to use a bit of reflection:

  • org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport
  • org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension
  • org.springframework.data.repository.core.support.RepositoryFactorySupport
  • org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean

All this, just to bypass a type signature. While doing this, I haven't found a single reason for this limitation. I think the return type of JpaRepositoryFactory methods can be changed to just org.springframework.data.repository.Repository and the private invokeAwareMethods() can be solved with an instance of. This should open the possibility to completely replace the base repository class, instead of having to extend it, giving flexibility to those who want it, and keeping the old behaviour for everybody else.

I would be happy to attempt sending a PR if there are no vetoes.

Have a nice day 🌞

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions