Spring boot @ResponseBody no serializa la identificación de la entidad

95

Tiene un problema extraño y no sabe cómo solucionarlo. Tener 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);
    }
}

y punto final de descanso:

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

En la respuesta json, hay todos los campos excepto Id, que necesito en el lado frontal para editar / eliminar a la persona. ¿Cómo puedo configurar Spring Boot para serializar Id también?

Así es como se ve la respuesta ahora:

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

UPD Tiene mapeo de hibernación bidireccional, tal vez esté relacionado de alguna manera con el problema.

Konstantin
fuente
¿Podría darnos más información sobre su configuración de primavera? ¿Qué json marshaller usas? ¿El predeterminado, Jackson, ...?
Ota
En realidad, no hay una configuración especial. Quería probar Spring Boot :) Se agregó spring-boot-starter-data-rest a pom y usando @EnableAutoConfiguration eso es todo. Lea un par de tutoriales y parece que todos tienen que funcionar de inmediato. Y lo es, excepto en el campo Id. Publicación actualizada con respuesta de endpoint.
Konstantin
1
En Spring 4 también debe usar @RestControlleren la clase de controlador y eliminar @ResponseBodyde los métodos. También sugeriría tener clases DTO para manejar solicitudes / respuestas json en lugar de objetos de dominio.
Vaelyr

Respuestas:

139

Recientemente tuve el mismo problema y es porque así es como spring-boot-starter-data-restfunciona de forma predeterminada. Vea mi pregunta SO -> Al usar Spring Data Rest después de migrar una aplicación a Spring Boot, he observado que las propiedades de la entidad con @Id ya no se agrupan en JSON

Para personalizar cómo se comporta, puede ampliar RepositoryRestConfigurerAdapterpara exponer los ID de clases 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
fuente
1
! Y no se olvide de apoyar ahora get y set para la identificación de la clase de entidad .. (se me olvidó y estaba buscando mucho tiempo para eso)
Phil
3
no podría encontrar RepositoryRestConfigurerAdapter enorg.springframework.data.rest.webmvc.config
Govind Singh
2
Rendimiento: The type RepositoryRestConfigurerAdapter is deprecated. Tampoco parece funcionar
GreenAsJade
2
De hecho, RepositoryRestConfigurerAdapterestá en desuso en las últimas versiones, debe implementar RepositoryRestConfigurerdirectamente (usar en implements RepositoryRestConfigurerlugar de extends RepositoryRestConfigurerAdapter)
Yann39
48

En caso de que necesite exponer los identificadores de todas las 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));
    }
}

Tenga en cuenta que en las versiones de Spring Boot anteriores 2.1.0.RELEASE, debe extender el (ahora en desuso) en org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter lugar de implementarlo RepositoryRestConfigurerdirectamente.


Si solo desea exponer los identificadores de entidades que amplían o implementan una superclase o interfaz específica :

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

Si solo desea exponer los identificadores de entidades con una anotación 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));
    }

Ejemplo de anotación:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExposeId {}
lcnicolau
fuente
Si uno usara el primer bloque de código, ¿en qué directorio y archivo podría entrar?
David Krider
1
@DavidKrider: Debe estar en su propio archivo, en cualquier directorio cubierto por Component Scan . Cualquier subpaquete debajo de su aplicación principal (con la @SpringBootApplicationanotación) debería estar bien.
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
He intentado añadir una @ConditionalOnBean(EntityManager.class)en MyRepositoryRestConfigurerAdapter, pero el método no se llama y el ID todavía no están expuestos. Utilizo datos de Spring con datos de Spring mybatis: github.com/hatunet/spring-data-mybatis
Dimitri Kopriwa
@DimitriKopriwa: El EntityManageres parte de la especificación JPA y MyBatis no implementa JPA (eche un vistazo a ¿MyBatis sigue a JPA? ). Entonces, creo que debería configurar todas las entidades una por una, usando el config.exposeIdsFor(...)método como en la respuesta aceptada .
lcnicolau
24

La respuesta de @ eric-peladan no funcionó de inmediato, pero estuvo bastante cerca, tal vez funcionó para versiones anteriores de Spring Boot. Ahora, así es como se supone que debe configurarse en su lugar, corrígeme si me equivoco:

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
fuente
3
Confirmado que esta solución funciona bien bajo Spring Boot v1.3.3.RELEASE a diferencia de la propuesta por @ eric-peladan.
Poliakoff
4

Con Spring Boot, debe extender SpringBootRepositoryRestMvcConfiguration
si usa RepositoryRestMvcConfiguration, la configuración definida en application.properties puede no funcionar

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

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

Pero para una necesidad temporal, puede usar la proyección para incluir la identificación en la serialización como:

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

}

eric peladan
fuente
En Spring Boot 2, esto no funciona. La clase RepositoryRestMvcConfiguration no tiene configureRepositoryRestConfiguration para anular.
pitchblack408
4

La clase RepositoryRestConfigurerAdapterha quedado obsoleta desde 3.1, implementar RepositoryRestConfigurerdirectamente.

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

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

Gisomar Junior
fuente
2

Manera fácil: cambie el nombre de su variable private Long id;aprivate Long Id;

Funciona para mi. Puedes leer más sobre esto aquí.

Андрей Миронов
fuente
13
oh, hombre ... ese es un olor a código ... de verdad, no hagas eso
Igor Donin
@IgorDonin le dice eso a la comunidad Java a la que le encanta rastrear y rastrear la semántica de los nombres de variables / clases / funciones ... todo el tiempo, en todas partes.
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.
fuente
3
¡Bienvenido a SO! Al publicar código en sus respuestas, es útil explicar cómo su código resuelve el problema del OP :)
Joel
1

Hm, parece que encontré la solución. Eliminar spring-boot-starter-data-rest del archivo pom y agregar @JsonManagedReference a phoneNumbers y @JsonBackReference a person da el resultado deseado. Json en respuesta ya no está bastante impreso, pero ahora tiene Id. No sé qué hace la bota de resorte mágica debajo del capó con esta dependencia, pero no me gusta :)

Konstantin
fuente
Es para exponer sus repositorios de Spring Data como puntos finales REST. Si no desea esa función, puede omitirla. La identificación tiene que ver con un Jackson Moduleque está registrado por SDR, creo.
Dave Syer
1
Las API RESTful no deben exponer las ID de clave sustitutas porque no significan nada para los sistemas externos. En la arquitectura RESTful, el ID es la URL canónica de ese recurso.
Chris DaMour
4
He leído esto antes, pero honestamente no entiendo cómo puedo vincular el front-end y el back-end sin Id. Tengo que pasar Id a orm para la operación de eliminación / actualización. Tal vez tenga algún enlace, ¿cómo se puede realizar de manera verdadera RESTful :)
Konstantin