diff --git a/.gitignore b/.gitignore index 7ed0d6b..e50227d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +# DB +.local-db + ### STS ### .apt_generated .classpath diff --git a/pom.xml b/pom.xml index 33f27dd..a2f8a7e 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,15 @@ spring-boot-configuration-processor true + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + org.springframework.boot spring-boot-devtools diff --git a/src/main/java/de/sample/schulung/spring/blog/domain/BlogPostService.java b/src/main/java/de/sample/schulung/spring/blog/domain/BlogPostService.java index b3b055f..7dfd920 100644 --- a/src/main/java/de/sample/schulung/spring/blog/domain/BlogPostService.java +++ b/src/main/java/de/sample/schulung/spring/blog/domain/BlogPostService.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -32,6 +33,7 @@ public Optional findById(UUID id) { } public void create(@Valid @NotNull BlogPost blogPost) { + blogPost.setTimestamp(LocalDateTime.now()); this.sink.create(blogPost); this.eventPublisher.publishEvent(new BlogPostCreatedEvent(blogPost)); } diff --git a/src/main/java/de/sample/schulung/spring/blog/domain/InMemoryBlogPostSink.java b/src/main/java/de/sample/schulung/spring/blog/domain/InMemoryBlogPostSink.java index 5c16961..fa8bf8f 100644 --- a/src/main/java/de/sample/schulung/spring/blog/domain/InMemoryBlogPostSink.java +++ b/src/main/java/de/sample/schulung/spring/blog/domain/InMemoryBlogPostSink.java @@ -1,6 +1,5 @@ package de.sample.schulung.spring.blog.domain; -import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -20,7 +19,6 @@ public long count() { public void create(BlogPost post) { final var id = UUID.randomUUID(); post.setId(id); - post.setTimestamp(LocalDateTime.now()); this.blogPosts.put(id, post); } diff --git a/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntity.java b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntity.java new file mode 100644 index 0000000..84a9d5a --- /dev/null +++ b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntity.java @@ -0,0 +1,37 @@ +package de.sample.schulung.spring.blog.persistence; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@Entity(name = "BlogPost") // Name der Entity (JPQL) +@Table(name = "BLOG_POSTS") +public class BlogPostEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + @Size(min = 3) + @NotNull + private String title; + @Size(min = 10) + @NotNull + private String content; + @Column(name = "TIME_STAMP") + private LocalDateTime timestamp; + + /* + @PrePersist + public void updateTimestamp() { + this.timestamp = LocalDateTime.now(); + } + */ + +} diff --git a/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntityMapper.java b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntityMapper.java new file mode 100644 index 0000000..80aba78 --- /dev/null +++ b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostEntityMapper.java @@ -0,0 +1,16 @@ +package de.sample.schulung.spring.blog.persistence; + +import de.sample.schulung.spring.blog.domain.BlogPost; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "spring") +public interface BlogPostEntityMapper { + + BlogPost map(BlogPostEntity source); + + BlogPostEntity map(BlogPost source); + + void copy(BlogPostEntity source, @MappingTarget BlogPost target); + +} diff --git a/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostRepository.java b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostRepository.java new file mode 100644 index 0000000..35580ea --- /dev/null +++ b/src/main/java/de/sample/schulung/spring/blog/persistence/BlogPostRepository.java @@ -0,0 +1,21 @@ +package de.sample.schulung.spring.blog.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.UUID; +import java.util.stream.Stream; + +@Repository +public interface BlogPostRepository + extends JpaRepository { + + // oder @Query("...") + Stream streamBlogPostEntitiesByTitleContainingIgnoreCaseOrderByTimestampDesc(String title); + + @Query("select p from BlogPost p where p.title = :title") + Stream streamIrgendwie(@Param("title") String title); + +} diff --git a/src/main/java/de/sample/schulung/spring/blog/persistence/JpaBlogPostSink.java b/src/main/java/de/sample/schulung/spring/blog/persistence/JpaBlogPostSink.java new file mode 100644 index 0000000..80a315d --- /dev/null +++ b/src/main/java/de/sample/schulung/spring/blog/persistence/JpaBlogPostSink.java @@ -0,0 +1,44 @@ +package de.sample.schulung.spring.blog.persistence; + +import de.sample.schulung.spring.blog.domain.BlogPost; +import de.sample.schulung.spring.blog.domain.BlogPostSink; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +@RequiredArgsConstructor +@Component +public class JpaBlogPostSink implements BlogPostSink { + + private final BlogPostRepository repo; + private final BlogPostEntityMapper mapper; + + @Override + public long count() { + return repo.count(); + } + + @Override + public void create(BlogPost post) { + final var entity = mapper.map(post); + final var savedEntity = repo.save(entity); + //post.setId(savedEntity.getId()); + mapper.copy(savedEntity, post); + } + + @Override + public Stream findAll() { + return repo.findAll() + .stream() + .map(mapper::map); + } + + @Override + public Optional findById(UUID id) { + return repo.findById(id) + .map(mapper::map); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b99f98..3e5e670 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,3 +10,19 @@ application: cors: allow: origins: ${CORS_ALLOW_ORIGINS:*} +spring: + datasource: + url: jdbc:h2:./.local-db/data + h2: + console: + enabled: true + path: /h2-console # http://localhost:9080/h2-console (leave username and password empty) + jpa: + generate-ddl: true + show-sql: true + open-in-view: false + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect diff --git a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiTests.java b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiTests.java index 39f0a8a..a774edb 100644 --- a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiTests.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; @@ -15,6 +16,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest +@AutoConfigureTestDatabase @AutoConfigureMockMvc @ActiveProfiles("no-initialization") class BlogPostApiTests { diff --git a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiWithMockedServiceTests.java b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiWithMockedServiceTests.java index 651cbbc..d5f9a28 100644 --- a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiWithMockedServiceTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostApiWithMockedServiceTests.java @@ -5,8 +5,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -21,7 +22,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest +@WebMvcTest +@ComponentScan(basePackageClasses = BlogPostApiWithMockedServiceTests.class) @AutoConfigureMockMvc class BlogPostApiWithMockedServiceTests { diff --git a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostControllerIntegrationTests.java b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostControllerIntegrationTests.java index eed06f4..848087b 100644 --- a/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostControllerIntegrationTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/boundary/BlogPostControllerIntegrationTests.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpStatus; @@ -9,6 +10,7 @@ // SO NICHT! @SpringBootTest +@AutoConfigureTestDatabase public class BlogPostControllerIntegrationTests { @Autowired diff --git a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostEventsTests.java b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostEventsTests.java index 389f5f9..9228b6f 100644 --- a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostEventsTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostEventsTests.java @@ -3,6 +3,7 @@ import jakarta.validation.ConstraintViolationException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.event.ApplicationEvents; @@ -12,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest +@AutoConfigureTestDatabase // TODO: wie ohne Datenbank? @RecordApplicationEvents @ActiveProfiles("test-domain") public class BlogPostEventsTests { diff --git a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostInitializationTests.java b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostInitializationTests.java index 2dd065a..ce0c4c1 100644 --- a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostInitializationTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostInitializationTests.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; @@ -13,6 +14,7 @@ * THIS WILL LEAD TO A SEPARATE CONTEXT !!! */ @SpringBootTest +@AutoConfigureTestDatabase // komplette Anwendung -> Test der Konfiguration in application.yml @TestPropertySource(properties = { "application.initialization.enabled=true", diff --git a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostServiceTests.java b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostServiceTests.java index 42059cd..f12c5b5 100644 --- a/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostServiceTests.java +++ b/src/test/java/de/sample/schulung/spring/blog/domain/BlogPostServiceTests.java @@ -2,11 +2,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest +@AutoConfigureTestDatabase // komplette Anwendung -> Test der Konfiguration in application.yml public class BlogPostServiceTests {