database
parent
997c67f20a
commit
d35729d123
22
.obsidian/workspace.json
vendored
22
.obsidian/workspace.json
vendored
@ -25,26 +25,26 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "IntegrationTests.md",
|
||||
"file": "Database.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ec5879550528c04c",
|
||||
"id": "af9b076d6f1ac377",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "GET.md",
|
||||
"file": "Database.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentTab": 2
|
||||
"currentTab": 1
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
@ -110,7 +110,7 @@
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "GET.md",
|
||||
"file": "Database.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
@ -127,7 +127,7 @@
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "GET.md",
|
||||
"file": "Database.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
}
|
||||
@ -150,7 +150,7 @@
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "GET.md"
|
||||
"file": "Database.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,12 +171,14 @@
|
||||
"command-palette:Open command palette": false
|
||||
}
|
||||
},
|
||||
"active": "ec5879550528c04c",
|
||||
"active": "7e5ec70badaa86f3",
|
||||
"lastOpenFiles": [
|
||||
"IntegrationTests.md",
|
||||
"Home.md",
|
||||
"GET.md",
|
||||
"UnitTests.md",
|
||||
"Database.md",
|
||||
"Home.md",
|
||||
"assets/Pasted image 20230719152007.png",
|
||||
"GET.md",
|
||||
"Pasted image 20230719102301.png",
|
||||
"assets/Pasted image 20230719102322.png",
|
||||
"assets",
|
||||
|
164
Database.md
Normal file
164
Database.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Spring Data and a database dependencies
|
||||
|
||||
This project was originally created using the [Spring Initializr](https://start.spring.io/), which allowed us to automatically add dependencies to our project. However, now we must manually add dependencies to our project.
|
||||
|
||||
In `build.gradle` file
|
||||
|
||||
```groovy
|
||||
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
|
||||
|
||||
```groovy
|
||||
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](https://spring.io/projects/spring-data-jdbc).
|
||||
|
||||
> Spring Data JDBC aims at being conceptually easy...This makes Spring Data JDBC a simple, limited, opinionated ORM.
|
||||
|
||||
## The actual database
|
||||
|
||||
```groovy
|
||||
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`.
|
||||
|
||||
```java
|
||||
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.
|
||||
|
||||
```java
|
||||
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.
|
||||
|
||||
```java
|
||||
@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`
|
||||
```java
|
||||
public interface CashCardRepository {
|
||||
}
|
||||
```
|
||||
|
||||
Compile the project and note the failure.
|
||||
|
||||
```shell
|
||||
[~/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](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html#findById-ID-), 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;`
|
||||
|
||||
```java
|
||||
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](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html).
|
||||
|
||||
# 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`
|
||||
|
||||
```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`
|
||||
|
||||
```sql
|
||||
INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45);
|
||||
```
|
||||
|
65
Home.md
65
Home.md
@ -17,28 +17,29 @@ Essentially, this will generate a default minimalistic jet functional SpringBoot
|
||||
|
||||
# TDD
|
||||
|
||||
Software development teams love to move fast. So how do you go fast forever? By continuously improving and simplifying your code – this is called **refactoring**. Refactoring is the act of altering the implementation of a software system without altering its inputs, outputs, or behavior. One of the only ways you can safely refactor is when you have a trustworthy test suite. Which brings us to the **TDD Cycle**:
|
||||
|
||||
1. **Red:** Write a failing test for the desired functionality.
|
||||
2. **Green:** Implement the simplest thing that can work to make the test pass.
|
||||
3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior - to _refactor._
|
||||
4. Repeat!
|
||||
|
||||
Different tests can be written at different levels of the system. At each level, there is a balance between the speed of execution, the “cost” to maintain the test, and the confidence it brings to system correctness. This hierarchy is often represented as a “testing pyramid”.
|
||||
|
||||
![Testing pyramid](https://blog.missiondata.com/wp-content/uploads/MD_TestingPyramid2x-1560x1045.png "Testing pyramid")
|
||||
|
||||
## Unit Tests
|
||||
|
||||
[[UnitTests]] exercises a small “unit” of the system that is isolated from the rest of the system. They should be simple and speedy. You want a high ratio of Unit Tests in your testing pyramid as they’re key to designing highly cohesive, loosely coupled software.
|
||||
|
||||
## Integration Tests
|
||||
|
||||
[[IntegrationTests]] exercise a subset of the system and may exercise groups of units in one test. They are more complicated to write and maintain, and run slower than unit tests.
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
An End-to-End Test exercises the system using the same interface that a user would, such as a web browser. While extremely thorough, End-to-End Tests can be very slow and fragile because they use simulated user interactions in potentially complicated UIs. Implement the smallest number of these tests.
|
||||
|
||||
## TDD Cycle
|
||||
|
||||
Software development teams love to move fast. So how do you go fast forever? By continuously improving and simplifying your code – this is called **refactoring**. One of the only ways you can safely refactor is when you have a trustworthy test suite. Thus, the best time to refactor the code you're currently focusing on is during the TDD cycle. This is called the Red, Green, Refactor development loop:
|
||||
|
||||
1. **Red:** Write a failing test for the desired functionality.
|
||||
2. **Green:** Implement the simplest thing that can work to make the test pass.
|
||||
3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior—to _refactor._
|
||||
4. Repeat!
|
||||
|
||||
# RESTful API
|
||||
|
||||
In a RESTful system, data objects are called Resource Representations. The purpose of a RESTful API is to manage the state of these Resources. The chart below shows details about RESTful CRUD operations of an application.
|
||||
@ -56,7 +57,7 @@ Another common concept associated with REST is the Hypertext Transfer Protocol.
|
||||
|
||||
One of the main things Spring does is to configure and instantiate objects. These objects are called *Spring Beans*, and are usually created by Spring (as opposed to using the Java `new` keyword). You can direct Spring to create Beans in several ways.
|
||||
|
||||
> We will annotate a class with a Spring Annotation, which directs Spring to create an instance of the class during Spring’s *Component Scan* phase. This happens at application startup. The Bean is stored in Spring’s `IoC Container`. From here, the bean can be injected into any code that requests it.
|
||||
> We will annotate a class with a `@RestController` Spring Annotation, which directs Spring to create an instance of the class during Spring’s *Component Scan* phase. This happens at application startup. The Bean is stored in Spring’s `IoC Container`. From here, the bean can be injected into any code that requests it.
|
||||
|
||||
![[Pasted image 20230719102322.png]]
|
||||
|
||||
@ -95,3 +96,47 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
# Database
|
||||
|
||||
The [**Separation of Concerns**](https://en.wikipedia.org/wiki/Separation_of_concerns) principle states that well-designed software should be modular, with each module having distinct and separate concerns from any other module.
|
||||
|
||||
Up until now, our codebase only returns a hard-coded response from the Controller. This setup violates the Separation of Concerns principle by mixing the concerns of a Controller, which is an abstraction of a web interface, with the concerns of reading and writing data to a data store, such as a database. In order to solve this, we’ll use a common software architecture pattern to enforce data management separation via the **[Repository](https://www.baeldung.com/java-dao-vs-repository)** pattern.
|
||||
|
||||
A common architectural framework that divides these layers, typically by function or value, such as business, data, and presentation layers, is called **Layered Architecture**. Here we can think of our Repository and Controller as two layers in a Layered Architecture. The Controller is in a layer near the Client (as it receives and responds to web requests) while the Repository is in a layer near the data store (as it reads from and writes to the data store). There may be intermediate layers as well, as dictated by business needs.
|
||||
|
||||
![[Pasted image 20230719152007.png]]
|
||||
|
||||
The Repository is the interface between the application and the database, and provides a **common abstraction** for any database, making it easier to switch to a different database when needed.
|
||||
|
||||
[Spring Data](https://spring.io/projects/spring-data) works with Spring Boot to make database integration simple.
|
||||
|
||||
## Choosing a Database
|
||||
|
||||
For our database selection, we’ll use an **embedded, in-memory** database. “Embedded” simply means that it’s a Java library, so it can be added to the project just like any other dependency. “In-memory” means that it stores data in memory only, as opposed to persisting data permanent, durable storage.
|
||||
|
||||
The specific in-memory database we’ll use is [H2](https://www.h2database.com/html/main.html). Fortunately, H2 is highly compatible with other relational databases, so dev-prod parity *(application might behave differently when running the in-memory database than when running in production)* won’t be a big issue. We’ll use H2 for **convenience for local development**, but want to recognize the tradeoffs.
|
||||
|
||||
## Auto Configuration
|
||||
|
||||
Simply by adding [[Database#Spring Data and a database dependencies]] we are getting full database functionality. This wonderfully showcases one of the most powerful features of Spring Boot: **Auto Configuration**. Without Spring Boot, we’d have to configure Spring Data to speak to H2. However, because we’ve included the Spring Data dependency (and a specific data provider, H2), Spring Boot will automatically configure your application to communicate with H2.
|
||||
|
||||
## Spring Data’s CrudRepository
|
||||
|
||||
For our Repository selection, we’ll use a specific type of Repository: Spring Data’s `CrudRepository`. At first glance, it’s slightly magical, but let’s unpack that magic.
|
||||
|
||||
The following is a complete implementation of all CRUD operations by extending `CrudRepository`:
|
||||
|
||||
```java
|
||||
public interface CashCardRepository extends CrudRepository<CashCard, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
With just the above code, a caller can call any number of predefined `CrudRepository` methods, such as `findById`:
|
||||
|
||||
```java
|
||||
cashCard = cashCardRepository.findById(99);
|
||||
```
|
||||
|
||||
You might immediately wonder: where is the implementation of the `CashCardRepository.findById()` method? `CrudRepository` and everything it inherits from is an Interface with no actual code! Well, based on the specific Spring Data framework used, which for us will be Spring Data JDBC, Spring Data takes care of this implementation for us during the IoC container startup time. The Spring runtime will then expose the repository as yet another bean that you can reference wherever needed in your application.
|
||||
|
||||
As we’ve learned, there are typically trade-offs. For example the `CrudRepository` generates SQL statements to read and write your data, which is useful for many cases, but sometimes you need to write your own custom SQL statements for specific use cases. But for now, we’re happy to take advantage of its convenient, out-of-the-box methods,
|
@ -94,3 +94,28 @@ assertThat(id).isNotNull();
|
||||
```
|
||||
|
||||
We expect that when we request a Cash Card with `id` of `99` a JSON object will be returned with _something_ in the `id` field. For now assert that the `id` is not `null`.
|
||||
|
||||
# Complex test debugging
|
||||
|
||||
To make tests output more verbose, add this section to the `build.gradle` file:
|
||||
|
||||
```yaml
|
||||
// This section causes useful test output to go to the terminal.
|
||||
test {
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed" //, "standardOut", "standardError"
|
||||
|
||||
showExceptions true
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
|
||||
// Change to `true` for more verbose test output
|
||||
showStandardStreams = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Additional resources
|
||||
|
||||
- [Integration Testing in Spring](https://www.baeldung.com/integration-testing-in-spring) written by baeldung.
|
BIN
assets/Pasted image 20230719152007.png
Normal file
BIN
assets/Pasted image 20230719152007.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Loading…
Reference in New Issue
Block a user