Expose all IDs when using Spring Data Rest
Asked Answered
D

15

39

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?

Drury answered 18/6, 2015 at 10:26 Comment(1)
Take a look at this to see some useful examples of how to expose the identifiers for all entities, or only those that extend or implement a specific superclass or interface, or marked with some specific annotation.Summer
C
17

Currently, there is no way to do this provided by SDR. This issue on the SDR Jira tracker gives some explanation as to why this isn't (and perhaps shouldn't) be possible.

The argument is basically that since the IDs are already contained within the self links in the response, you don't need to expose them as properties of the object itself.

That said, you may be able to use reflection to retrieve all classes that have a javax.persistence.Id annotation and then call RepositoryRestConfiguration#exposeIdsFor(Class<?>... domainTypes).

Communistic answered 16/10, 2015 at 13:26 Comment(2)
There is an example that use reflection (in this case ClassPathScannning) at thomas-letsch.de/2015/spring-data-rest-hateoasTopheavy
That being said, IDs are needed especially on headless services and mix and match use cases. there has to be a unique key to point to the object and being interpreted as such on the backend retrieve the mapped object for that unique key. Might as well use the ID itself.Omission
R
45

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]));
    }

}
Readiness answered 12/12, 2017 at 5:24 Comment(3)
.map(EntityType::getJavaType)Pegeen
This should be the accepted answer, as manual configuration would only be possible in the smallest of use cases. Thanks @Readiness this is a thing of beauty! It's working for me.Cathcart
.toArray(Class[]::new));Anking
L
19

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>
Loadstar answered 14/8, 2016 at 2:59 Comment(4)
The downside to not following java naming conventions is that bean evaluation languages such as SpEL won't easily be able to reference the field.Loadstar
Worked like a charm +10Pitcher
super find! Id works for me . NOTE: in the json, it only shows as id: 1 , that too at the end of all other fields. "addresses": [ { "custid": "1", "address1": "1, stree1", "address2": "stree1", "city": "city1", "zip": "1111", "ccc": "ccc1", "id": 1, "_links": { "self": { "href": "localhost:8080/addresses/1" }, "address": { "href": "localhost:8080/addresses/1" } } },Autoradiograph
That's much easier than setting the RepositoryRestConfigurer to expose Ids for each class you want.Camelback
C
17

Currently, there is no way to do this provided by SDR. This issue on the SDR Jira tracker gives some explanation as to why this isn't (and perhaps shouldn't) be possible.

The argument is basically that since the IDs are already contained within the self links in the response, you don't need to expose them as properties of the object itself.

That said, you may be able to use reflection to retrieve all classes that have a javax.persistence.Id annotation and then call RepositoryRestConfiguration#exposeIdsFor(Class<?>... domainTypes).

Communistic answered 16/10, 2015 at 13:26 Comment(2)
There is an example that use reflection (in this case ClassPathScannning) at thomas-letsch.de/2015/spring-data-rest-hateoasTopheavy
That being said, IDs are needed especially on headless services and mix and match use cases. there has to be a unique key to point to the object and being interpreted as such on the backend retrieve the mapped object for that unique key. Might as well use the ID itself.Omission
B
10

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));
    }
}
Bluenose answered 31/5, 2021 at 9:17 Comment(2)
Type::getJavaType is which import exactly? Please never omit import declarations.Berard
Type::getJavaType is javax.persistence.metamodel.TypeBluenose
P
3

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());
                }
            });
    }
}
Plauen answered 13/12, 2017 at 12:26 Comment(1)
It works perfectly. However, RepositoryRestConfigurerAdapter is deprecated. I extended RepositoryRestConfigurer class and did override to configureRepositoryRestConfiguration method and write your code as it is. And it's working fine with meHyde
S
3

Here is a short solution to expose all ids using only things from springframework:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;

@Configuration
public class MyRepositoryRestConfigurer implements RepositoryRestConfigurer {
    @Autowired
    private Repositories repositories;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
        this.repositories.iterator().forEachRemaining(r -> {
            config.exposeIdsFor(r);
        });
    }
}
Sapless answered 17/12, 2022 at 11:30 Comment(0)
R
2

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);
                }
            }
    }
}
Ribald answered 20/2, 2017 at 3:48 Comment(0)
C
2

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));
        });
    }
}
Clara answered 5/11, 2020 at 20:40 Comment(1)
Helped me to get the IDs shown for a specific Entity, by replacing entityManager.getMetamodel().getEntities() .stream().map(Type::getJavaType).toArray(Class[]::new) with just Question.class.Antimatter
C
1

Following piece of code looks prettier:

.exposeIdsFor(entityManager.getMetamodel().getEntities().stream().map(entityType -> entityType.getJavaType()).toArray(Class[]::new))
Capitulum answered 15/3, 2019 at 14:38 Comment(0)
B
1

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;
}
Bookmobile answered 4/6, 2021 at 8:51 Comment(0)
S
0

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

Septima answered 27/9, 2016 at 16:39 Comment(0)
P
0

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.

Patty answered 11/5, 2017 at 6:51 Comment(0)
D
0

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.

Duppy answered 20/11, 2017 at 17:13 Comment(0)
S
0

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;
        }
    }
}
Strawflower answered 11/9, 2018 at 23:54 Comment(1)
Yields: The type RepositoryRestConfigurerAdapter is deprecatedJuly
A
0

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!

Arlin answered 21/6, 2019 at 8:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.