Spring Boot @ResponseBody ne sérialise pas l'ID d'entité

95

Vous avez un problème étrange et vous ne savez pas comment y faire face. Avoir un POJO simple:

@Entity
@Table(name = "persons")
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "middle_name")
    private String middleName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "comment")
    private String comment;

    @Column(name = "created")
    private Date created;

    @Column(name = "updated")
    private Date updated;

    @PrePersist
    protected void onCreate() {
        created = new Date();
    }

    @PreUpdate
    protected void onUpdate() {
        updated = new Date();
    }

    @Valid
    @OrderBy("id")
    @OneToMany(mappedBy = "person", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PhoneNumber> phoneNumbers = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public Date getCreated() {
        return created;
    }

    public Date getUpdated() {
        return updated;
    }

    public List<PhoneNumber> getPhoneNumbers() {
        return phoneNumbers;
    }

    public void addPhoneNumber(PhoneNumber number) {
        number.setPerson(this);
        phoneNumbers.add(number);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

@Entity
@Table(name = "phone_numbers")
public class PhoneNumber {

    public PhoneNumber() {}

    public PhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "phone_number")
    private String phoneNumber;

    @ManyToOne
    @JoinColumn(name = "person_id")
    private Person person;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

et point final de repos:

@ResponseBody
@RequestMapping(method = RequestMethod.GET)
public List<Person> listPersons() {
    return personService.findAll();
}

Dans la réponse json, il y a tous les champs sauf Id, dont j'ai besoin du côté frontal pour modifier / supprimer la personne. Comment puis-je configurer Spring Boot pour sérialiser également l'ID?

Voilà à quoi ressemble la réponse maintenant:

[{
  "firstName": "Just",
  "middleName": "Test",
  "lastName": "Name",
  "comment": "Just a comment",
  "created": 1405774380410,
  "updated": null,
  "phoneNumbers": [{
    "phoneNumber": "74575754757"
  }, {
    "phoneNumber": "575757547"
  }, {
    "phoneNumber": "57547547547"
  }]
}]

UPD Avoir un mappage bidirectionnel d'hibernation, c'est peut-être lié à un problème.

Konstantin
la source
Pourriez-vous s'il vous plaît nous donner plus d'informations sur votre configuration de printemps? Quel json marshaller utilisez-vous? Celui par défaut, jackson, ...?
Ota
En fait, il n'y a pas de configuration spéciale. Je voulais essayer Spring Boot :) Ajout de spring-boot-starter-data-rest à pom et en utilisant @EnableAutoConfiguration c'est tout. Lisez quelques tutoriels et semble que tous doivent fonctionner hors de la boîte. Et ça l'est, sauf ce champ Id. Message mis à jour avec la réponse du point de terminaison.
Konstantin
1
Au printemps 4, vous devez également utiliser @RestControllersur la classe de contrôleur et supprimer @ResponseBodydes méthodes. Aussi, je suggérerais d'avoir des classes DTO pour gérer les demandes / réponses json au lieu des objets de domaine.
Vaelyr

Réponses:

139

J'ai récemment eu le même problème et c'est parce que c'est comme ça que ça spring-boot-starter-data-restmarche par défaut. Voir ma question SO -> Lors de l'utilisation de Spring Data Rest après la migration d'une application vers Spring Boot, j'ai observé que les propriétés d'entité avec @Id ne sont plus rassemblées vers JSON

Pour personnaliser son comportement, vous pouvez l'étendre RepositoryRestConfigurerAdapterpour exposer des ID pour des classes spécifiques.

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 RepositoryConfig extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Person.class);
    }
}
Patrick Grimard
la source
1
Et n'oubliez pas de prendre en charge maintenant getter et setter pour l'identifiant dans la classe d'entité! .. (je l'ai oublié et je cherchais beaucoup de temps pour cela)
phil
3
coudnt trouver RepositoryRestConfigurerAdapter dansorg.springframework.data.rest.webmvc.config
Govind Singh
2
Les rendements: The type RepositoryRestConfigurerAdapter is deprecated. Ne semble pas non plus fonctionner
GreenAsJade
2
Indeed RepositoryRestConfigurerAdapterest obsolète dans les dernières versions, vous devez l'implémenter RepositoryRestConfigurerdirectement (utiliser à la implements RepositoryRestConfigurerplace de extends RepositoryRestConfigurerAdapter)
Yann39
48

Au cas où vous auriez besoin d'exposer les identifiants de toutes les entités :

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.RepositoryRestConfigurer;

import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

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

Notez que dans les versions de Spring Boot antérieures à, 2.1.0.RELEASEvous devez étendre le (désormais obsolète) org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter au lieu de l'implémenter RepositoryRestConfigurerdirectement.


Si vous souhaitez uniquement exposer les identifiants des entités qui étendent ou implémentent une super classe ou une interface spécifique :

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

Si vous souhaitez uniquement exposer les identifiants des entités avec une annotation spécifique :

    ...
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .filter(c -> c.isAnnotationPresent(ExposeId.class))
                .toArray(Class[]::new));
    }

