38

Is it possible to create read-only repositories using Spring Data?

I have some entities linked to views and some child entities for which I would like to provide a repository with some methods like findAll(), findOne() and some methods with the @Queryannotation. I would like to avoid providing methods like save(…) and delete(…) since they make no sense and could create errors.

public interface ContactRepository extends JpaRepository<ContactModel, Integer>, JpaSpecificationExecutor<ContactModel> {
    List<ContactModel> findContactByAddress_CityModel_Id(Integer cityId);

    List<ContactModel> findContactByAddress_CityModel_Region_Id(Integer regionId);

    // ... methods using @Query

    // no need to save/flush/delete
}

Thanks!

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
jpboudreault
  • 977
  • 2
  • 9
  • 17

6 Answers6

67

Yes, the way to go is to add a handcrafted base repository. You usually use something like this:

public interface ReadOnlyRepository<T, ID extends Serializable> extends Repository<T, ID> {

  T findOne(ID id);

  Iterable<T> findAll();
}

You can now have you concrete repos extend that just defined one:

public interface PersonRepository extends ReadOnlyRepository<Person, Long> {

  T findByEmailAddress(String emailAddress);
}

The crucial part defining the base repo is that the method declarations carry the very same signature as the methods declared in CrudRepository if that's the case we can still route the calls into the implementation bean backing the repository proxy. I've written a more detailed blog post about that topic in the SpringSource blog.

ZeroOne
  • 3,041
  • 3
  • 31
  • 52
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 1
    There is one tricky thing. If I am using an in-memory database like HSQL for integration testing, I can use the save method of the view to create test data instead of creating a relation between its objects and populating them. So in this case I will need the view's save method only for testing but not for production code. Is there a way to achieve it ? – DBS Feb 26 '18 at 13:50
  • 3
    Note that from 2.0 M3 (Kay) version in the onwards findOne method has been renamed to findById, details in https://stackoverflow.com/questions/44101061/missing-crudrepositoryfindone-method – JuanMoreno Aug 23 '18 at 18:41
  • 3
    Suppose you still want Spring to create implementations of some `PagingAndSortingRepository` methods but not all of them. How would you then go about creating a `PagingAndSortingReadOnlyRepository` that has the same interface minus the save and delete methods? – Paul-Sebastian Manole Dec 09 '18 at 23:18
  • This solution throws `Caused by: org.springframework.data.mapping.PropertyReferenceException: No property findOne found for type CustomEntity!` exception for newer versions. Use `findById` instead. – Praytic Apr 29 '21 at 08:19
  • For extra efficiency, you can mark the repository transaction as read only by adding `@Transactional(readonly=true)` to the interface: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html – Soupy Dec 02 '21 at 17:02
36

To expand on Oliver Gierke's answer, in the more recent versions of Spring Data you will need the @NoRepositoryBean annotation on your ReadOnlyRepository (parent interface) to prevent application start up errors:

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;

@NoRepositoryBean
public interface ReadOnlyRepository<T, ID extends Serializable> extends Repository<T, ID> {

    T findOne(ID id);

    List<T> findAll();

}
Kimball Robinson
  • 3,287
  • 9
  • 47
  • 59
Tristan Perry
  • 607
  • 6
  • 14
  • The findOne method is no longer necessary. – Kimball Robinson Mar 26 '18 at 23:10
  • 2
    Getting `Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property findOne found for type` Error while starting up. When remove `T findOne(ID id);`, it works fine, but cannot view single item via URL. – Iqbal May 11 '18 at 13:18
  • 1
    maybe because it's not called `findById` as explained in a comment above... also see https://stackoverflow.com/questions/44101061/missing-crudrepositoryfindone-method ??? – maxxyme Sep 22 '20 at 09:03
7

As far as we can see in documentation, this is possible by implementing org.springframework.data.repository.Repository.

Francisco Spaeth
  • 23,493
  • 7
  • 67
  • 106
6

For me the following worked. Using Oliver's solution, I was getting error Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property findOne found for type while starting up.

@NoRepositoryBean
public interface ReadOnlyRepository<T,ID> extends Repository<T, ID> {
    Optional<T> findById(ID var1);
    boolean existsById(ID var1);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> var1);
    long count();
}
Iqbal
  • 2,094
  • 1
  • 20
  • 28
  • 1
    that's because there has been a renaming of several methods; see https://stackoverflow.com/questions/44101061/missing-crudrepositoryfindone-method – maxxyme Sep 22 '20 at 09:03
  • This is the valid solution for latest spring version – jpprade Apr 28 '22 at 08:03
5

This is for read-only PagingAndSortingRepository

package com.oracle.odc.data.catalog.service.core.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RestResource;

/**
 * Extension of {@link PagingAndSortingRepository} but without modification capabilities
 *
 * @author XYZ
 * @see Sort
 * @see Pageable
 * @see Page
 */
@NoRepositoryBean
public interface ReadOnlyPagingAndSortingRepository<T, ID> extends PagingAndSortingRepository<T, ID> {

    @Override
    @RestResource(exported=false)
    <S extends T> S save(S entity);

    @Override
    @RestResource(exported=false)
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);

    @Override
    @RestResource(exported=false)
    void deleteById(ID id);

    @Override
    @RestResource(exported=false)
    void delete(T entity);

    @Override
    @RestResource(exported=false)
    void deleteAll(Iterable<? extends T> entities);

    @Override
    @RestResource(exported=false)
    void deleteAll();

}

If you try to POST or DELETE, you will get 405 (Method Not Allowed).

The Code Guy
  • 311
  • 5
  • 10
0

Or if you want to make your own implementation or block this actions - you can do something like this (works withJava 8 and higher):

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.lang.NonNull;

import java.util.List;

@NoRepositoryBean
public interface ReadOnlyRepository<T, ID> extends JpaRepository<T, ID> {

    @Override
    @NonNull
    default <S extends T> S save(@NonNull S entity) {
        throw new RuntimeException("Action not allowed");
    }

    @Override
    @NonNull
    default <S extends T> List<S> saveAll(@NonNull Iterable<S> iterable) {
        throw new RuntimeException("Action not allowed.");
    }

    @Override
    @NonNull
    default <S extends T> S saveAndFlush(@NonNull S s) {
        throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void delete(@NonNull T entity) {
        throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void deleteAll() {
        throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void deleteAll(@NonNull Iterable<? extends T> entities) {
        throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void deleteAllInBatch() {
       throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void deleteById(@NonNull ID id) {
       throw new RuntimeException("Action not allowed.");
    }

    @Override
    default void deleteInBatch(@NonNull Iterable<T> iterable) {
        throw new RuntimeException("Action not allowed.");
    }
}

Hope I help somebody (ノ^∇^)

Andrew Mikolyk
  • 101
  • 1
  • 2