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);