Simplifying CRUD Operations in Spring Boot

Simplifying CRUD Operations in Spring Boot

Creating a CRUD (Create, Read, Update, Delete) example in Spring Boot that adheres to SonarQube rules involves writing clean, efficient, and secure code. Below is a simplified example of a CRUD application that should meet SonarQube's quality standards. This example assumes you have basic Spring Boot setup done, including the necessary dependencies for Spring Web and Spring Data JPA in your pom.xml or build.gradle file.

Entity Class

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private double price;

    // Constructors, getters, and setters
    public Product() {
    }

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

Explanation

Sure, let's break down the Product entity class line by line:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

These lines import annotations from the javax.persistence package which are used for defining the class as an entity model for JPA (Java Persistence API).

@Entity

The @Entity annotation specifies that the class is an entity. This means it's a JPA entity and should be mapped to a database table.

public class Product {

Defines the class named Product. This class will be used to model the data that the application will be dealing with.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
  • @Id marks the id field as the primary key of the entity.

  • @GeneratedValue specifies that the primary key will be generated automatically by the database. The strategy GenerationType.IDENTITY indicates that the database will automatically increment the value.

    private String name;
    private double price;

These lines declare two private fields, name and price, representing the product's name and price, respectively.

    // Constructors, getters, and setters
    public Product() {
    }

Defines a no-argument constructor. JPA requires an entity to have a no-argument constructor.

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

This is a parameterized constructor used to create instances of Product with a name and price.

The // Getters and Setters comment indicates the following methods are getter and setter methods for the class fields. These methods are used to access and modify the private fields of the class.

    public Long getId() {
        return id;
    }

The getId method returns the value of the id field.

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

The setId method sets the value of the id field.

    public String getName() {
        return name;
    }

The getName method returns the value of the name field.

    public void setName(String name) {
        this.name = name;
    }

The setName method sets the value of the name field.

    public double getPrice() {
        return price;
    }

The getPrice method returns the value of the price field.

    public void setPrice(double price) {
        this.price = price;
    }

The setPrice method sets the value of the price field.

The class ends with a closing curly brace }.

Repository Interface

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Explanation

The ProductRepository interface is defined as follows:

import org.springframework.data.jpa.repository.JpaRepository;

This line imports the JpaRepository interface from Spring Data JPA. The JpaRepository is a JPA-specific extension of the Repository interface that provides a rich set of methods to manipulate entities.

public interface ProductRepository extends JpaRepository<Product, Long> {
}
  • public interface ProductRepository: This declares a public interface named ProductRepository. An interface in Java is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Interfaces cannot contain instance fields. The interface is intended to be implemented by classes that then provide implementations for the abstract methods declared by the interface.

  • extends JpaRepository<Product, Long>: This means that ProductRepository extends the JpaRepository interface. By extending JpaRepository, ProductRepository inherits a large number of methods for performing operations such as saving, deleting, and finding entities. JpaRepository takes two parameters: the entity type and the type of the entity's primary key. In this case, Product is the entity type, and Long is the type of its primary key. This setup enables ProductRepository to work with Product entities and their IDs of type Long.

The ProductRepository interface does not explicitly declare any methods because it inherits them from JpaRepository. However, it can be used to declare custom query methods that are specific to the Product entity

Service Class

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public List<Product> findAllProducts() {
        return productRepository.findAll();
    }

    public Optional<Product> findProductById(Long id) {
        return productRepository.findById(id);
    }

    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }

    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }

    public Product updateProduct(Long id, Product productDetails) {
        Product product = productRepository.findById(id).orElseThrow(() -> new RuntimeException("Product not found"));
        product.setName(productDetails.getName());
        product.setPrice(productDetails.getPrice());
        return productRepository.save(product);
    }
}

Explanation

Certainly, let's break down the ProductService class line by line:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
  • These lines import the necessary classes and annotations. @Autowired is used for dependency injection, @Service marks the class as a service component in Spring, List is used for collections, and Optional is a container object which may or may not contain a non-null value.
