I've got a Spring Data Rest application with a sent of entities exposed as REST endpoints.
My main entity is a Listing
, which includes a list of Item
s:
@Entity
@Data
@AllArgsConstructor
@RequiredArgsConstructor
public class Listing extends RepresentationModel<Listing> {
@Id
@GeneratedValue
private Long id;
private int type;
private String name;
private String description;
.
.
.
@JsonIgnore
@OneToMany(mappedBy = "listing")
private List<Item> items;
}
@Entity
@Data
@AllArgsConstructor
@RequiredArgsConstructor
public class Item extends RepresentationModel<Item> {
@Id
@GeneratedValue
private Long id;
private String title;
private int quantity;
private float price;
.
.
.
@ManyToOne
@JoinColumn(name="listing_id")
public Listing listing;
}
Each of the entities has its own Spring Data Repository:
public interface ItemRepository extends CrudRepository<Item, Long> {
}
public interface ListingRepository extends CrudRepository<Listing, Long> {
}
If I GET all listings from http://<IP>:<PORT>/listings
, as expected I get the following json:
curl "http://192.168.99.100:8080/listings"
{
"_embedded" : {
"listings" : [ {
"type" : 0,
"name" : "one",
"description" : "this is one",
"_links" : {
"self" : {
"href" : "http://192.168.99.100:8080/listings/1"
},
"listing" : {
"href" : "http://192.168.99.100:8080/listings/1"
},
"items" : {
"href" : "http://192.168.99.100:8080/listings/1/items"
}
}
},
.
.
.
If I create a custom controller, thoug:
@RestController
public class CustomController{
@Autowired
private ListingRepository repository;
@GetMapping("/matches")
public Iterable<Listing> getMatches() {
Iterable<Listing> listings = repository.findAll();
return listings;
}
.
.
.
And invoke http://<IP>:<PORT>/matches
(which in theory should return the same results as the \listings
endpoint), I get the following json:
[
{
"id": 1,
"type": 0,
"name": "one",
"description": "this is one",
"links": []
},
.
.
.
That is, there are two differences: (1) I have a "links vs. "_links" field and (2, most important) in the /matches endpoint in the CustomController "links" are always empty. How can I get more consistent results?
These are the relevant parts of my pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
.
.
.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
.
.
.
</dependencies>
NOTE: IN Listing
entity, I had to put @JsonIgnore
to the items collection, because I was getting an infinite output, with a listing including items, each of which included the listing, including again the items, etc.
CodePudding user response:
(2, most important) in the /matches endpoint in the CustomController "links" are always empty.
Manual use of @RestController
gives the developer direct control of how the endpoint should work.
In this situation spring
can't just take over and implement another workflow for links. You have to provide the code with which the links are to be created in each response.
You need to add in dependencies the
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
And then modify your controller into
@GetMapping("/matches")
public Iterable<EntityModel<Listing>> getMatches() {
Iterable<Listing> listings = repository.findAll();
return Stream.of(listings).map(listing ->
EntityModel.of(listing,
linkTo(methodOn(CustomController.class).findOne(id)).withSelfRel()
).collect(Collectors.toList())
return listings;
}
If you inspect closely you will understand that
linkTo(methodOn(CustomController.class).findOne(id)).withSelfRel()
creates a link for each specific Listing
to be retrieved from this controller. For this to be provided to the user, a such endpoint should also be exposed from the same controller. Otherwise it would not make any sence. So in the same controller you also need to provide the
@GetMapping("/listings/{id}")
EntityModel<Listing> findOne(@PathVariable Long id) {
Listing listing = repository.findById(id) //
.orElseThrow(() -> new RunntimeException(id));
return EntityModel.of(listing, //
linkTo(methodOn(CustomController.class).findOne(id)).withSelfRel()
);
}
The above is just an example as to how you can provide 1 link for each Listing
response with which the user could retrieve the specific Listing
directly from controller. Understanding this you can now build all the links you expect to deliver as well.
On the other side, when you plug in spring-data-rest
then all those functionalities are provided by default from spring
like a black box where you have limited access to make configurations. When you use normal controllers you still have hateoas-support
but the functionality need to be manually implemented by you, in how you expect it to work.