15

I have a two JPA entities, one with a SDR exported repository, and another with a Spring MVC controller, and a non-exported repository.

The MVC exposed entity has a reference to the SDR managed entity. See below for code reference.

The problem comes into play when retrieving a User from the UserController. The SDR managed entity won't serialize, and it seems that Spring may be trying to use HATEOAS refs in the response.

Here's what a GET for a fully populated User looks like:

{
  "username": "foo@gmail.com",
  "enabled": true,
  "roles": [
    {
      "role": "ROLE_USER",
      "content": [],
      "links": [] // why the content and links?
    }
    // no places?
  ]
}

How do I plainly return the User entity from my Controller with the embedded SDR managed Entity?

Spring MVC Managed

Entity

@Entity
@Table(name = "users")
public class User implements Serializable {

    // UID

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    private Long id;

    @Column(unique = true)
    @NotNull
    private String username;

    @Column(name = "password_hash")
    @JsonIgnore
    @NotNull
    private String passwordHash;

    @NotNull
    private Boolean enabled;

    // No Repository
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @NotEmpty
    private Set<UserRole> roles = new HashSet<>();

    // The SDR Managed Entity
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_place", 
        joinColumns = { @JoinColumn(name = "users_id") }, 
        inverseJoinColumns = { @JoinColumn(name = "place_id")})
    private Set<Place> places = new HashSet<>();

    // getters and setters
}

Repo

@RepositoryRestResource(exported = false)
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
    // Query Methods
}

Controller

@RestController
public class UserController {

    // backed by UserRepository
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(path = "/users/{username}", method = RequestMethod.GET)
    public User getUser(@PathVariable String username) {
        return userService.getByUsername(username);
    }

    @RequestMapping(path = "/users", method = RequestMethod.POST)
    public User createUser(@Valid @RequestBody UserCreateView user) {
        return userService.create(user);
    }

    // Other MVC Methods
}

SDR Managed

Entity

@Entity
public class Place implements Serializable {

    // UID

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @NotBlank
    private String name;

    @Column(unique = true)
    private String handle;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "address_id")
    private Address address;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "contact_info_id")
    private ContactInfo contactInfo;

    // getters and setters
}

Repo

public interface PlaceRepository extends PagingAndSortingRepository<Place, Long> {
    // Query Methods
}
bvulaj
  • 5,023
  • 5
  • 31
  • 45
  • I assume you do have @Repository annotation for PlaceRepository - just didn't post it here? Could you add the text of the exception as well? – lenach87 May 15 '16 at 00:29
  • @lenach87 - SDR doesn't require the `@Repository` annotation unless you need to further configure it. There is also no Exception, just the lack of serialization. – bvulaj May 15 '16 at 04:08
  • Maybe the problem with your JPA implementation? If you are using Hibernate, it can make eager load of only ONE bag. you can create a custom query to force it to load eagerly, or simply call accessor of the property before sending it as a response (this will force hibernate to load the property bag). – Ilya Dyoshin May 21 '16 at 20:18
  • Maybe check this, assuming that you use jackson for serialization: http://stackoverflow.com/questions/19580856/jackson-list-serialization-nested-lists – riddy May 23 '16 at 09:37
  • Can you add your repository configuration? Is it possible that there are two entityManagers handling these repositories and one cannot see the entities in the other? – woemler May 24 '16 at 13:55
  • @woemler That's possible. Let me get a dump from `/beans` and see. I don't define any myself. – bvulaj May 24 '16 at 15:55
  • @BrandonV: It should be pretty easy to write a test to fetch some `User` records and see if the `Place` records populate, since it is set to eager fetching. – woemler May 24 '16 at 16:52
  • @woemler - I take back my comment. I actually already do know that Place entities are being populated within the User entity. They just won't serialize. I'm now wondering if it's an issue with not using `@RepositoryRestController` as specified in the SDR docs. Though I'm not sure why. – bvulaj May 24 '16 at 18:11
  • have you tried to deserialize explicitly (just to check if its called at all)? Normally your code (if all getters are there) should work, i feel. In the end you serialize Pojos, don't you? The annotations shouldnt have any effects on that. – riddy May 30 '16 at 11:23

2 Answers2

2

In a nutshell: Spring Data REST and Spring HATEOAS hijack the ObjectMapper and want to represent relationships between resources as links rather than embedding the resource.

Take an entity with a one to one relationship with another entity:

@Entity
public class Person {
    private String firstName;
    private String lastName;
    @OneToOne private Address address;
}

SDR/HATEOAS will return address as a link:

{
    "firstName": "Joe",
    "lastName": "Smith",
    "_links": {
        "self": { "href": "http://localhost:8080/persons/123123123" },
        "address": { "href": "http://localhost:8080/addresses/9127391273" }
    }
}

The default format can change depending on what you have on your classpath. I believe this is HAL in my example which is the default when you've included SDR and HATEOAS. It may be different but similar depending on said config.

Spring will do this when Address is managed by SDR. If it were not managed by SDR at all it would include the entire address object in the response. I suspect that alone explains the behavior you're seeing.

Roles

You haven't included information on UserRole but based on your code it appears that is likely not managed outside of User and therefore doesn't have a Spring Data repository registered. If this is the case that's why it's getting embedded -- there's no other repository to 'link' to.

The content and links under roles looks like Spring trying to serialize it like a Page. Generally content will have an array of resources and links will have the links such as 'self' or links to other resources. I'm not sure what's causing that.

Place

Place has it's own Spring Data repository so it's going to be treated as a managed entity and linked to rather than embedded. I suspect what you're looking for is a projection. Checkout the Spring documentation on projections. It would look something like this:

@Projection(name = "embedPlaces", types = { User.class })
interface EmbedPlaces {
    String getUsername();
    boolean isEnabled();
    Set<Place> getPlaces();
}

That should serialize the username, enabled and roles and omit everything else. I've not personally used projections yet so I can't vouch for how well it works but this is the solution in the documentation.

EDIT: While we're at it please note that this applies to creating or updating resources as well. Spring will expect the resource as a URL. So taking the Person/Address example if I were creating a new person my body might look like:

{
    "firstName": "New",
    "lastName": "Person",
    "address": "http://localhost:8080/addresses/1290312039123"
}

It's rather easy to forget these things as the vast, vast, vast, vast, vast majority of "REST" APIs are not REST and SDR/HATEOAS take an opinionated view of REST (e.g. that it should be REST, for one).

kab
  • 131
  • 4
-1

You can very well use a @ResponseEntity in your Controller and then set User Object in the ResponseEntity.

Please see example below:

ResponseEntity<User> respEntity = new ResponseEntity<User>(user, HttpStatus.OK);

Then on the client side you can call, restTemplate.getForEntity

Please see restTemplate documentation below:

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html#getForObject-java.lang.String-java.lang.Class-java.lang.Object...-

shankarsh15
  • 1,947
  • 1
  • 11
  • 16