Home > database >  Spring Data REST inconsistent custom Controller enpoints
Spring Data REST inconsistent custom Controller enpoints

Time:10-25

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 Items:

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

  • Related