Exemple d'annotation:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExposeId {}
lcnicolau
la source
Si l'on utilisait le premier bloc de code, dans quel répertoire et fichier pourrait-il entrer?
David Krider
1
@DavidKrider: Il doit être dans son propre fichier, dans n'importe quel répertoire couvert par l' analyse des composants . Tout sous-package sous votre application principale (avec l' @SpringBootApplicationannotation) devrait convenir.
lcnicolau
Field entityManager in com.myapp.api.config.context.security.RepositoryRestConfig required a bean of type 'javax.persistence.EntityManager' that could not be found.
Dimitri Kopriwa
J'ai essayé d'ajouter un @ConditionalOnBean(EntityManager.class)in MyRepositoryRestConfigurerAdapter, mais la méthode n'est pas appelée et l'identifiant n'est toujours pas exposé. J'utilise les données Spring avec les données Spring mybatis: github.com/hatunet/spring-data-mybatis
Dimitri Kopriwa
@DimitriKopriwa: Cela EntityManagerfait partie de la spécification JPA et MyBatis n'implémente pas JPA (jetez un œil à MyBatis suit-il JPA? ). Donc, je pense que vous devriez configurer toutes les entités une par une, en utilisant la config.exposeIdsFor(...)méthode comme dans la réponse acceptée .
lcnicolau
24

La réponse de @ eric-peladan n'a pas fonctionné immédiatement, mais était assez proche, peut-être que cela fonctionnait pour les versions précédentes de Spring Boot. Maintenant, voici comment il est censé être configuré à la place, corrigez-moi si je me trompe:

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 RepositoryConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(User.class);
        config.exposeIdsFor(Comment.class);
    }
}
Salvatorelab
la source
3
Confirmé que cette solution fonctionne correctement sous Spring Boot v1.3.3.RELEASE contrairement à celle proposée par @ eric-peladan.
Poliakoff
4

Avec Spring Boot, vous devez étendre SpringBootRepositoryRestMvcConfiguration
si vous utilisez RepositoryRestMvcConfiguration, la configuration définie dans application.properties peut ne pas fonctionner

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

@Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
    config.exposeIdsFor(Project.class);
}
}

Mais pour un besoin temporaire, vous pouvez utiliser la projection pour inclure l' id dans la sérialisation comme:

@Projection(name = "allparam", types = { Person.class })
public interface ProjectionPerson {
Integer getIdPerson();
String getFirstName();
String getLastName();

}

Eric Peladan
la source
Dans Spring Boot 2, cela ne fonctionne pas. La classe RepositoryRestMvcConfiguration n'a pas configureRepositoryRestConfiguration à remplacer.
pitchblack408
4

La classe RepositoryRestConfigurerAdapterest obsolète depuis la version 3.1, implémentée RepositoryRestConfigurerdirectement.

@Configuration
public class RepositoryConfiguration implements RepositoryRestConfigurer  {
	@Override
	public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
		config.exposeIdsFor(YouClass.class);
		RepositoryRestConfigurer.super.configureRepositoryRestConfiguration(config);
	}
}

Police: https://docs.spring.io/spring-data/rest/docs/current-SNAPSHOT/api/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurer.html

Gisomar Junior
la source
2

Méthode simple: renommez votre variable private Long id;enprivate Long Id;

Travaille pour moi. Vous pouvez en savoir plus ici

Андрей Миронов
la source
13
oh, mec ... c'est une telle odeur de code ... vraiment, ne fais pas ça
Igor Donin
@IgorDonin dit cela à la communauté Java qui aime renifler et explorer la sémantique à partir des noms de variables / classes / fonctions ... tout le temps, partout.
EralpB
2
@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;
        }
    }
}
David B.
la source
3
Bienvenue à SO! Lorsque vous publiez du code dans vos réponses, il est utile d'expliquer comment votre code résout le problème de l'OP :)
Joel
1

Hm, ok semble avoir trouvé la solution. La suppression de spring-boot-starter-data-rest du fichier pom et l'ajout de @JsonManagedReference à phoneNumbers et @JsonBackReference à la personne donne le résultat souhaité. Json en réponse n'est plus assez imprimé mais maintenant il a un identifiant. Je ne sais pas ce que la botte à ressort magique effectue sous le capot avec cette dépendance mais je n'aime pas ça :)

Konstantin
la source
C'est pour exposer vos référentiels Spring Data en tant que points de terminaison REST. Si vous ne voulez pas de cette fonctionnalité, vous pouvez la laisser de côté. La chose id est à voir avec un Jackson Modulequi est enregistré par SDR je crois.
Dave Syer
2
Les API RESTful ne doivent pas exposer les ID de clé de substitution car ils ne signifient rien pour les systèmes externes. Dans l'architecture RESTful, l'ID est l'URL canonique de cette ressource.
Chris DaMour
4
J'ai lu ceci avant, mais honnêtement, je ne comprends pas comment puis-je lier le front-end et le back-end sans ID. Je dois transmettre l'identifiant à orm pour l'opération de suppression / mise à jour. Peut-être avez-vous un lien, comment peut-il être exécuté de manière RESTful :)
Konstantin