My idea is to generate a CRUD for a Movie database so the first thing I want to design is the DB ERD:
So I wanted to store Movies, with their directors and actors, also wanted to add a Catalog of categories so we can search by it.
So with these I know that the application should have 4 views to implement CRUD in each table.
I used Spring Initializer to generate a Maven based project with the Spring Web Dependencies, I added Spring Web, and Spring Data for the Hibernate dependencies.
So first I wanted to see if everything worked so I just created a WelcomeController class
package com.bebo.moviecrud.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class WelcomeController {
@GetMapping("/welcome")
public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) {
model.addAttribute("name", name);
return "greeting";
}
}
Its just a Simple controller that binds the /welcome path to resources/templates/welcome.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Welcome, ' + ${name} + '!'" />
</body>
</html>
I tried to run the app using ./mvnw spring-boot:run but it fails with:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
So I guess its because I added the Spring Data dependencies and its trying to connect to the database as default, I created my DB using MySQL so lets add the dependencies and the configurations files to do the connection:
First the MySQL Driver Dependency:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Then the src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/movies
spring.datasource.username=moviesAdmin
spring.datasource.password=Mexico123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
server.port=8081
I also changed the default port that Spring Boot uses to 8081
And with that I re run ./mvnw spring-boot:run
And now is working, good old Hello World !
So, I have already create my DB, can I automatically generate my Entities using Hibernate?
I remember it was a thing with Eclipse and eclipse link, but for this POC im using Visual Studio Code, so lets search if there is a tool for that… hmm , 10 minutes later I just find the plugin for Eclipse and IntelliJ, well I guess I have to generate them by hand, at least its not a big DB.
So lets start by the Category catalog, its just 2 fields
package com.bebo.moviecrud.model.entities;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
@Entity
@Table(name = "categories")
@Data
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long categoryId;
private String categoryName;
}
I also added loombok library to handle the getters and setters.
Its needs a Repository so:
package com.bebo.moviecrud.model.repositories;
import com.bebo.moviecrud.model.entities.Category;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CategoryRepository extends CrudRepository<Category, Long> {}
Now the controller to handle the logic, to the Create, Read, Update and Delete
package com.bebo.moviecrud.controller;
import javax.validation.Valid;
import com.bebo.moviecrud.model.entities.Category;
import com.bebo.moviecrud.model.repositories.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class CategoryController {
private final CategoryRepository categoryRepository;
@Autowired
public CategoryController(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@GetMapping("/addcat")
public String showAddCategoryForm(Category category) {
return "add-category";
}
@PostMapping("/addcategory")
public String addCategory(@Valid Category category, BindingResult result, Model model) {
if (result.hasErrors()) {
return "add-category";
}
categoryRepository.save(category);
return "redirect:/categories";
}
@GetMapping("/editcategory/{id}")
public String showUpdateCategoryForm(@PathVariable("id") long id, Model model) {
Category category = categoryRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid category Id:" + id));
model.addAttribute("category", category);
return "update-category";
}
@GetMapping("/deletecategory/{id}")
public String deleteCategory(@PathVariable("id") long id, Model model) {
Category category = categoryRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid user Id:" + id));
categoryRepository.delete(category);
return "redirect:/categories";
}
@PostMapping("/updatecategory/{id}")
public String updateCategory(@PathVariable("id") long id, @Valid Category category, BindingResult result, Model model) {
category.setCategoryId(id);
categoryRepository.save(category);
return "redirect:/categories";
}
@GetMapping("/categories")
public String showCategoryList(Model model) {
model.addAttribute("categories", categoryRepository.findAll());
return "categories";
}
}
With the controller created, I just need the views:
So categories.html, list all the categories and add the delete and edit butons to each, as well as an add button to crete new Categories
<div th:switch="${categories}">
<h2 th:case="null">No categories yet!</h2>
<div th:case="*">
<h2>Categories</h2>
<table>
<thead>
<tr>
<th>Category Name</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr th:each="category : ${categories}">
<td th:text="${category.categoryName}"></td>
<td><a th:href="@{/editcategory/{id}(id=${category.categoryId})}">Edit</a></td>
<td><a th:href="@{/deletecategory/{id}(id=${category.categoryId})}">Delete</a></td>
</tr>
</tbody>
</table>
</div>
<p><a href="/addcat">Add a new category</a></p>
</div>
The add-categories.html recive the input from the user to add a category:
<form action="#" th:action="@{/addcategory}" th:object="${category}" method="post">
<label for="name">Category Name</label>
<input type="text" th:field="*{categoryName}" id="name" placeholder="name">
<span th:if="${#fields.hasErrors('categoryName')}" th:errors="*{name}"></span>
<input type="submit" value="Add Category">
</form>
And last the update.category.html its very similar but handles the update-category
<form action="#"
th:action="@{/updatecategory/{id}(id=${category.categoryId})}"
th:object="${category}"
method="post">
<label for="name">Category Name</label>
<input type="text" th:field="*{categoryName}" id="name" placeholder="Name">
<span th:if="${#fields.hasErrors('categoryName')}" th:errors="*{categoryName}"></span>
<input type="submit" value="Update Category">
</form>
So with that I can show how to do a simple CRUD with one entity, lets review the assigment:
• Spring Transactions • Sprint AOP • Spring Web • Hibernate o Hibernate criteria o HQL o Hibernate mappings o Hibernate Inheritance mapping
This covers: Spring Web, Hibernate and Hibernate Mappings
So next I will cover Hibernate Inheritance, its a way to handle inhereitance in entities without interfiering wit the actual database, and my tables Directors and Employes are very similar, so I can create a Super class called Person and that Directors and Employees inherits from it.
Lets see if I can pull it off:
So I create the Person MappedSuperClass
package com.bebo.moviecrud.model.entities;
import java.sql.Date;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.Data;
@MappedSuperclass
@Data
public class Person {
@Id
private long id;
private String name;
private String gender;
private Date birthdate;
}
And then the Directors and the Actores entities are just empty classes that heredits from person, but with the Inheritance strategy TABLE_PER_CLASS to keep each table separately.
Actor.java
package com.bebo.moviecrud.model.entities;
import javax.persistence.Entity;
import javax.persistence.*;
@Entity
@Table(name = "actors")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Actor extends Person {
}
Director.java
package com.bebo.moviecrud.model.entities;
import javax.persistence.*;
@Entity
@Table(name = "directors")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Director extends Person {
}
Ok so now we need a Repository and a Controller, but because I created a super class I can creat just PersonRepository and PersonController to handle the logic to both of these entities.
PersonRepository.java
package com.bebo.moviecrud.model.repositories;
import com.bebo.moviecrud.model.entities.Person;
import org.springframework.data.repository.CrudRepository;
public interface PersonRepository extends CrudRepository<Person, Long> {}
Ok so very much like the CategoriesController, I inyect the Repository
@Autowired
public PersonController(PersonRepository personRepository) {
this.personRepository = personRepository;
}
and then try to get all the directors like this:
@GetMapping("/directors")
public String showDirectorsList(Model model) {
model.addAttribute("directors", personRepository.findAll());
return "directors";
}
but wait, is Hibernate smart enough to give me the correct entity?
context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: Not an entity: class com.bebo.moviecrud.model.entities.Person; nested exception is java.lang.IllegalArgumentException: Not an entity: class com.bebo.moviecrud.model.entities.Person] with root cause
I guess not, so Person is not an entity class and cant give me results, hmm so how about if I add a method in the controller to find all directors:
@Query("from Director")
List<Director> findAllDirectors();
So here im using HQL sintaxis, insetad of telling the tale name, im telling the entity name, lets change the in the controller to use this instead:
@GetMapping("/directors")
public String showDirectorsList(Model model) {
model.addAttribute("directors", personRepository.findAllDirectors());
return "directors";
}
and with that and the corresponding html templates we have our directors showing up.
So for actors is a copy paste, so now I can finally put some Movies in .
So for Movies it went similar but I had to figure out how to get relationships
package com.bebo.moviecrud.model.entities;
import java.sql.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.Data;
@Entity
@Table(name = "movies")
@Data
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long movieId;
private String movieName;
private String movieSinopsis;
@OneToOne(optional = false)
@JoinColumn(name = "director_id", referencedColumnName = "id")
private Director director;
@OneToOne(optional = false)
@JoinColumn(name = "category_id")
private Category category;
private Date movieDate;
}
JoinColumn was doing some weird behavior, if I dont use it, even for category_id so, if the two collumns have the same name you still need to set the name attribute.
Also, because I'm using just one repository for Directors and Actors, Hibernate could not resolve the entities and its giving me Persons only, so in the end I created a DirectorsRepository just to get the correct table. So the View for add-movie finished like this:
<form action="#" th:action="@{/addmovie}" th:object="${movie}" method="post">
<label for="name">Movie Name</label>
<input type="text" th:field="*{movieName}" id="name" placeholder="name">
<span th:if="${#fields.hasErrors('movieName')}" th:errors="*{name}"></span>
<label for="name">Movie Sinopsis</label>
<input type="text" th:field="*{movieSinopsis}" id="sinopsis" placeholder="sinopsis">
<span th:if="${#fields.hasErrors('movieSinopsis')}" th:errors="*{sinopsis}"></span>
<label for="name">Movie Category</label>
<select class="form-control" th:field="*{category}">
<option value="0">select category</option>
<option th:each="category : ${categories}" th:value="${category.categoryId}" th:text="${category.categoryName}"></option>
</select>
<label for="name">Movie Director</label>
<select class="form-control" th:field="*{director}">
<option value="0">select director</option>
<option th:each="director : ${directors}" th:value="${director.id}" th:text="${director.name}"></option>
</select>
<label for="name">Movie Release</label>
<input type="date" th:field="*{movieDate}" id="movieDate" placeholder="movieDate">
<span th:if="${#fields.hasErrors('movieDate')}" th:errors="*{movieDate}"></span>
<input type="submit" value="Add Movie">
</form>
So with the basic functionality working i will be adding AOP, Aspects are a way to crosscut an application and adding functionality without modifying the implemented code. I will be adding an audit for DB access, so because all my methods are using the CrudRepository Interface i can set a pinpoint to all the methods in that class to log any acces to the DB.
package com.bebo.moviecrud.audit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ControllerAspect {
@Before(value = "execution(* org.springframework.data.repository.CrudRepository.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("DB Access from method: " + joinPoint.getSignature());
}
}
So i set it to Before, so it actually an attempt to access, but we can set it to Before and also also there is a Trow to catch errors specifically.
And also i Need to add the net tag to my MovieCrudApplication class
@EnableAspectJAutoProxy(proxyTargetClass=true)
For the Spring Transactions i was planning to do a Bulk insert of actors, but i was having troubles desingning a form that dinamically generate an actor form, so I did just a test method to demostrate the funcionality.
So First I tried to add the transactional method to the PersonController
@Transactional(rollbackFor = { SQLException.class })
public void bulkActors(List<Actor> actors) throws SQLException {
try {
for (Actor actor : actors) {
personRepository.save(actor);
}
} catch (Exception e) {
throw new SQLException("Throwing exception for demoing rollback");
}
}
and of course this is a very bad way to implement a Try but its just for the sake of the demonstration.
This was not rolling back, so I read that I needed another stereotype of @Component, the @Service, that its just a Component but for the Service logic, so I ended up creating a new Package with the service.