@Service
  • The @Service annotation marks this class as a service layer component, making it eligible for Spring's component scanning to detect and configure it as a Spring bean.
public class ProductService {
  • Declares the ProductService class.
    private final ProductRepository productRepository;
  • Declares a private final field of type ProductRepository. The final keyword indicates that the reference to productRepository cannot be changed once it has been assigned.
    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
  • This is a constructor with @Autowired annotation, which means Spring will automatically inject an instance of ProductRepository when ProductService is created. This is an example of constructor-based dependency injection.
    public List<Product> findAllProducts() {
        return productRepository.findAll();
    }
  • Defines a method findAllProducts that calls findAll on the productRepository to retrieve all products from the database. It returns a list of Product objects.
    public Optional<Product> findProductById(Long id) {
        return productRepository.findById(id);
    }
  • Defines a method findProductById which takes a Long id as a parameter and returns an Optional<Product>. It retrieves a product by its ID using the findById method of the repository.
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }
  • Defines a method saveProduct that takes a Product object as a parameter and returns a Product object. It saves the product to the database using the save method of the repository.
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
  • Defines a method deleteProduct that takes a Long id as a parameter. It deletes the product with the specified ID from the database using the deleteById method of the repository.
    public Product updateProduct(Long id, Product productDetails) {
        Product product = productRepository.findById(id).orElseThrow(() -> new RuntimeException("Product not found"));
        product.setName(productDetails.getName());
        product.setPrice(productDetails.getPrice());
        return productRepository.save(product);
    }
  • Defines a method updateProduct that takes a Long id and a Product productDetails as parameters and returns a Product object. It first retrieves the product with the given ID from the database. If the product is not found, it throws a RuntimeException. Then, it updates the product's name and price with the values from productDetails and saves the updated product to the database using the save method of the repository.

Controller Class

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAllProducts();
    }

    @GetMapping("/{id}")
    public Product getProductById(@PathVariable Long id) {
        return productService.findProductById(id).orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productService.saveProduct(product);
    }

    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        return productService.updateProduct(id, product);
    }

    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
    }
}

Explanation

The ProductController class manages HTTP requests for CRUD operations on products. Here's a detailed explanation:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

These lines import the necessary classes and annotations from Spring Framework to build the REST controller.

@RestController

This annotation marks the class as a controller where every method returns a domain object instead of a view. It's a shorthand for including both @Controller and @ResponseBody.

@RequestMapping("/products")

Specifies that all request mappings in this controller will be prefixed with /products.

public class ProductController {

Defines the class as a public controller class named ProductController.

    @Autowired
    private ProductService productService;

Automatically injects the ProductService instance, allowing the controller to use its services.

    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAllProducts();
    }

Defines a method to handle GET requests without any specific ID. It retrieves all products by calling findAllProducts on the productService.

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productService.findProductById(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

Handles GET requests for a specific product by its ID. Uses @PathVariable to extract the ID from the URL. If the product is found, it returns the product wrapped in a 200 OK response; otherwise, it returns a 404 Not Found response.

    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productService.saveProduct(product);
    }

Handles POST requests to create a new product. The product details are passed in the request body, saved by the productService, and then returned.

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product productDetails) {
        return productService.updateProduct(id, productDetails)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

Manages PUT requests to update an existing product by its ID. The new product details are in the request body. If the product is successfully updated, it returns a 200 OK response; otherwise, it returns a 404 Not Found response.

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.ok().build();
    }

Handles DELETE requests to remove a product by its ID. After deletion, it returns a 200 OK response to signify successful deletion.

This breakdown explains how the ProductController class processes HTTP requests for CRUD operations on products, leveraging Spring Boot's annotations and patterns.

Controller using Response Entity

(same code)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.findAllProducts();
        return ResponseEntity.ok(products);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productService.findProductById(id)
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        Product savedProduct = productService.saveProduct(product);
        return ResponseEntity.ok(savedProduct);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
        Product updatedProduct = productService.updateProduct(id, product);
        return ResponseEntity.ok(updatedProduct);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.ok().build();
    }
}

