Building RESTful API with Spring JPA in a Complex Domain

codemonkey

Thirumalai Parthasarathi

Posted on August 14, 2020

Building RESTful API with Spring JPA in a Complex Domain

You may be thinking this is just another tutorial with a Simple Book object with scalar fields. But hell no! This is a tutorial with nested domain objects that will throw LazyInitializationException on your face if you are a little less careful.

Without further ado let's proceed towards discussing the domain.

Let us take the domain of online shopping. Without thinking much about Authentication & Authorization, Order Conversion, Billing, Shipping etc., that is present in the domain, Let us only concentrate on adding a Product(s) to a shopping cart.

A Seller sells a Product. A Customer who wants to buy it, adds it to his/her Cart. The nouns in that sentence obviously hints the entities that we must have in our application model.

Minimal ER Diagram

Before diving into the Code and Examples, make sure you have the following in your spring boot application.properties file.

spring.jpa.open-in-view=false

That one is pure evil!

CartItem.java
@Entity
public class CartItem implements Serializable {

    @Id
    @GeneratedValue(generator = "COMMON_ID_GENERATOR")
    private Long id;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_CUSTOMER_CARTITEM"))
    private Customer customer;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PRODUCT_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_PRODUCT_CARTITEM"))
    private Product product;

    @NotNull
    @Min(1)
    @Column(nullable = false)
    private int quantity;
    // omitting other fields, getters & setters for readability
}

I'm not gonna bore you with the mundane details of how the Customer, Seller and Product classes look like. You can rest assured it is similar in style if not simpler. The ER diagram is pretty self explanatory.

Let's move on to the Repository and CartService. I have used Spring JPA for the repository so it is only a simple interface that extends the Spring JPARepository interface.

CartRepository.java
public interface CartRepository extends JpaRepository<CartItem, Long> {
    public List<CartItem> findAllByCustomer(Customer customer);
}
CartService.java
@Service
public class CartService {

    @Autowired
    private CartRepository cartRepo;

    @Transactional
    public List<CartItem> addToCart(CartItem cartItem) {
        cartItem = cartRepo.save(cartItem);
        return cartRepo.findAllByCustomer(cartItem.getCustomer());
    }
}

After this we can write the controller, which is simply going to call our Cart Service to save the CartItem object and return the List of CartItems.

CartRestController.java
@RestController
@RequestMapping("/api/v1/order")
public class OrderRestController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/cart")
    public ResponseEntity<List<CartItem>> addToCart(@RequestBody CartItem cartItem) throws JsonProcessingException {
        return ResponseEntity.ok(orderService.addToCart(cartItem));
    }
}

Now for testing this, we are gonna use @SpringBootTest to run a proper System test instead of running a Narrow Integration Test using Mocks. (See Martin Fowler's blog for more info on the concept of Narrow integration tests)

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// other imports omitted
@SpringBootTest
@AutoConfigureMockMvc
public class OrderRestControllerSystemTest {

    @Autowired
    private CustomerRepository customerRepo;

    @Autowired
    private SellerRepository sellerRepo;

    @Autowired
    private ProductRepository productRepo;

    @Autowired
    private CartRepository cartRepo;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void addToCart_Adds_CartItem_And_Returns_AllCartItems() throws JsonProcessingException, Exception {
        Customer customer = new Customer();
        customer.setUsername("tUser");
        customer.setEmailAddress("tUser@test.com");
        customer.setId(customerRepo.save(customer).getId());

        Seller seller = new Seller();
        seller.setName("The Store");
        seller.setEmailAddress("thestore@test.com");
        seller.setId(sellerRepo.save(seller).getId());

        Product product = new Product();
        product.setSku("RBK2018BLS");
        product.setName("Reebok Shoe");
        product.setRate(new BigDecimal(120));
        product.setSeller(seller);
        product.setId(productRepo.save(product).getId());

        CartItem cartItem = new CartItem();
        cartItem.setCustomer(customer);
        cartItem.setProduct(product);
        cartItem.setQuantity(2);

        String input = objectMapper.writeValueAsString(cartItem);   
        MvcResult result = mockMvc
          .perform(post("/api/v1/order/cart")
            .contentType(MediaType.APPLICATION_JSON)
            .characterEncoding("UTF-8")
            .content(input).accept(MediaType.APPLICATION_JSON))
          .andExpect(status().isOk()).andReturn();
        List<CartItem> cartItemList = objectMapper.readerForListOf(CartItem.class)
          .readValue(result.getResponse().getContentAsString());
        assertThat(cartItemList.size()).isEqualTo(1);
        }
}

The test code looks complex but is really a simple thing.

// enables us to test the entire service stack
// by starting the server
@SpringBootTest 

// automatically configures the MockMvc object
// which makes us perform HTTP requests to the controller
@AutoConfigureMockMvc

Then in the test method, we create sample data that must pre-exist for creating a CartItem object, like Customer, Seller and Product. We save them using the respective repositories which are all simple JPARepository implementations.

It then creates a POST HTTP request with the CartItem as the content and sets up the request by setting the relevant headers.

// creates a HTTP POST request for the URI given
post("/api/v1/order/cart")

// informs the server the data being sent is Json
.contentType(MediaType.APPLICATION_JSON)

// informs the server that we expect back some Json data
.accept(MediaType.APPLICATION_JSON)

We verify that the server responds with 200 OK by calling the status().isOk() method from the static import.

We also verify that the Json Data returned from the server can be deserialized into List<CartItem> objects and the count is 1.

If you run this test, you will see that this will miserably fail!!!

Now now, don't get mad. I'm giving you a failed example only so that you will learn how to solve a problem rather than just copying/typing and compiling & running some random bit of code from the internet.

Why does it fail?

The reason is the JSON serialization that happens behind the scenes to provide the client with the CartItem data in JSON format.

When we call the findAllByCustomer method from JPA repository, it returns only the CartItem data initialized. The foreign keys (Product & Customer) are not fully initialized and only a proxy is supplied.

When this gets returned from the service method into the controller method, the transaction that produced the CartItem object is committed and closed. Now upon returning the List, spring knows that the client requested this in JSON format and tries to serialize the data.

But the proxy cannot be serialized as it will throw LazyInitializationException.

How do we solve this?

Well we can write a custom JPQL to eagerly fetch the desired data like this.

CartRepository.java
public interface CartRepository extends JpaRepository<CartItem, Long> {
    @Query("select ci from CartItem ci "
            + "join fetch ci.customer c "
            + "join fetch ci.product p "
            + "join fetch p.seller s " // why are we pulling the seller info?
            + "where ci.customer= :customer")
    public List<CartItem> findAllByCustomer(@Param("customer") Customer customer);
}

This will make your test pass but why are we pulling the Seller information too? Because JSON serialization does a deep parsing by traversing the object graph.

Though this works, it is not ideal. To fix this we can use DTO projections. But that's another article for another time.

Hope this was informative to you folks. :)

💖 💪 🙅 🚩
codemonkey
Thirumalai Parthasarathi

Posted on August 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related