Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare repositories based on entity interfaces?

In a new project we would like to use Spring Data JPA and define interfaces for all JPA entities, e.g. like this:

public interface Person extends Serializable {

  void setId(Long id);

  Long getId();

  void setLastName(String lastName);

  String getLastName();

  void setFirstName(String firstName);

  String getFirstName();

  // ...

}

@Entity
@Table(name = "t_persons")
public class PersonEntity implements Person {

  private static final long serialVersionUID = 1L;

  @Id
  @Column
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  @Column
  private String firstName;

  @Column
  private String lastName;

  // ...
}

However, when declaring a Spring Data repository based on the interface like

public interface PersonRepository extends JpaRepository<Person, Long> {

}

The Spring context fails to initialize with an exception whose cause is

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'personRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: interface com.example.Person
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1513)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:293)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:290)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:191)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:917)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:860)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:775)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:489)
    ... 24 more
Caused by: java.lang.IllegalArgumentException: Not an managed type: interface com.example.Person
    at org.hibernate.ejb.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:171)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:70)
    at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getMetadata(JpaEntityInformationSupport.java:65)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:146)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:84)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:67)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:150)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:224)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:210)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:84)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1572)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1510)
    ... 34 more

I haven't found any example of Repository relying on the interface instead of the concrete type so is this possible at all? And if yes, how?

It seems that if we cannot use the interface to declare the repositories, then it will be very difficult to use those interfaces at all since we will end up with explicit casts everywhere in our services, and even unchecked casts as soon as we deal with generics (List, Iterable…).

like image 812
Didier L Avatar asked Aug 07 '14 16:08

Didier L


2 Answers

I had the same problem and solved it using @NoRepositoryBean on the repository interface that uses an interface and not a concrete class in that way (thanks to that blog post):

import org.springframework.data.repository.NoRepositoryBean;

@NoRepositoryBean
public interface PersonRepository<P extends Person> extends JpaRepository<P, Long> {
    // code all generic methods using fields in Person interface
}

And then, use a concrete repository that extends the other:

public interface PersonEntityRepository extends PersonRepository<PersonEntity> {
    // code all specific methods that use fields in PersonEntity class
}

This annotation is at least present in spring-data-commons-2.1.9.RELEASE.jar.

like image 72
Anthony O. Avatar answered Sep 21 '22 11:09

Anthony O.


Here is a solution to your problem. I don't know why Spring guys decided to base their repositories on concrete classes. But at least they made it possible to change that.

  1. You need to provide custom repositoryFactoryBeanClass in EnableJpaRepositories e.g. something like that:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author goraczka
 */
@EnableJpaRepositories(
    repositoryFactoryBeanClass = InterfaceBasedJpaRepositoryFactoryBean.class
)
public class DatabaseConfig {
}
  1. Then you need to implement InterfaceBasedJpaRepositoryFactoryBean. It is a Spring hook to enable creating a custom factory for repository beans.
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import javax.persistence.EntityManager;

/**
 * @author goraczka
 */
public class InterfaceBasedJpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
        extends JpaRepositoryFactoryBean<T, S, ID> {

    public InterfaceBasedJpaRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
        super(repositoryInterface);
    }

    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new InterfaceBasedJpaRepositoryFactory(entityManager);
    }
}
  1. And last but not least the custom repository bean factory that tries to match interface defined on the repository itself with a entity class registered on EntityManager.
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author goraczka
 */
public class InterfaceBasedJpaRepositoryFactory extends JpaRepositoryFactory {

    private final Map<? extends Class<?>, ? extends Class<?>> interfaceToEntityClassMap;
    private final EntityManager entityManager;

    public InterfaceBasedJpaRepositoryFactory(EntityManager entityManager) {

        super(entityManager);

        this.entityManager = entityManager;

        interfaceToEntityClassMap = entityManager
                .getMetamodel()
                .getEntities()
                .stream()
                .flatMap(et -> Arrays.stream(et.getJavaType().getInterfaces())
                        .map(it -> new AbstractMap.SimpleImmutableEntry<>(it, et.getJavaType())))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (possibleDuplicateInterface, v) -> v));
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {

        Assert.isTrue(domainClass.isInterface(), "You are using interface based jpa repository support. " +
                "The entity type used in DAO should be an interface");

        Class<T> domainInterface = domainClass;

        Class<?> entityClass = interfaceToEntityClassMap.get(domainInterface);

        Assert.notNull(entityClass, "Entity class for a interface" + domainInterface + " not found!");

        return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(entityClass, entityManager);
    }
}

Don't bash me for any mistakes. I did it in 10 minutes after reading this question and realizing that there is no solution for it yet. And I really needed one. I did not create any tests for it yet but seems to work. Improvements are welcomed.

like image 30
goroncy Avatar answered Sep 22 '22 11:09

goroncy