The Spring Boot Manual
Spring Boot is a framework that simplifies the development and deployment of Spring applications by providing production-ready defaults, easy dependency management, and auto-configuration.
Some key factors:
- Reduces JEE complexity
- POJO based and Interface Driven
- Lightweight and unobtrusive
- AOP/Proxies
- Annotation based configuration
JPA stands for Java persistence API, ORM design principle, POJO based and offers multiple persistence providers.
JPA’s purpose is to simplify development of databases, configuration, data modeling and it’s more business focused than other solutions.
JPA removes a lot of boilerplate code, it’s object focused, spring handles all the configuration, it’s easily testable and transactions are transparent.
The important files of a Spring Boot application are:
- AppConfig.java: launches the application.
- application.properties: contains all the information needed for the application to work, such as database urls.
The following is the Spring with JPA architecture:
Components
Controllers
In Spring Boot, a controller is a Java class that processes incoming HTTP requests and generates appropriate responses. Controllers are responsible for handling requests and managing the application's workflow.
Business logic should not be handled in the controller, instead it should grab the information and return it as a response.
Controllers are usually annotated with the @Controller annotation, which is a specialization of the @Component annotation. Spring Boot automatically detects and registers all @Controller classes and creates a URL mapping to the methods within them.
Here's an example of a simple controller in Spring Boot:
@Controller
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String sayHello() {
return "Hello, world!";
}
}
In this example, the @RequestMapping annotation specifies that all requests to the "/hello" path will be handled by the HelloController. The @GetMapping annotation specifies that the sayHello() method should handle GET requests.
Here's another example of a controller that uses path variables:
@Controller
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String getUser(@PathVariable("id") Long id, Model
model) {
// Get user by id and add it to the model
User user = userService.findById(id);
model.addAttribute("user", user);
return "userDetails";
}
In this example, the @GetMapping annotation specifies that the getUser() method should handle GET requests to the "/users/{id}" path, where {id} is a path variable. The @PathVariable annotation is used to map the value of the {id} variable to the id method parameter.
The Model parameter is used to add the User object to the model, which is then used to render the "userDetails" view.
These are just a couple of examples of controllers in Spring Boot. Controllers can handle many types of requests and can interact with various services and repositories to process and manipulate data.
Model is added to the HTTP request by Springboot automatically when the request is received from the client.
Here's an example of a Spring Boot controller method that uses @RequestParam to handle HTTP requests with query parameters:
@Controller
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping("/hello")
public String sayHello(@RequestParam(name = "name", required
= false, defaultValue = "World") String name, Model model) {
model.addAttribute("greeting", "Hello,
" + name + "!");
return "greeting";
}
}
In this example, the @RequestParam annotation has three parameters specified:
- name: Specifies the name of the query parameter. In this case, the name is "name".
- required: Specifies whether the parameter is required or not. If required is set to true (the default value), Spring Boot will throw an exception if the parameter is not present in the request. If required is set to false, the parameter is optional and Spring Boot will not throw an exception if it's missing.
- defaultValue: Specifies a default value for the parameter if it's not present in the request. In this example, the default value is "World".
So in this example, if a request is made to /greeting/hello without the name query parameter, the value of name will be set to "World" and the response will be "Hello, World!". However, if a request is made to /greeting/hello?name=John, the response will be "Hello, John!".
RestController
The main difference between @Controller and @RestController is that the latter is a specialized version of the former that is used for building RESTful web services. While @Controller is used to render web pages, @RestController is used to return data in JSON, XML, or other formats that can be consumed by other applications.
Here's the updated GreetingController implemented as a @RestController instead of a @Controller:
@RestController
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping("/hello")
public String sayHello(@RequestParam(name = "name", required
= false, defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
}
In a @RestController, you typically don't return a view name as you do with @Controller. Instead, you directly return the data that should be sent back to the client. In the updated GreetingController, the sayHello() method returns a plain string that will be automatically serialized to JSON or XML by Spring Boot based on the Accept header of the incoming request.
Another difference between the two is that @RestController is a combination of @Controller and @ResponseBody. This means that every method in a @RestController class is automatically annotated with @ResponseBody, which tells Spring Boot to serialize the return value of the method and include it in the response body.
Here's an example of a @RestController that returns a JSON greeting message:
@RestController
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping("/hello")
public Map<String, String> sayHello(@RequestParam(name =
"name", required = false, defaultValue = "World") String name) {
Map<String, String> message = new
HashMap<>();
message.put("message", "Hello, " +
name + "!");
return message;
}
}
In this example, the @RestController annotation tells Spring Boot to treat this class as a RESTful web service that returns data in JSON format.
The @GetMapping annotation maps the HTTP GET requests to the /greeting/hello endpoint. The @RequestParam annotation specifies that the name parameter is optional and has a default value of "World". The sayHello() method creates a Map that contains the greeting message and returns it. The Map is automatically serialized to JSON by Spring Boot and included in the response body.
When a GET request is made to /greeting/hello, the response will be a JSON object that looks like this:
{
"message": "Hello, World!"
}
If a value is provided for the name parameter, the message will be personalized accordingly.
Service
In Spring Boot, services are typically implemented as Java classes annotated with the @Service annotation, which indicates that the class is a service component and will be managed by the Spring container. Services in Spring Boot are used to implement the business logic of an application, which typically involves manipulating data and executing complex operations.
Spring Boot services can use dependency injection to inject other Spring-managed components, such as repositories and other services, to perform their work. This allows for loose coupling between components and promotes code reuse and testability.
Here are some key features and best practices when working with Spring Boot services:
Services should be designed to be stateless wherever possible. This makes them easier to test and can improve performance by allowing the Spring container to reuse the same instance of the service for multiple requests.
Services should define clear, well-defined interfaces that describe their functionality. This makes it easier to understand how to use the service and promotes code reuse.
Services should handle exceptions in a consistent and meaningful way. This typically involves catching exceptions and converting them to appropriate error responses, which can then be returned to the caller or logged for later analysis.
Services should be unit tested thoroughly to ensure they behave as expected. This typically involves mocking dependencies and writing tests that cover a range of scenarios and edge cases.
Services can also be integrated with other Spring Boot features, such as security and caching, to implement more complex functionality. For example, a service may use Spring Security to authenticate users before allowing them to access certain resources.
Overall, Spring Boot services provide a flexible and powerful way to implement the business logic of an application, while promoting loose coupling and testability. By following best practices and leveraging the full range of Spring Boot features, you can build robust and scalable applications with ease.
Here is an example of a Spring Boot service class:
public interface UserService {
User getUserById(Long userId);
void saveUser(User user);
List<User> getAllUsers();
void deleteUserById(Long userId);
}
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long userId) {
Optional<User> optionalUser =
userRepository.findById(userId);
if (optionalUser.isPresent()) {
return optionalUser.get();
} else {
throw new UserNotFoundException("User
not found with ID: " + userId);
}
}
@Override
public void saveUser(@Valid @ModelAttribute("user") User
user, BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().build();
} else {
userService.saveUser(user);
return
ResponseEntity.created(URI.create("/users/" + user.getId())).build();
}
}
@Override
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
public void deleteUserById(Long userId) {
userRepository.deleteById(userId);
}
@Override
public User getUserByUsername(String username) {
Optional<User> optionalUser =
userRepository.findByUsername(username);
if (optionalUser.isPresent()) {
return optionalUser.get();
} else {
throw new UserNotFoundException("User
not found with username: " + username);
}
}
}
In this example, the UserService class is annotated with @Service, which indicates that it is a Spring-managed component.
The class contains several methods that provide functionality for managing users. The getUserById method retrieves a user from the database by its ID, the saveUser method saves a new user to the database, the getAllUsers method retrieves a list of all users, and the deleteUserById method deletes a user from the database by its ID.
Note that we've removed the @Autowired annotation on the UserRepository field and added it to the constructor instead. This is a recommended best practice, as it makes the dependencies of the class explicit and promotes better encapsulation.
This allows the service to interact with the database using JPA.
Finally, the class includes error handling for the case where a user is not found by its ID, throwing a UserNotFoundException in this case.
This is just a basic example of a Spring Boot service, but it demonstrates how services can be used to encapsulate business logic and interact with data sources using JPA.
Services are placed under the services directory of our application.
Our controller for the previous service will be:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{userId}")
public ResponseEntity<User> getUserById(@PathVariable Long
userId) {
User user = userService.getUserById(userId);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<Void> createUser(@RequestBody User user)
{
userService.saveUser(user);
return
ResponseEntity.created(URI.create("/users/" + user.getId())).build();
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users =
userService.getAllUsers();
return ResponseEntity.ok(users);
}
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUserById(@PathVariable Long
userId) {
userService.deleteUserById(userId);
return ResponseEntity.noContent().build();
}
@GetMapping
public ResponseEntity<User> getUserByUsername(@RequestParam
String username) {
User user =
userService.getUserByUsername(username);
return ResponseEntity.ok(user);
}
}
In this example, the UserController is annotated with @RestController, which indicates that it is a controller that returns data in response to requests. The @RequestMapping annotation specifies the base URL path for all endpoints in the controller.
The UserController has a dependency on the UserService, which is injected via the constructor using the @Autowired annotation.
The getUserById() method handles GET requests to retrieve a user by ID. It delegates to the UserService to retrieve the user and returns a ResponseEntity with the user and an HTTP status code of 200 (OK).
The createUser() method handles POST requests to create a new user. It delegates to the UserService to save the user and returns a ResponseEntity with a location header and an HTTP status code of 201 (Created).
The getAllUsers() method handles GET requests to retrieve all users. It delegates to the UserService to retrieve the users and returns a ResponseEntity with the users and an HTTP status code of 200 (OK).
The deleteUserById() method handles DELETE requests to delete a user by ID. It delegates to the UserService to delete the user and returns a ResponseEntity with an HTTP status code of 204 (No Content).
Note that this is just a simple example of a Spring Boot controller that uses a service. In a real application, you may need to handle more complex logic, validate input data, and return more detailed error messages.
Repository
In Spring Boot, repositories are used to provide a way to interact with persistent data stores, such as databases or caches. A repository is a Spring-managed component that provides a set of methods for querying, creating, updating, and deleting data.
Spring Boot provides a rich set of tools for creating and managing repositories, including support for JPA (Java Persistence API), MongoDB, Cassandra, and other data stores.
The key features and best practices of Spring Boot repositories include:
Repositories should define clear, well-defined interfaces that describe the data being managed and the operations that can be performed on that data. This allows for easy integration with other components and promotes code reuse.
Repositories should use appropriate query methods to interact with the data store. Query methods can be defined using the Spring Data JPA @Query annotation or by naming conventions that map method names to database queries.
Repositories should handle exceptions in a consistent and meaningful way. This typically involves catching exceptions and converting them to appropriate error responses, which can then be returned to the caller or logged for later analysis.
Repositories should be unit tested thoroughly to ensure they behave as expected. This typically involves mocking the data store and writing tests that cover a range of scenarios and edge cases.
Repositories can be integrated with other Spring Boot features, such as caching and transaction management, to implement more complex functionality. For example, a repository may use Spring Cache to cache frequently accessed data, or use Spring Transactions to ensure that data is consistently and reliably updated in the data store.
Here is an example of a Spring Boot repository interface:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
User saveUser(User user);
}
In this example, the UserRepository interface extends the Spring Data JpaRepository interface, which provides a set of common methods for querying and managing data.
The interface includes several query methods, such as findByUsername, which retrieves a user by their username, findByRole, which retrieves a list of users with a specific role, and findByLastNameIgnoreCase, which retrieves a list of users with a specific last name, ignoring case.
Spring Boot provides a rich set of tools for creating and managing repositories, which makes it easy to interact with data stores and implement complex functionality. By following best practices and leveraging the full range of Spring Boot features, you can build robust and scalable applications with ease.
Here's an example of a custom implementation of the UserRepository interface:
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersByAgeGreaterThan(int age) {
CriteriaBuilder cb =
entityManager.getCriteriaBuilder();
CriteriaQuery<User> query =
cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.select(root)
.where(cb.greaterThan(root.get("age"), age));
TypedQuery<User> typedQuery =
entityManager.createQuery(query);
return typedQuery.getResultList();
}
@Override
@Transactional
public User saveUser(User user) {
entityManager.persist(user);
return user;
}
}
In this example, we've created a class called UserRepositoryImpl that implements a custom interface called UserRepositoryCustom. The custom interface defines a method called findUsersByAgeGreaterThan() that returns a list of users whose age is greater than a given value.
The UserRepositoryImpl class is annotated with @Repository, which indicates that it is a Spring repository component. The class also has a field called entityManager, which is injected using the @PersistenceContext annotation. The entityManager is used to create and execute JPA queries.
The findUsersByAgeGreaterThan() method uses the CriteriaBuilder API to create a JPA query that selects all User entities whose age property is greater than the given age parameter. The query is executed using an instance of TypedQuery<User>, which returns a list of matching User entities.
By creating a custom implementation of the UserRepository interface, you can define additional methods that are not provided by the built-in JpaRepository interface. These custom methods can use any JPA query technique, such as JPQL, Criteria API, or Native SQL. Note that the custom repository interface must be named with the suffix Custom, and the custom implementation class must be named with the suffix Impl and implement the custom interface.
Dependencies
Dependencies in Springboot are handled in the pom.xml file.
Let’s add postgres:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
Configuration
To configure parts of our Spring Boot app we just need to edit the application.properties file.
Let’s add a datasource:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myusername
spring.datasource.password=mypassword
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
In a Spring Boot application that uses Hibernate as the persistence provider, you can use the generate-ddl and ddl-auto properties to generate and execute DDL scripts to create and update database tables.
The generate-ddl property controls whether Hibernate should generate DDL scripts based on the entity classes and other configuration settings. This property is set to false by default, which means that Hibernate will not generate DDL scripts automatically.
Here's an example of setting the generate-ddl property in the application.properties file:
# Enable DDL generation
spring.jpa.generate-ddl=true
When generate-ddl is set to true, Hibernate will generate DDL scripts based on the entity classes and other configuration settings. The scripts will be printed to the console, but they will not be executed automatically.
To execute the generated DDL scripts, you can set the ddl-auto property to one of several values:
none: Do not perform any automatic DDL operations.
validate: Validate the schema, but do not make any changes to the database.
update: Update the schema if necessary, but do not drop any tables or data.
create: Create the schema from scratch, dropping any existing tables and data.
create-drop: Create the schema from scratch, dropping any existing tables and data when the SessionFactory is closed.
Here's an example of setting the ddl-auto property in the application.properties file:
# Enable automatic DDL operations
spring.jpa.hibernate.ddl-auto=update
In this example, the ddl-auto property is set to update, which means that Hibernate will update the database schema if necessary, but it will not drop any tables or data.
It's important to be careful when using automatic DDL operations, as they can cause data loss or corruption if used incorrectly. It's recommended to test DDL scripts thoroughly in a development environment before applying them to a production database.
In a Spring Boot application that uses JPA as the persistence provider, you can force uppercase table names in the database by setting the hibernate.ejb.naming_strategy property to org.hibernate.cfg.ImprovedNamingStrategy and the hibernate.physical_naming_strategy property to org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl. Here's an example:
spring.jpa.properties.hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Injection
There are 2 types of injections in Spring:
- Setters: is the most common, it uses class methods to set properties of an object.
- Constructor: it enforces a contract with the application that was intended by the creator of the class.
Logging
To enable logging just go to the application.properties file and add the following line:
logging.level.root=INFO
logging.level.org.springframework.web=DEBUG
logging.file.name=myapp.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} -
%msg%n
logging.level.org.postgresql=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Entity
In Spring Boot, entities are classes that represent persistent data in a database. An entity typically corresponds to a table in the database, and the fields of the entity correspond to columns in the table. Entities are managed by a persistence framework, such as Hibernate or JPA, which provides an abstraction layer over the database.
Here are some key features and best practices when working with Spring Boot entities:
Entities should define clear, well-defined interfaces that describe their attributes and relationships with other entities. This makes it easier to understand how to use the entity and promotes code reuse.
Entities should define primary keys and relationships with other entities using annotations or XML mappings. This allows the persistence framework to automatically generate SQL statements and manage relationships between entities.
Entities should be designed to be immutable wherever possible. This promotes data consistency and can help prevent errors caused by concurrent updates.
Entities should handle validation and errors in a consistent and meaningful way. This typically involves using annotations to validate fields and throwing exceptions when errors occur.
Entities can be integrated with other Spring Boot features, such as caching and security, to implement more complex functionality. For example, an entity may use Spring Cache to cache frequently accessed data or use Spring Security to enforce access control policies.
Here is an example of a Spring Boot entity class:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", unique = true, nullable =
false)
private String username;
@Column(name = "password", nullable = false)
private String password;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Role> roles;
private String name;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
orphanRemoval = true)
private Address address;
// getters and setters
}
In this example, the User class is annotated with @Entity, which indicates that it is a persistent entity. The @Table annotation specifies the name of the database table that the entity corresponds to.
The @Id annotation specifies that the id field is the primary key for the entity, and the @GeneratedValue annotation specifies that the primary key value will be generated by the database.
The @Column annotation specifies the properties of the username and password fields, such as their names and whether they are nullable or unique.
The @OneToMany annotation specifies a one-to-many relationship between the User entity and the Role entity. The mappedBy attribute specifies the field in the Role entity that maps to the User entity, and the cascade and orphanRemoval attributes specify how changes to the User entity should be propagated to the Role entity.
This is just a basic example of a Spring Boot entity, but it demonstrates how entities can be used to represent persistent data and interact with a database using JPA.
Entities are usually placed under the models directory of the project.
JPA Annotations
@Entity: Indicates that a class is a JPA entity. JPA entities are objects that can be persisted to a database.
@Table: Specifies the name of the database table that corresponds to an entity. By default, JPA will use the name of the entity class as the table name. Example: @Table(name = "users")
@Id: Specifies the primary key of an entity. The @Id annotation can be applied to a field or to a getter method.
@GeneratedValue: Specifies the strategy used to generate primary key values. The @GeneratedValue annotation can be used in combination with @Id. Example: @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column: Specifies the properties of a column in a database table. The @Column annotation can be used to specify the name of the column, its data type, whether it is nullable or unique, and other properties. Example: @Column(name = "username", unique = true, nullable = false)
@OneToOne, @OneToMany, @ManyToOne, @ManyToMany: Specifies the type of relationship between two entities. These annotations can be used to specify the cardinality of the relationship and the name of the foreign key column(s).
@JoinColumn: Specifies the name of a foreign key column in a database table. This annotation can be used in combination with @OneToOne, @OneToMany, and @ManyToOne.
@Transient: Specifies that a field or property should not be persisted to a database. This annotation can be used to mark fields that are derived from other fields or that are not relevant for persistence.
@NamedQuery: Defines a named query that can be used to retrieve entities from a database. Named queries can be defined in the entity class or in a separate XML mapping file.
@MappedSuperclass: Specifies that a class is a superclass for a set of entities, and that its properties should be mapped to the database tables of its subclasses. This annotation can be used to define common properties and behavior for a group of related entities.
@Valid: is an annotation that is used to validate the properties of an object based on the constraints defined on those properties. It is commonly used in Spring MVC controllers to validate the data submitted by the user in a form or API request. The validation is performed by a validation framework such as Hibernate Validator, which supports various constraint annotations such as @NotNull, @Size, and @Pattern. When the @Valid annotation is used on a method parameter or field, Spring will automatically invoke the validation framework to check that the data is valid. If any validation errors are found, they are stored in a BindingResult object, which can be used to display error messages to the user.
@ModelAttribute: is an annotation that is used to bind a request parameter to a model attribute. It is commonly used in Spring MVC controllers to populate the fields of a domain object from the request parameters. When the @ModelAttribute annotation is used on a method parameter or field, Spring will automatically map the corresponding request parameter to the attribute based on the name of the parameter or field. If the parameter name and attribute name do not match, you can use the value attribute of the @ModelAttribute annotation to specify the name of the request parameter.
OneToOne
Suppose we have two entities, User and Address, where each user has one address. The User entity can be annotated with @OneToOne to represent the relationship between the two entities:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
orphanRemoval = true)
private Address address;
// getters and setters
}
In this example, the User entity has an address field annotated with @OneToOne. The mappedBy attribute is used to indicate that the Address entity owns the relationship and is responsible for maintaining the foreign key in the database. The cascade attribute is set to CascadeType.ALL to ensure that any changes made to the User or Address entities are cascaded to the other entity. The orphanRemoval attribute is set to true to ensure that any address that is no longer associated with a user is automatically deleted from the database.
OneToMany and ManyToOne
Suppose we have two entities, User and Order, where each user can have multiple orders. The User entity can be annotated with @OneToMany to represent the relationship between the two entities:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// getters and setters
}
In this example, the User entity has an orders field annotated with @OneToMany. The mappedBy attribute is used to indicate that the Order entity owns the relationship and is responsible for maintaining the foreign key in the database. The cascade attribute is set to CascadeType.ALL to ensure that any changes made to the User or Order entities are cascaded to the other entity. The orphanRemoval attribute is set to true to ensure that any order that is no longer associated with a user is automatically deleted from the database.
The Order entity can be annotated with @ManyToOne to represent the relationship between the two entities:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
// getters and setters
}
In this example, the Order entity has a user field annotated with @ManyToOne. The fetch attribute is set to FetchType.LAZY to ensure that the associated User entity is only loaded from the database when it is needed. The JoinColumn annotation is used to specify the name of the foreign key column in the orders table.
ManyToMany
Suppose we have two entities, Book and Author, where each book can be written by multiple authors and each author can write multiple books. To represent this many-to-many relationship, we can use the @ManyToMany annotation.
First, we need to define the Book entity:
@Entity
@Table(name = "books")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE
})
@JoinTable(
name = "book_author",
joinColumns = @JoinColumn(name =
"book_id"),
inverseJoinColumns = @JoinColumn(name =
"author_id")
)
private Set<Author> authors = new HashSet<>();
// constructors, getters, and setters
}
In this example, the Book entity has a authors field annotated with @ManyToMany. The cascade attribute is set to { CascadeType.PERSIST, CascadeType.MERGE } to ensure that any new authors added to a book are persisted in the database, and that any existing authors associated with the book are updated in the database. The @JoinTable annotation is used to specify the name of the join table that is used to store the many-to-many relationship. The joinColumns attribute specifies the name of the foreign key column in the join table that references the Book entity, and the inverseJoinColumns attribute specifies the name of the foreign key column in the join table that references the Author entity.
Next, we need to define the Author entity:
@Entity
@Table(name = "authors")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "authors")
private Set<Book> books = new HashSet<>();
// constructors, getters, and setters
}
In this example, the Author entity has a books field annotated with @ManyToMany. The mappedBy attribute is used to indicate that the Book entity owns the relationship and is responsible for maintaining the foreign key in the join table.
Finally, we can use these entities in our application to create, read, update, and delete books and authors:
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Book createBook(Book book) {
return bookRepository.save(book);
}
public Book addAuthorToBook(Long bookId, Author author) {
Book book =
bookRepository.findById(bookId).orElseThrow(() -> new BookNotFoundException("Book not found with ID:
" + bookId));
book.getAuthors().add(author);
return bookRepository.save(book);
}
// other methods to update, delete, and retrieve books
}
@Service
public class AuthorService {
@Autowired
private AuthorRepository authorRepository;
public List<Author> getAllAuthors() {
return authorRepository.findAll();
}
public Author createAuthor(Author author) {
return authorRepository.save(author);
}
public Author addBookToAuthor(Long authorId, Book book) {
Author author =
authorRepository.findById(authorId).orElseThrow(() -> new AuthorNotFoundException("Author not found
with ID: " + authorId));
author.getBooks().add(book);
return authorRepository.save(author);
}
}
JPQL
JPQL (Java Persistence Query Language) is a query language used to execute database queries in JPA (Java Persistence API). It is similar to SQL (Structured Query Language), but instead of working with tables and columns, JPQL works with entities and their properties.
Here's an example of how to use JPQL in Spring Boot:
Suppose we have an entity User with a name field, and we want to retrieve all users with a given name. We can use JPQL to create a query to retrieve these users:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findUsersByName(@Param("name") String
name);
}
In this example, the @Query annotation is used to specify the JPQL query. The query selects all entities of type User where the name property matches the given name parameter.
The :name syntax in the JPQL query refers to a named parameter that is passed in as an argument to the findUsersByName() method. The @Param annotation is used to bind the name parameter to the named parameter in the JPQL query.
We can then use this method in our application to retrieve all users with a given name:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getUsersByName(String name) {
return userRepository.findUsersByName(name);
}
// other methods to save, update, delete, and retrieve users
}
In this example, the getUsersByName() method calls the userRepository.findUsersByName() method to retrieve all users with the given name. The resulting list of users is returned to the caller.
JPQL supports many of the same features as SQL, including joins, aggregates, and subqueries. Here's an example of a more complex JPQL query that retrieves all users who have placed an order in the last 30 days:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT DISTINCT u FROM User u JOIN u.orders o WHERE
o.datePlaced >= :startDate")
List<User> findUsersWithOrdersSince(@Param("startDate")
LocalDate startDate);
}
In this example, the JPQL query uses a join to retrieve all orders placed in the last 30 days, and then selects the distinct users who placed those orders. The :startDate parameter is used to specify the start date of the 30-day period.
JPQL is a powerful tool for working with JPA entities and performing complex queries on database data. By using JPQL in Spring Boot, we can write queries that are optimized for our specific use cases, and that take advantage of the full power of the JPA framework.
Testing
Under the test folder you will find the unit test files used to test the application.
To unit test a Spring Boot controller like GreetingController, you can use JUnit and Spring's MockMvc library. However, if you don't want to use MockMvc, you can also unit test the controller method by creating an instance of the controller and invoking its methods directly.
Here's an example of how to unit test the sayHello method in GreetingController using JUnit and MockMvc:
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
public class GreetingControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testSayHelloWithName() throws Exception {
mockMvc.perform(get("/greeting/hello")
.param("name",
"John"))
.andExpect(status().isOk())
.andExpect(view().name("greeting"))
.andExpect(model().attribute("greeting", "Hello, John!"));
}
@Test
public void testSayHelloWithoutName() throws Exception {
mockMvc.perform(get("/greeting/hello"))
.andExpect(status().isOk())
.andExpect(view().name("greeting"))
.andExpect(model().attribute("greeting", "Hello, World!"));
}
}
In this example, the @RunWith annotation specifies that the test should be run with the SpringRunner class, which is the JUnit test runner for Spring Boot applications.
The @WebMvcTest annotation specifies that the test should only load the GreetingController and its dependencies. This helps keep the test suite focused and efficient.
The MockMvc class is injected using the @Autowired annotation. This class allows you to simulate HTTP requests to your controller and validate the responses.
The two test methods use the mockMvc instance to perform HTTP GET requests to the /greeting/hello endpoint with and without the name parameter, respectively. The andExpect methods are used to validate the HTTP response, including the HTTP status code, view name, and model attributes.
Here's an example of how to write a unit test for the updated GreetingController using JUnit and the Spring Boot Test framework:
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
public class GreetingControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testSayHelloWithName() throws Exception {
mockMvc.perform(get("/greeting/hello")
.param("name",
"John"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, John!"));
}
@Test
public void testSayHelloWithoutName() throws Exception {
mockMvc.perform(get("/greeting/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, World!"));
}
}
In this example, the test methods are similar to the previous ones, but the andExpect method for validating the response body is different. Since @RestController returns plain data rather than a view, we use the content().string() method to validate the response body directly.