Explanation in detail :

This Spring Boot ProductController class is structured to handle RESTful web services for CRUD operations on a Product entity. Here's a detailed explanation of each line:

  1. import org.springframework.beans.factory.annotation.Autowired;
    This imports the Autowired annotation, enabling Spring's dependency injection mechanism.

  2. import org.springframework.http.ResponseEntity;
    This imports the ResponseEntity class, which allows the controller to specify HTTP status codes and response bodies.

  3. import org.springframework.web.bind.annotation.*;
    This imports Spring MVC annotations used for web controllers, including @RestController, @RequestMapping, @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping.

  4. import java.util.List;
    Imports the List interface from the Java Collections framework for handling collections of objects.

  5. @RestController
    Marks the class as a RESTful controller, indicating that each method's return value is automatically written directly to the HTTP response body.

  6. @RequestMapping("/api/products")
    Defines the base URI for all resource URLs provided by this controller, setting the path to /api/products.

  7. public class ProductController {
    Declares the ProductController class, defining the structure for handling product-related requests.

  8. private final ProductService productService;
    Declares a ProductService dependency that will be injected by Spring, marked as final to indicate it won't change after construction.

  9. @Autowired public ProductController(ProductService productService) {
    Defines a constructor for ProductController with ProductService as a parameter, demonstrating constructor-based dependency injection.

  10. this.productService = productService;
    Assigns the injected ProductService instance to the productService field within the controller.

  11. @GetMapping
    Indicates that the method following this annotation handles HTTP GET requests, fetching all products.

  12. public ResponseEntity> getAllProducts() {
    Defines a method to retrieve all products, returning them wrapped in a ResponseEntity to include HTTP status information.

  13. List products = productService.findAllProducts();
    Invokes findAllProducts() method of ProductService to fetch all products, storing them in a list.

  14. return ResponseEntity.ok(products);
    Wraps the list of products in a ResponseEntity with an HTTP status of OK (200), preparing it for the response.

  15. @GetMapping("/{id}")
    Specifies that the following method handles GET requests for a specific product identified by its {id} path variable.

  16. public ResponseEntity getProductById(@PathVariable Long id) {
    Defines a method to fetch a single product by its ID, with the possibility of throwing a runtime exception if not found.

  17. @PostMapping
    Marks the method to handle POST requests, used for creating a new product.

  18. public ResponseEntity createProduct(@RequestBody Product product) {
    Defines a method to save a new product passed in the request body, returning the saved product.

  19. Product savedProduct = productService.saveProduct(product);
    Calls saveProduct() on ProductService to persist the new product, capturing the saved instance.

  20. return ResponseEntity.ok(savedProduct);
    Returns the saved product within a ResponseEntity with an OK status, indicating success.

  21. @PutMapping("/{id}")
    Indicates that the following method handles PUT requests for updating a product by its ID.

  22. public ResponseEntity updateProduct(@PathVariable Long id, @RequestBody Product product) {
    Defines a method to update an existing product, identified by ID, with new data from the request body.

  23. Product updatedProduct = productService.updateProduct(id, product);
    Updates the product using ProductService, storing the updated version.

  24. return ResponseEntity.ok(updatedProduct);
    Wraps the updated product in a ResponseEntity with an OK status, signifying a successful update.

  25. @DeleteMapping("/{id}")
    Marks a method to handle DELETE requests, used for removing a product by its ID.

  26. public ResponseEntity deleteProduct(@PathVariable Long id) {
    Defines a method to delete a product, returning an empty ResponseEntity to indicate the operation's success.

  27. productService.deleteProduct(id);
    Calls deleteProduct() on ProductService to remove the specified product.

  28. return ResponseEntity.ok().build();
    Builds and returns an empty ResponseEntity with an OK status, confirming the product's deletion.