Skip to content

Commit

Permalink
Fully integrated JPA
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisgleissner committed Jun 14, 2024
1 parent 9c20c85 commit 6d69a21
Show file tree
Hide file tree
Showing 21 changed files with 514 additions and 357 deletions.
18 changes: 18 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'io.github.hakky54:logcaptor:2.9.2'
testImplementation 'io.github.oshai:kotlin-logging-jvm:6.0.9'
testImplementation 'org.apache.commons:commons-compress:1.26.2'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.junit.jupiter:junit-jupiter'
Expand All @@ -40,6 +41,23 @@ test {
jvmArgs '-Xshare:off'
}

tasks.withType(Test) {
ext.failedTests = []
afterTest { descriptor, result ->
if (result.resultType == TestResult.ResultType.FAILURE) {
failedTests << ["${descriptor.className}::${descriptor.name}"]
}
}
afterSuite { suite, result ->
if (!suite.parent) {
if (!failedTests.empty) {
logger.lifecycle("Failed tests:")
failedTests.each { failedTest -> logger.lifecycle("${failedTest}") }
}
}
}
}

jacocoTestReport {
reports {
xml.required = true
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
restart: unless-stopped
shm_size: 128mb
environment:
POSTGRES_DB: loom-webflux
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/uk/gleissner/loomwebflux/LoomWebfluxApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import uk.gleissner.loomwebflux.config.AppProperties;

@SpringBootApplication
@ConfigurationPropertiesScan(basePackageClasses = AppProperties.class)
@EnableScheduling
public class LoomWebfluxApp {
static ConfigurableApplicationContext ctx;

public static void main(String[] args) {
SpringApplication.run(LoomWebfluxApp.class, args);
ctx = SpringApplication.run(LoomWebfluxApp.class, args);
}
}
28 changes: 17 additions & 11 deletions src/main/java/uk/gleissner/loomwebflux/movie/MovieController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package uk.gleissner.loomwebflux.movie;

import jakarta.transaction.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -13,24 +14,25 @@
import reactor.core.publisher.Mono;
import uk.gleissner.loomwebflux.controller.LoomWebFluxController;
import uk.gleissner.loomwebflux.movie.domain.Movie;
import uk.gleissner.loomwebflux.movie.repo.MovieRepo;
import uk.gleissner.loomwebflux.movie.repo.AppPropertiesAwareMovieRepo;

import java.util.List;
import java.util.Set;
import java.util.UUID;

import static reactor.core.scheduler.Schedulers.boundedElastic;
import static uk.gleissner.loomwebflux.Approaches.LOOM_NETTY;
import static uk.gleissner.loomwebflux.Approaches.LOOM_TOMCAT;
import static uk.gleissner.loomwebflux.Approaches.PLATFORM_TOMCAT;
import static uk.gleissner.loomwebflux.Approaches.WEBFLUX_NETTY;

@RestController
@Transactional
public class MovieController extends LoomWebFluxController {

private static final String API_PATH = "/movies";
private final MovieRepo movieRepo;
private final AppPropertiesAwareMovieRepo movieRepo;

MovieController(WebClient webClient, MovieRepo movieRepo) {
MovieController(WebClient webClient, AppPropertiesAwareMovieRepo movieRepo) {
super(webClient);
this.movieRepo = movieRepo;
}
Expand All @@ -42,7 +44,7 @@ public Set<Movie> findMoviesByDirectorLastName(@RequestParam String directorLast
@RequestParam Long delayInMillis) throws InterruptedException {
log("findMoviesByDirectorLastName");
waitOrFetchEpochMillis(delayCallDepth, delayInMillis);
return movieRepo.findMoviesByDirector(directorLastName);
return movieRepo.findByDirectorName(directorLastName);
}

@GetMapping(WEBFLUX_NETTY + API_PATH)
Expand All @@ -52,7 +54,7 @@ public Flux<Movie> findMoviesByDirectorLastNameReactive(@RequestParam String dir
@RequestParam Long delayInMillis) {
log("findMoviesByDirectorLastNameReactive");
return waitOrFetchEpochMillisReactive(delayCallDepth, delayInMillis)
.thenMany(Flux.defer(() -> Flux.fromIterable(movieRepo.findMoviesByDirector(directorLastName))));
.thenMany(Flux.defer(() -> Flux.fromIterable(movieRepo.findByDirectorName(directorLastName))));
}

@PostMapping({PLATFORM_TOMCAT + API_PATH, LOOM_TOMCAT + API_PATH, LOOM_NETTY + API_PATH})
Expand All @@ -61,7 +63,7 @@ public List<Movie> saveMovies(@RequestBody List<Movie> movies,
@RequestParam Long delayInMillis) throws InterruptedException {
log("saveMovies");
waitOrFetchEpochMillis(delayCallDepth, delayInMillis);
return movieRepo.saveAll(movies);
return movies.stream().map(movieRepo::save).toList();
}

@PostMapping(WEBFLUX_NETTY + API_PATH)
Expand All @@ -70,11 +72,13 @@ public Flux<Movie> saveMoviesReactive(@RequestBody Flux<Movie> movies,
@RequestParam Long delayInMillis) {
log("saveMoviesReactive");
return waitOrFetchEpochMillisReactive(delayCallDepth, delayInMillis)
.flatMapMany(ignore -> movies.flatMap(movie -> Mono.just(movieRepo.save(movie))));
.flatMapMany(ignore -> movies
.publishOn(boundedElastic())
.map(movieRepo::save));
}

@DeleteMapping({PLATFORM_TOMCAT + API_PATH + "/{id}", LOOM_TOMCAT + API_PATH + "/{id}", LOOM_NETTY + API_PATH + "/{id}"})
public void deleteMovieById(@PathVariable UUID id,
public void deleteMovieById(@PathVariable Long id,
@RequestParam Integer delayCallDepth,
@RequestParam Long delayInMillis) throws InterruptedException {
log("deleteMoviesById");
Expand All @@ -83,11 +87,13 @@ public void deleteMovieById(@PathVariable UUID id,
}

@DeleteMapping(WEBFLUX_NETTY + API_PATH + "/{id}")
public Mono<Void> deleteMovieByIdReactive(@PathVariable UUID id,
public Mono<Void> deleteMovieByIdReactive(@PathVariable Long id,
@RequestParam Integer delayCallDepth,
@RequestParam Long delayInMillis) {
log("deleteMoviesByIdReactive");
return waitOrFetchEpochMillisReactive(delayCallDepth, delayInMillis)
.then(Mono.fromRunnable(() -> movieRepo.deleteById(id)));
.then(Mono.fromRunnable(() -> movieRepo.deleteById(id))
.subscribeOn(boundedElastic()))
.then();
}
}
13 changes: 0 additions & 13 deletions src/main/java/uk/gleissner/loomwebflux/movie/domain/Actor.java

This file was deleted.

25 changes: 20 additions & 5 deletions src/main/java/uk/gleissner/loomwebflux/movie/domain/Award.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Value
@Builder
@Jacksonized
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Award {

@Id
@GeneratedValue(strategy = IDENTITY)
@EqualsAndHashCode.Exclude
Long id;

String name;
int year;
}
32 changes: 32 additions & 0 deletions src/main/java/uk/gleissner/loomwebflux/movie/domain/Character.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Character {

@Id
@GeneratedValue(strategy = IDENTITY)
@EqualsAndHashCode.Exclude
Long id;

String name;

@ManyToOne(cascade = PERSIST)
Person actor;
}
43 changes: 31 additions & 12 deletions src/main/java/uk/gleissner/loomwebflux/movie/domain/Movie.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.jetbrains.annotations.NotNull;

import java.util.Comparator;
import java.util.List;
import java.util.UUID;

@Value
import static jakarta.persistence.CascadeType.ALL;
import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Data
@Builder(toBuilder = true)
@Jacksonized
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor
@AllArgsConstructor
public class Movie implements Comparable<Movie> {

private final static Comparator<Movie> comparator = Comparator.comparing(Movie::getTitle)
.thenComparingInt(Movie::getReleaseYear)
.thenComparing(Movie::getGenre)
.thenComparing(Movie::getId);
.thenComparingInt(Movie::getReleaseYear)
.thenComparing(Movie::getGenre)
.thenComparing(Movie::getId);

@EqualsAndHashCode.Include
UUID id;
@Id
@GeneratedValue(strategy = IDENTITY)
@EqualsAndHashCode.Exclude
Long id;

@NonNull
String title;
Expand All @@ -34,10 +45,18 @@ public class Movie implements Comparable<Movie> {
@NonNull
Genre genre;

List<Actor> actors;
@ManyToMany(cascade = ALL)
List<Character> characters;

@ManyToMany(cascade = PERSIST)
List<Person> directors;

@ManyToMany(cascade = PERSIST)
List<Person> writers;

@ManyToMany(cascade = ALL)
List<Award> awards;

Double rating;

@Override
Expand Down
33 changes: 24 additions & 9 deletions src/main/java/uk/gleissner/loomwebflux/movie/domain/Person.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
package uk.gleissner.loomwebflux.movie.domain;

import jakarta.annotation.Nullable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.val;

import java.time.LocalDate;

@Value
@Builder
@Jacksonized
import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Person {

@Id
@GeneratedValue(strategy = IDENTITY)
@EqualsAndHashCode.Exclude
Long id;

String firstName;
String lastName;
LocalDate birthday;
Expand All @@ -23,9 +38,9 @@ public static Person of(String fullName) {
public static Person of(String fullName, @Nullable LocalDate birthday) {
val lastSpaceIdx = fullName.lastIndexOf(' ');
return Person.builder()
.firstName(fullName.substring(0, lastSpaceIdx).trim())
.lastName(fullName.substring(lastSpaceIdx + 1))
.birthday(birthday)
.build();
.firstName(fullName.substring(0, lastSpaceIdx).trim())
.lastName(fullName.substring(lastSpaceIdx + 1))
.birthday(birthday)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package uk.gleissner.loomwebflux.movie.repo;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import uk.gleissner.loomwebflux.config.AppProperties;
import uk.gleissner.loomwebflux.movie.domain.Movie;

import java.util.Set;

@Component
@RequiredArgsConstructor
public class AppPropertiesAwareMovieRepo {
private final AppProperties appProperties;
private final MovieRepo underlying;

public Set<Movie> findByDirectorName(String directorName) {
return underlying.findByDirectorName(directorName);
}

public Movie save(Movie movie) {
return appProperties.repoReadOnly() ? movie : underlying.save(movie);
}

public void deleteById(Long id) {
if (!appProperties.repoReadOnly())
underlying.deleteById(id);
}
}
Loading

0 comments on commit 6d69a21

Please sign in to comment.