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
}
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With