-
Notifications
You must be signed in to change notification settings - Fork 6
Workshop #2: Connecting to the database
Workshop #2 is about revising the Spring basics from previous workshop and learning the rest of core functionalities that will help understand how to design and develop applications based on this framework. The second part of the workshop is related to providing persistence support via connecting to the database using Spring Data JPA. Therefore, you can find here topics such as:
- Spring Boot basics
- Spring Data JPA
- H2 database
- CRUD
You can find the source code that represents the status of the project after this workshop - here
_If you are already familiar with the Dependency Injection and Inversion of Control concept and how it is implemented in Spring - you can skip this part 😉
Based on the official site, Spring Boot is a framework that allows you to easily create stand-alone, production-grade Spring based app that you can "just run" without any additional code setup. Spring Boot provides a default configuration for different areas such as database management, REST API exposure, observability, etc. via so-called starters that can be added to our project via project dependencies. Therefore it makes web applications development really fast.
Based on the definition that can be found on the official documentation site:
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.
In other words, Spring provides us a container (called Spring IoC container) where we can define our components (like controllers, repositories, services, etc.) and dependencies between them without worrying about object instantiation and implementing connection structure - Spring does it for us.
Dependency injection is a design pattern in which a component receives other components that it depends on - it does not create them itself but only defines the proper dependencies.
Example without DI:
public class PaymentService {
private final PaymentProvider paymentProvider = new CreditCardPaymentProvider();
public PaymentService() {
}
}
Example with DI:
public class PaymentService {
private final PaymentProvider paymentProvider;
public PaymentService(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
}
Inversion of Control is the design pattern where some external sources (e.g. Spring container, framework, service) have control of creating and injecting object instances instead of us. Therefore, we can say that the control flow of the program is inverted.
How IoC works in Spring:
@Component
public class PaymentService {
// ...
}
@Configuration
public class PaymentModule {
@Bean
public PaymentService paymentService() {
return new PaymentService();
}
}
We know what are core principles of how Spring framework works now - so let's explore other core functionalities that are used mostly in every Spring application.
Spring offers several ways how we can inject dependencies between our components. However, the most preferred option is to use injecting via constructor because it provides the most flexibility and makes our domain components framework-independent.
Injecting via constructor
@Component
public class PaymentService {
private final PaymentProvider paymentProvider;
public PaymentService(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
}
Injecting via field
@Component
public class PaymentService {
@Autowired
private PaymentProvider paymentProvider;
}
Injecting via setter
@Component
public class PaymentService {
private PaymentProvider paymentProvider;
@Autowired
public void setPaymentProvider(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
}
Here you can find some useful annotations that are commonly used in Spring Boot application that exposes REST API
@Component
This annotation is used to mark class as a Spring component. In other words, we are telling Spring that it should create object of this class during application startup and put it in Spring IoC container as a bean.
Usage:
@Component
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
}
@Service
This annotation has the same functionality as the @Component
- Spring create bean of a class annotated with it during application startup. The only difference is the fact that @Service
annotation is used to indicate that our class represents a service.
Usage:
@Service
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
}
@Configuration
& @Bean
These annotations are used for bean creation purposes (reminder: bean is an object of some type created and injected to Spring IoC container during application startup).
The @Configuration
class is used to annotate a class that consists of beans, so basically it tells Spring to scan such a class and invoke methods annotated with @Bean
to create instances of the objects that will be later inject into IoC container then.
Usage:
@Configuration
public class ItemModule {
@Bean
public ItemService itemService(ItemRepository itemRepository) {
return new ItemService(itemRepository);
}
}
@RestController
This annotation is used for defining REST endpoints that will be exposed by our application. Additionally, Spring IoC container will automatically create a bean of such annotated class, so we don't need to explicitly create an instance of a controller - it will be done automatically.
Usage:
@RestController
@RequestMapping("/items")
public class ItemsController {
private final ItemService;
public ItemsController(ItemService itemService) {
this.itemService = itemService;
}
@GetMapping
public ResponseEntity<List<ItemResponse>> list() {
return ResponseEntity.ok(itemService.list());
}
}
@RequestMapping
This annotation is used to define a path for the controller (so for all endpoints defined in it). Therefore, we can easily separate the common part of all endpoints defined in a controller.
Usage:
@RestController
@RequestMapping("/items")
public class ItemsController {
private final ItemService;
public ItemsController(ItemService itemService) {
this.itemService = itemService;
}
// this method defines `GET /items` endpoint
@GetMapping
public ResponseEntity<List<ItemResponse>> list() {
return ResponseEntity.ok(itemService.list());
}
// this method defines `GET /items/{id}` endpoint
@GetMapping("/{id}")
public ResponseEntity<ItemResponse> get(@PathVariable String id) {
return ResponseEntity.ok(itemService.list());
}
}
@GetMapping
, @PutMapping
, @PostMapping
, DeleteMapping
These annotations are used to define the HTTP method for defined requests in the controller.
Usage:
@RestController
@RequestMapping("/items")
public class ItemsController {
private final ItemService;
public ItemsController(ItemService itemService) {
this.itemService = itemService;
}
// this method defines `GET /items` endpoint
@GetMapping
public ResponseEntity<List<ItemResponse>> list() {
return ResponseEntity.ok(itemService.list());
}
// this method defines `POST /items` endpoint
@PostMapping
public ResponseEntity<ItemResponse> create(@ResponseBody CreateItemRequest createRequest) {
var createdItem = itemService.create(createRequest);
return new ResponseEntity<>(createdItem, HttpStatus.CREATED);
}
}
Spring profiles provide a way to segregate parts of your application configuration and make it available in certain environments. We can also configure our Spring components to be created only when the certain profile is active (via @Profile
annotation).
Usage:
@Configuration
public class PaymentModule {
@Profile("local")
public PaymentProvider stubPaymentProvider() {
return new StubPaymentProvider();
}
@Profile("!local")
public PaymentProvider defaultPaymentProvider() {
return new CreditCardPaymentProvider();
}
}
How to enable Spring profiles:
You can enable specific Spring profiles by the following ways:
- In application properties:
spring: profiles: active: local
- Via system variable:
-Dspring.profiles.active=local
- Via environment variable:
SPRING_PROFILES_ACTIVE=local
Every Spring Boot application can be configured via so-called application properties. They are helpful if we want to provide e.g. some credentials or application-related configs like thread pool size, etc. Application properties also support spring profiles - we can have separate config files for every profile.
Structure
Application properties can be stored either in PROPERTIES
or YAML
format:
Sample application.properties
config:
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
Sample application.yml
config:
spring:
datasource:
url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
username: sa
password: sa
Location
Application properties are located under resources
folder:
Profiles
We can create separate application properties files for every profile we have in our application. Spring identifies config file by its name, so we only need to create a config file with the proper name: application-<profile_name>.yml
/application-<profile_name>.properties
.
Then we will have list of config files in our resources
folder:
src
∟ main
∟ java
∟ resources
∟ application.yml
∟ application-local.yml
∟ application-dev.yml
∟ application-prod.yml
ORM stands for object-relation mapping and it is a technique for converting data between type systems (here: database system) using object-oriented programming languages. This creates, in effect, a "virtual object database" that can be used from within the programming language.
Hibernate is ORM framework for Java.
Hibernate is an JPA (Java Persistence API) specific implementation whereas Spring Data JPA is just a data access abstraction. Therefore Spring Data JPA can use Hibernate under the hood but it can also use another JPA provider.
Entity is a Java object representing data that can be persisted to the databases. Entity as a class represents a table in a database and each instance of the class represents a row in a table.
Sample @Entity
class:
@Entity
@Table(name = "items")
public class Item {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "description")
private String description;
}
There are also some useful annotation that can help us define our model:
@Column
@Column
annotation is used to provide additional details related to the column (e.g. column name, size, etc.).
Usage:
@Column(name="name", length=50, unique=true)
private String name;
@Id
@Id
annotation is used to indicate that the given field represents the primary key in a table in the database. This annotation is used together with @GeneratedValue
which describes the way of generating new IDs.
@GeneratedValue
@GeneratedValue
is used to define the way of creating new identifiers. There are available four of them:
AUTO
TABLE
SEQUENCE
IDENTITY
Usage:
private static final String SEQUENCE = "items_seq";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long id;
@Enumerated
@Enumerated
annotation is used to configure how we want to store our enum values. Enums are stored by ordinal by default (if we do not provide any explicit configuration) - however, we can specify by this annotation to store them by their name.
Usage:
@Enumerated(EnumType.STRING)
private UnitType unitType;
A relationship between tables in a database is a connection between them where rows in one table can relate to the rows in the other one (there is an association between them).
Example:
Types of relationships
One to one:
One to many / Many to one:
Many to many:
Spring Data JPA provides a repository abstraction in form of a Repository
interface. It takes the domain class to manage as well as the id type of the domain class as type arguments. This interface is the core one and it is used mostly as a marker interface to capture the types to work with (entity type and ID type). There are plenty of other interfaces that extends the core Repository
one and they provide more sophisticated functionalities related to data management.
What's more, we don't need to worry about its implementation when we define repositories for entities in our application. We just need to define the interface that extends the Repository
one (or any more sophisticated one) and Spring Data JPA automatically generates the proper implementation during runtime.
CrudRepository
The CrudRepository
provides sophisticated CRUD functionality for the entity class that is being managed. The following list consists of some of most common methods that this interface provides:
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
T findOne(ID primaryKey);
Iterable<T> findAll();
Long count();
void delete(T entity);
boolean exists(ID primaryKey);
// ...
}
-
<S extends T> S save(S entity)
- saves given entity -
T findOne(ID primaryKey)
- returns the entity identified by the given id\ -
Iterable<T> findAll()
- returns all entities -
Long count()
- returns the number of entities -
void delete(T entity)
- deletes the given entity -
boolean exists(ID primaryKey)
- indicates whether an entity with the given id exists
We need to add the following dependencies in shop-service build.gradle
to use Spring Data JPA and embedded H2 database for local development purposes. We will also use Lombok to avoid unnecessary java boilerplate.
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.5'
implementation 'com.h2database:h2:2.1.214'
compileOnly 'org.projectlombok:lombok:1.18.24'
}
In the next step, we need to configure our data source. We will use in-memory H2 database for local development purposes. To configure it properly, add the following lines in application.yml
file located under resources
directory (if does not exist - create a new file or rename the existing application.properties
to application.yml
).
spring:
datasource:
url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
username: sa
password: sa
Let's refactor our existing Item
class to make it from record to JPA entity. We will use Lombok to generate getters and setters (via @Getter
and @Setter
annotations.
@Entity
@Getter
@Setter
@Table(name = "ITEMS")
public class Item {
private static final String SEQUENCE = "ITEMS_SEQ";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, initialValue = 1)
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "UNIT_PRICE")
private BigDecimal unitPrice;
@Column(name = "UNIT_TYPE")
@Enumerated(EnumType.STRING)
private UnitType unitType;
}
Secondly, create ItemRepository
interface that extends from JpaRepository
to have some useful methods provided like e.g. listing all entities.
public interface ItemRepository extends JpaRepository<Item, Long> {
}
In the last step, let's align our ItemService
and ItemModule
to use created ItemRepository
component.
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
public List<Item> list() {
return itemRepository.findAll();
}
public Item create(Item item) {
return itemRepository.save(item);
}
public void deleteById(Long id) {
itemRepository.deleteById(id);
}
}
@Configuration
public class ItemModule {
@Bean
public ItemService itemService(ItemRepository itemRepository) {
return new ItemService(itemRepository);
}
}
Now, we need to create analogical components for Basket
domain.
@Entity
@Getter
@Setter
@Builder
@Table(name = "BASKETS")
@NoArgsConstructor
@AllArgsConstructor
public class Basket {
private static final String SEQUENCE = "BASKETS_SEQ";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, initialValue = 1)
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany(mappedBy = "basket")
private Set<BasketItem> basketItems;
}
As you can notice, we defined one-to-many relationship with basket items (one basket can contain many basket items) via @OneToMany
annotation. We also used the mappedBy
property to tell Hibernate which variable we are using to represent the parent class in our child class (as our relationship will be bidirectional).
The BasketItem
entity:
@Entity
@Getter
@Setter
@Builder
@Table(name = "BASKET_ITEMS")
@NoArgsConstructor
@AllArgsConstructor
public class BasketItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Item item;
@ManyToOne
private Basket basket;
}
Let's also see how we can implement the BasketStorage
to manage both baskets and basket items:
public class BasketStorage {
private final BasketRepository basketRepository;
private final BasketItemRepository basketItemRepository;
private final ItemService itemService;
public BasketStorage(BasketRepository basketRepository, BasketItemRepository basketItemRepository, ItemService itemService) {
this.basketRepository = basketRepository;
this.basketItemRepository = basketItemRepository;
this.itemService = itemService;
}
public Basket getById(Long id) {
return basketRepository.findById(id)
.orElseThrow(() -> new BasketNotFoundException(id));
}
public Basket createBasket(BasketCreateRequest createRequest) {
var basket = Basket.builder()
.name(createRequest.name())
.build();
return basketRepository.save(basket);
}
public BasketItem createBasketItem(Long basketId, BasketItemCreateRequest createRequest) {
var item = itemService.getById(createRequest.itemId());
var basket = getById(basketId);
var basketItem = BasketItem.builder()
.item(item)
.basket(basket)
.build();
return basketItemRepository.save(basketItem);
}
}
Other classes created in this module:
BasketItemRepository
BasketRepository
BasketModule
BasketNotFoundException
All of them can be found here [TBD: link]
Now we have both domain ready to use: basket and item. Let's create some endpoints that will provide us a possibility to e.g. create a new basket, add some items or list them:
@RestController
@RequestMapping("/baskets")
public class BasketController {
private final BasketStorage basketStorage;
public BasketController(BasketStorage basketStorage) {
this.basketStorage = basketStorage;
}
@GetMapping("/{id}")
public ResponseEntity<Basket> get(@PathVariable Long id) {
var basket = basketStorage.getById(id);
return ResponseEntity.ok(basket);
}
@PostMapping
public ResponseEntity<Basket> createItem(@RequestBody BasketCreateRequest createRequest) {
var createdBasket = basketStorage.createBasket(createRequest);
return new ResponseEntity<>(createdBasket, HttpStatus.CREATED);
}
@GetMapping("/{id}/items")
public ResponseEntity<Collection<BasketItem>> listItems(@PathVariable Long id) {
var items = basketStorage.getById(id).getBasketItems();
return ResponseEntity.ok(items);
}
@PostMapping("/{id}/items")
public ResponseEntity<BasketItem> createItem(@PathVariable(name = "id") Long basketId,
@RequestBody BasketItemCreateRequest createRequest) {
var createdItem = basketStorage.createBasketItem(basketId, createRequest);
return new ResponseEntity<>(createdItem, HttpStatus.CREATED);
}
}
You can see additional classes: BasketCreateRequest
and BasketItemCreateRequest
. They are so-called DTO classes that define here the data which we need to provide to call the proper endpoints (here: define request body for POST requests). Tip: java records are the best fit to define DTOs :)
public record BasketCreateRequest(String name) {
}
public record BasketItemCreateRequest(Long itemId) {
}
This part will available after workshops 📽