Spring boot @ResponseBody não serializa id de entidade

95

Tem um problema estranho e não consigo descobrir como lidar com ele. Tenha um POJO simples:

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

e ponto final de descanso:

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

Na resposta json, existem todos os campos, exceto Id, que preciso no lado frontal para editar / excluir pessoa. Como posso configurar a inicialização do Spring para serializar Id também?

É assim que a resposta se parece agora:

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

UPD Possui mapeamento de hibernação bidirecional, talvez esteja de alguma forma relacionado ao problema.

Konstantin
fonte
Você poderia nos dar mais informações sobre sua configuração de primavera? Qual json marshaller você usa? O padrão, jackson, ...?
Outubro
Na verdade, não há configuração especial. Queria experimentar o spring boot :) Adicionado spring-boot-starter-data-rest ao pom e usando @EnableAutoConfiguration isso é tudo. Leia alguns tutoriais e parece que tudo tem que funcionar fora da caixa. E é, exceto aquele campo Id. Postagem atualizada com resposta do endpoint.
Konstantin
1
No Spring 4, você também deve usar @RestControllerna classe do controlador e remover @ResponseBodydos métodos. Também sugiro ter classes DTO para lidar com solicitações / respostas json em vez de objetos de domínio.
Vaelyr

Respostas:

139

Recentemente, tive o mesmo problema e é assim que spring-boot-starter-data-restfunciona por padrão. Veja minha pergunta sobre SO -> Ao usar Spring Data Rest depois de migrar um aplicativo para Spring Boot, observei que as propriedades de entidade com @Id não são mais enviadas para JSON

Para personalizar como ele se comporta, você pode estender RepositoryRestConfigurerAdapterpara expor IDs para classes específicas.

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
fonte
1
E não se esqueça de suportar agora getter e setter para id na classe de entidade! .. (Eu esqueci e estava procurando muito tempo para isso)
phil
3
coudnt find RepositoryRestConfigurerAdapter inorg.springframework.data.rest.webmvc.config
Govind Singh
2
Rendimentos: The type RepositoryRestConfigurerAdapter is deprecated. Também não parece funcionar
GreenAsJade
2
Na verdade, RepositoryRestConfigurerAdapterestá obsoleto nas versões mais recentes, você deve implementar RepositoryRestConfigurerdiretamente (usar em implements RepositoryRestConfigurervez de extends RepositoryRestConfigurerAdapter)
Yann39
48

Caso você precise expor os identificadores para todas as entidades :

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

Observe que nas versões do Spring Boot anteriores 2.1.0.RELEASEvocê deve estender o (agora obsoleto) em org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter vez de implementar RepositoryRestConfigurerdiretamente.


Se você deseja apenas expor os identificadores de entidades que estendem ou implementam superclasse ou interface específica :

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

Se você deseja apenas expor os identificadores de entidades com uma anotação específica :

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

Anotação de amostra:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExposeId {}
lcnicolau
fonte
Se alguém fosse usar o primeiro bloco de código, em que diretório e arquivo ele poderia ir?
David Krider
1
@DavidKrider: Deve estar em seu próprio arquivo, em qualquer diretório coberto pela Varredura de Componentes . Qualquer subpacote abaixo de seu aplicativo principal (com a @SpringBootApplicationanotação) deve servir.
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
Tentei adicionar um @ConditionalOnBean(EntityManager.class)in MyRepositoryRestConfigurerAdapter, mas o método não foi chamado e os id ainda não foram expostos. Eu uso dados de primavera com dados de primavera mybatis: github.com/hatunet/spring-data-mybatis
Dimitri Kopriwa
@DimitriKopriwa: Isso EntityManagerfaz parte da especificação JPA e MyBatis não implementa JPA (dê uma olhada em Será que MyBatis segue JPA? ). Então, acho que você deve configurar todas as entidades uma a uma, usando o config.exposeIdsFor(...)método como na resposta aceita .
lcnicolau
24

A resposta de @ eric-peladan não funcionou fora da caixa, mas foi bem parecida, talvez funcionasse para as versões anteriores do Spring Boot. Agora é assim que ele deve ser configurado, corrija-me se eu estiver errado:

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
fonte
3
Confirmado que esta solução funciona bem no Spring Boot v1.3.3.RELEASE, ao contrário da proposta por @ eric-peladan.
Poliakoff
4

Com o Spring Boot, você deve estender SpringBootRepositoryRestMvcConfiguration
se usar RepositoryRestMvcConfiguration, a configuração definida em application.properties pode não funcionar

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

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

Mas, para uma necessidade temporária, você pode usar a projeção para incluir id na serialização como:

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

}

eric Peladan
fonte
Na bota 2, isso não funciona. A classe RepositoryRestMvcConfiguration não tem configureRepositoryRestConfiguration para substituir.
pitchblack408
4

A classe RepositoryRestConfigurerAdapterestá obsoleta desde 3.1, implemente RepositoryRestConfigurerdiretamente.

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

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

Gisomar Junior
fonte
2

Maneira fácil: renomeie sua variável private Long id;paraprivate Long Id;

Funciona para mim. Você pode ler mais sobre isso aqui

Андрей Миронов
fonte
13
oh, cara ... isso é um cheiro de código ... sério, não faça isso
Igor Donin
@IgorDonin diz isso para a comunidade Java que adora farejar e rastrear a semântica de nomes de variáveis ​​/ classes / funções ... o tempo todo, em qualquer lugar.
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.
fonte
3
Bem-vindo ao SO! Ao postar código em suas respostas, é útil explicar como seu código resolve o problema do OP :)
Joel
1

Hm, ok, parece que encontrei a solução. Remover spring-boot-starter-data-rest do arquivo pom e adicionar @JsonManagedReference a phoneNumbers e @JsonBackReference para pessoa fornece a saída desejada. Em resposta, Json não está mais bem impresso, mas agora tem o Id. Não sei o que a bota de mola mágica faz sob o capô com essa dependência, mas eu não gosto dela :)

Konstantin
fonte
É para expor seus repositórios Spring Data como endpoints REST. Se você não quiser esse recurso, pode deixá-lo de fora. A coisa do id é fazer com um Jackson Moduleque está registrado na SDR eu acredito.
Dave Syer de
1
APIs RESTful não devem expor IDs de chaves substitutas porque não significam nada para sistemas externos. Na arquitetura RESTful, o ID é a URL canônica desse recurso.
Chris DaMour 01 de
4
Já li isso antes, mas honestamente não entendo como posso vincular front end e back end sem ID. Tenho que passar a Id para o orm para a operação de exclusão / atualização. Talvez você tenha algum link, como isso pode ser executado de forma verdadeiramente RESTful :)
Konstantin