'Expose all IDs when using Spring Data Rest

I'd like to expose all IDs using a Spring Rest interface.

I know that per default an ID like this will not be exposed via the rest interface:

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(unique=true, nullable=false)
    private Long id;

I'm aware that I can use this to expose the ID for User:

@Configuration
public class RepositoryConfig extends RepositoryRestMvcConfiguration {
    @Override
    protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(User.class);
    }
}

But is there an easy way to expose all IDs without manually maintaining a list in this configureRepositoryRestConfiguration method?



Solution 1:[1]

If you want to expose the id field for all your entity classes:

import java.util.stream.Collectors;

import javax.persistence.EntityManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class MyRepositoryRestConfigurerAdapter extends RepositoryRestConfigurerAdapter {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(entityManager.getMetamodel().getEntities().stream().map(e -> e.getJavaType()).collect(Collectors.toList()).toArray(new Class[0]));
    }

}

Solution 2:[2]

I discovered that if you name the @Id field 'Id' it will display in the JSON if you have a public getter for the Id. The Id will show up as a JSON key called 'id'

For example: @Id @Column(name="PERSON_ROLE_ID") private Long Id;

This also works for @EmbeddedId fields called 'Id' as well as long as it has a public getter. In this case the fields of the Id will show up as a JSON object.

For example: @EmbeddedId private PrimaryKey Id;

Surprisingly this is case sensitive, calling id 'id' doesn't work even though it would be a more conventional name for a Java field.

I should say that I discovered this completely by accident so I don't know if this is an accepted convention or will work with previous or future versions of Spring Data and REST. Therefore I have included the relevant parts of my maven pom just incase it's sensittive to versions...

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc7</artifactId>
        <version>12.1.0.2</version>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
</dependencies>

Solution 3:[3]

An updated answer to @mekasu. The RepositoryRestConfigurer interface was changed a bit in 2.4.

Pre 2.4:

import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;

@Configuration
public class Config implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(entityManager.getMetamodel().getEntities().stream().map(Type::getJavaType).toArray(Class[]::new));
    }
}

Post 2.4

import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;

@Configuration
public class Config implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
        config.exposeIdsFor(entityManager.getMetamodel().getEntities().stream().map(Type::getJavaType).toArray(Class[]::new));
    }
}

Solution 4:[4]

Try this configuration. It works perfectly fine for me.

@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter{

      @PersistenceContext
      private EntityManager entityManager;

      @Override
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
           //TODO: Expose for specific entity!
           //config.exposeIdsFor(Officer.class);
           //config.exposeIdsFor(Position.class);

           //TODO: Expose id for all entities!
           entityManager.getMetamodel().getEntities().forEach(entity->{
                try {
                     System.out.println("Model: " + entity.getName());
                     Class<? extends Object> clazz = Class.forName(String.format("yourpackage.%s", entity.getName()));
                     config.exposeIdsFor(clazz);
                } catch (Exception e) {
                     System.out.println(e.getMessage());
                }
            });
    }
}

Solution 5:[5]

You can use this method to find all @Entity classes of the EntityManagerFactory:

private List<Class<?>> getAllManagedEntityTypes(EntityManagerFactory entityManagerFactory) {
    List<Class<?>> entityClasses = new ArrayList<>();
    Metamodel metamodel = entityManagerFactory.getMetamodel();
    for (ManagedType<?> managedType : metamodel.getManagedTypes()) {
        Class<?> javaType = managedType.getJavaType();
        if (javaType.isAnnotationPresent(Entity.class)) {
            entityClasses.add(managedType.getJavaType());
        }
    }
    return entityClasses;
}

then, to expose the IDs for all your entity classes:

@Configuration
public class RestConfig extends RepositoryRestMvcConfiguration {

    @Bean
    public RepositoryRestConfigurer repositoryRestConfigurer(EntityManagerFactory entityManagerFactory) {
        List<Class<?>> entityClasses = getAllManagedEntityTypes(entityManagerFactory);

        return new RepositoryRestConfigurerAdapter() {

            @Override
            public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
                for (Class<?> entityClass : entityClasses) {
                    config.exposeIdsFor(entityClass);
                }
            }
    }
}

Solution 6:[6]

Following piece of code looks prettier:

.exposeIdsFor(entityManager.getMetamodel().getEntities().stream().map(entityType -> entityType.getJavaType()).toArray(Class[]::new))

Solution 7:[7]

Full working example based on @FrancoisGengler's answer:

@SpringBootApplication
public class DataRestApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataRestApplication.class, args);
    }

    @Bean
    public RepositoryRestConfigurer repositoryRestConfigurer(EntityManager entityManager) {
        return RepositoryRestConfigurer.withConfig(config -> {
            config.exposeIdsFor(entityManager.getMetamodel().getEntities()
                    .stream().map(Type::getJavaType).toArray(Class[]::new));
        });
    }
}

