Search code examples
javascriptangularjsspring-data-resthateoasspring-hateoas

How to loop and retrieve value from HATEOAS _link attribute (e.g. retrieve a description)?


tl;dr

My code gets an array of javascript/json objects from a Restful GET. How do I write code to loop and retrieve, for display, a description (or any value) from a HATEOAS "_link" attribute?

Context

I've inherited a small Spring-based project --it tracks servers, software installations, etc for our team's internal use. It uses Angular front end and Spring/java/mysql back end for a restful back end.

I'm in the process of converting the hand-coded SQL to JPA and 'spring starter data rest'

Current API Does

The current handcoded SQL joins tables to give "display friendly results".

Product
---------
Product ID
Product Name
Category ID

Category
---------
Category ID
Category Name

The 'retrieve products' sql joins product and category to give a 'display friendly name'. The Rest API retrieves an 'product object' with this 'category name' tacked on.

Spring Data Rest Domain Objects

Product

@Entity
@Table(name="products")
public class Products
{

    private static final long serialVersionUID = 5697367593400296932L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)

    public long id;

    public String product_name;

    @ManyToOne(optional = false,cascade= CascadeType.MERGE)
    @JoinColumn(name = "category_id")
    private ProductCategory productCategory;

    public Products(){}

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Products(String product_name) {
        this.product_name = product_name;
    }

    public String getProduct_name() {
        return product_name;
    }

    public void setProduct_name(String product_name) {
        this.product_name = product_name;
    }

    public ProductCategory getProductCategory()
    {
        return productCategory;
    }

    public void setProductCategory(ProductCategory pProductCategory)
    {
        productCategory = pProductCategory;
    }
}

Product Category

@Entity
@Table(name="productcat")

public class ProductCategory implements Serializable{

        private static final long serialVersionUID = 890485159724195243L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;
    public String category;

    @OneToMany(mappedBy = "productCategory", cascade = CascadeType.ALL)
    @JsonBackReference
    Set<Products> products;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public Set<Products> getProducts() {
        return products;
    }

}

Problem

I've removed the joins, and added Spring repositories for Product and Category. The 'get products' restful API now returns a list of these:

{
      "product_name" : "ForceFive 1.0",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/rest/products/8"
        },
        "products" : {
          "href" : "http://localhost:8080/api/rest/products/8"
        },
        "productCategory" : {
          "href" : "http://localhost:8080/api/rest/products/8/productCategory"
        }
      }

Question

How do I display the category name for the "productCategory" link?

"productCategory" : {
  "href" : "http://localhost:8080/api/rest/products/8/productCategory"
}

I initially thought I could retrieve the categories, then build a map of 'category url' to 'description.' However the url's differ:

The 'product category' url looks liks this: http://localhost:8080/api/rest/productcat/1

Whereas the Product has this: "productCategory" : { "href" : "http://localhost:8080/api/rest/products/8/productCategory" }

Problem Clarification

so if the javascript controller http gets the products:

requests.get(productsUrl, $scope).success(function(data, status){
    $scope.models = data._embedded.products;  
});

So then what? Are these the steps?

The javascript controller loops through each data._embedded.products. for each product, the code

-stores the description somewhere for reuse on the page (i.e. adding it to the javascript object )

If these are the steps:

  • that's a lot of code
  • for a long list (e.g. 50 products), that's another 100 http get requests.

Even if I add a call to get/cache all product categories, that's still an extra 50 http gets

Bonus for:

  • easy to code
  • fast to run

Attempt #01: Added Projections

I added this projection:

  public interface ProductRepository extends PagingAndSortingRepository<Products, Long>
    {

       @Projection(name = "dummyNameForProjection", types = { Products.class })
       interface VirtualProjection
       {

          String getProduct_name() ;
          //Get Product Category
          @Value("#{target.productCategory.category}")
          String getCategoryName();
       }
    }

However this url does not return the category name:

http://localhost:8080/api/rest/products/4?projection=dummyNameForProjection

Returned json

{
    "product_name": "Force Five",
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/rest/products/4"
        },
        "products": {
            "href": "http://localhost:8080/api/rest/products/4"
        },
        "productCategory": {
            "href": "http://localhost:8080/api/rest/products/4/productCategory"
        }
    }
}

Additionally, I set debugging on for these

 logging.level.org.springframework.data=DEBUG
 logging.level.org.springframework.data.rest=DEBUG
 logging.level.org.hibernate=DEBUG

Log/console do not mention any projections.

Attempt #02: Fixed Projections File under D for D'oh. I stuck projection on the repository not on the entity. Reviewing some sample code showed the issue. Projections are the ticket!


Solution

  • Spring Projection

    @Projection(name = "dummyNameForProjection", types = { Product.class })
    public interface VirtualProjection {
    
    // this will get the attribute name from the product entity
    String getProductName();
    
    //this will get the name of the category entity related to the product
    @Value("#{target.category.name}") 
    String getCategoryName();
    

    }

    Just include projection name in your request ex: /products?projection=dummyNameForProjection