1 Database
djmil edited this page 2023-07-20 12:33:27 +02:00

Spring Data and a database dependencies

This project was originally created using the Spring Initializr, which allowed us to automatically add dependencies to our project. However, now we must manually add dependencies to our project.

In build.gradle file

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
	// Add the two dependencies below
    implementation 'org.springframework.data:spring-data-jdbc'
    testImplementation 'com.h2database:h2'
}

The two dependencies we added are related, but different.

Spring Data JDBC

  implementation 'org.springframework.data:spring-data-jdbc'

Spring Data - has many implementations for a variety of relational and non-relational database technologies. Spring Data also has several abstractions on top of those technologies. These are commonly called an Object-Relational Mapping framework, or ORM.

Here we'll elect to use Spring Data JDBC.

Spring Data JDBC aims at being conceptually easy...This makes Spring Data JDBC a simple, limited, opinionated ORM.

The actual database

testImplementation 'com.h2database:h2'

Database management frameworks only work if they have a linked database. H2 is a "very fast, open source, JDBC API" SQL database implemented in Java. It works seamlessly with Spring Data JDBC.

  • Note testImplementation This tells Spring Boot to make the H2 database available only when running tests. Eventually we'll need a database outside of a testing context, but not yet.

Create the CashCardRepository

Create src/main/java/djmil/cashcard/CashCardRepository.java and have it extend CrudRepository.

package djmil.cashcard;

import org.springframework.data.repository.CrudRepository;

public interface CashCardRepository extends CrudRepository<CashCard, Long> {
}

We have to indicated which data object the CashCardRepository should manage. For our application, the "domain type" of this repository will be the CashCard. When we configure the repository as CrudRepository<CashCard, Long> we indicate that the CashCard's ID is Long. However, we still need to tell Spring Data which field is the ID.

Edit the CashCard class to configure the id as the @Id for the CashCardRepository.

Don't forget to add the new import.

package djmil.cashcard;

// Add this import
import org.springframework.data.annotation.Id;

public record CashCard(@Id Long id, Double amount) {
}

Inject the CashCardRepository into CashCardController

Although we've configured our CashCard and CashCardRepository classes, we haven't utilized the new CashCardRepository to manage our CashCard data. Let's do that now.

@RestController
@RequestMapping("/cashcards")
public class CashCardController {
   private CashCardRepository cashCardRepository;

   public CashCardController(CashCardRepository cashCardRepository) {
      this.cashCardRepository = cashCardRepository;
   }
   ...

The tests will now pass, despite no other changes to the codebase utilizing the new, required constructor CashCardController(CashCardRepository cashCardRepository)!

Spring's Auto Configuration is utilizing its dependency injection (DI) framework, specifically constructor injection, to supply CashCardController with the correct implementation of CashCardRepository at runtime.

Learn to read compiler error

Temporarily change the CashCardRepository to remove the implementation of CrudRepository

public interface CashCardRepository {
}

Compile the project and note the failure.

[~/exercises] $ ./gradlew build

## shouldNotReturnACashCardWithAnUnknownId()
java.lang.IllegalStateException: Failed to load ApplicationContext for 
...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'cashCardController' defined in file [/Users/oxbee/code/hqlxa/FamilyCashCard/build/classes/java/main/djmil/cashcard/CashCardController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'djmil.cashcard.CashCardRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'djmil.cashcard.CashCardRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

Clues such as NoSuchBeanDefinitionException, No qualifying bean, and expected at least 1 bean which qualifies as autowire candidate tell us that Spring is trying to find a properly configured class to provide during the dependency injection phase of Auto Configuration, but none qualify. We can satisfy this DI requirement by implementing the CrudRepository.

Use the CashCardRepository for data management

The CrudRepository interface provides many helpful methods, including findById(ID id). Update the CashCardController to utilize this method on the CashCardRepository and update the logic; be sure to import java.util.Optional;

import java.util.Optional;
...
   
@GetMapping("/{requestedId}")
public ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {
	Optional<CashCard> cashCardOptional = cashCardRepository.findById(requestedId);
	
	if (cashCardOptional.isPresent()) {
	    return ResponseEntity.ok(cashCardOptional.get());
    } else {
	    return ResponseEntity.notFound().build();
    }
}

We're calling CrudRepository.findById which returns an Optional. This smart object might or might not contain the CashCard for which we're searching. Learn more about Optional here.

Configure an in-memory Database

Spring Data and H2 can automatically create and populate the in-memory database we need for our test. The tests would expect the API to find and return a CashCard with id of 99. If you'd to run test not - you'd get a 500 INTERNAL_SERVER_ERROR, meaning that our application has no database to connect to. So let's create one!

schema.sql - create database/tables

Spring Data will automatically configure a database for tests if we provide src/test/resources/schema.sql

CREATE TABLE cash_card
(
   ID     BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
   AMOUNT NUMBER NOT NULL DEFAULT 0
);

A database schema is a "blueprint" for how data is stored in a database. Our database schema reflects the CashCard object that we understand, which contains an id and an amount.

Running tests at this moment will produce 404 NOT_FOUND, meaning that we have to populate freshly created DB with some test data.

data.sql - load data into DB

src/test/resources/data.sql

INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45);