Solution 8:[8]

Proabably you can try this to include all id fields. I havent tried it yet, but will keep posted.

 public class ExposeAllRepositoryRestConfiguration extends RepositoryRestConfiguration {
    @Override
    public boolean isIdExposedFor(Class<?> domainType) {
        return true;
        }
    }

Excerpt from this link

Solution 9:[9]

You can add all your entity classes by exposeIdsFor. Replace "db.entity" to whick package you put your entities.

@Configuration
public class CustomRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
    Logger logger = Logger.getLogger(this.getClass());

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        Set<String> classNameSet = ClassTool.getClassName("db.entity", false);
        for (String className : classNameSet) {
            try {
                config.exposeIdsFor(Class.forName(className));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        logger.info("exposeIdsFor : " + classNameSet);
    }
}

The ClassTool is my custom function to get class from given package, you can write by yourself.

Solution 10:[10]

Here is what worked perfectly for me (source here):

@Configuration
public class RepositoryRestConfig extends RepositoryRestConfigurerAdapter {

  @Override
  public void configureRepositoryRestConfiguration(final RepositoryRestConfiguration config) {

    final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
        false);
    provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class));

    final Set<BeanDefinition> beans = provider.findCandidateComponents("com.your.domain");

    for (final BeanDefinition bean : beans) {
      try {
        config.exposeIdsFor(Class.forName(bean.getBeanClassName()));
      } catch (final ClassNotFoundException e) {
        // Can't throw ClassNotFoundException due to the method signature. Need to cast it
        throw new IllegalStateException("Failed to expose `id` field due to", e);
      }
    }
  }
}

It finds all beans with the @Entity annotation and exposes them.

Solution 11:[11]

Please find a simple solution for this, avoiding to find entities related.

@Component
public class EntityExposingIdConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        try {
            Field exposeIdsFor = RepositoryRestConfiguration.class.getDeclaredField("exposeIdsFor");
            exposeIdsFor.setAccessible(true);
            ReflectionUtils.setField(exposeIdsFor, config, new ListAlwaysContains());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    class ListAlwaysContains extends ArrayList {

        @Override
        public boolean contains(Object o) {
            return true;
        }
    }
}

Solution 12:[12]

You can try with this solution: - First import reflections library to your POM file:

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>

- Then change your RepositoryConfig class to:

@Configuration
public class RepositoryConfig extends RepositoryRestMvcConfiguration {
    @Override
    protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        Reflections reflections = new Reflections("com.example.entity");
        Set<Class<?>> idExposedClasses = reflections.getTypesAnnotatedWith(Entity.class, false);
        idExposedClasses.forEach(config::exposeIdsFor);
        return config;
    }
}

Change "com.example.entity" to your Entity package and you are good to go. Good luck!

Solution 13:[13]

I'm sharing my solution which is based on other answer.

In my case which configures multiple databases, I don't why but, I need to autowire instances of EntityManagerFactory.

@Db1 @Autowire
EntityManagerFactory entityManagerFactoryDb1;

@Db2 @Autowire
EntityManagerFactory entityManagerFactoryDb2;

Now all I need is a method streaming all entity classes gathered from all injected persistence units.

(Maybe, checking the existence of @Entity annotation or a custom annotation , say @EntityRestExposeId, can be applied.)

    private void forEachEntityClass(final Consumer<? super Class<?>> consumer) {
        Arrays.stream(DataRestConfiguration.class.getDeclaredFields())
                .filter(f -> {
                    final int modifiers = f.getModifiers();
                    return !Modifier.isStatic(modifiers);
                })
                .filter(f -> EntityManagerFactory.class.isAssignableFrom(f.getType()))
                .map(f -> {
                    f.setAccessible(true);
                    try {
                        return (EntityManagerFactory) f.get(this);
                    } catch (final ReflectiveOperationException roe) {
                        throw new RuntimeException(roe);
                    }
                })
                .flatMap(emf -> emf.getMetamodel().getEntities().stream().map(EntityType::getJavaType))
                .forEach(consumer);
    }

Calling the exposeIdFor method is straightforward.

@Configuration
class DataRestConfiguration {

    @Bean
    public RepositoryRestConfigurer repositoryRestConfigurer() {
        return RepositoryRestConfigurer.withConfig((configuration, registry) -> {
            forEachEntityClass(configuration::exposeIdsFor);
            // ...
        });
    }

    private void forEachEntityClass(final Consumer<? super Class<?>> consumer) {
        // ...
    }

    @Db1 @Autowired
    EntityManagerFactory entityManagerFactoryDb1;

    @Db2 @Autowired
    EntityManagerFactory entityManagerFactoryDb2;

    @Db3 @Autowired
    EntityManagerFactory entityManagerFactoryDb3;
}