Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing Spring MVC + Spring Data Rest results in odd MVC responses

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": "[email protected]",
  "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
}
like image 749
bvulaj Avatar asked May 14 '16 22:05

bvulaj


1 Answers

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

like image 184
kab Avatar answered Nov 06 '22 22:11

kab