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.


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

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 {}

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

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

}


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


2

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

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


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

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 :)


É 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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.