├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── docker-compose.yml ├── documentation ├── jpa-relationships.drawio ├── persons_person_details.png ├── players_weapons.png ├── restaurants_dishes.png ├── reviewers_comments_articles.png ├── students_courses.png ├── teams_team_details.png └── writers_books.png ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── ivanfranchin │ │ └── springdatajparelationships │ │ ├── SpringDataJpaRelationshipsApplication.java │ │ ├── config │ │ ├── ErrorAttributesConfig.java │ │ └── SwaggerConfig.java │ │ ├── manytomany │ │ ├── compositepkextracolumn │ │ │ ├── exception │ │ │ │ ├── CourseNotFoundException.java │ │ │ │ ├── CourseStudentNotFoundException.java │ │ │ │ └── StudentNotFoundException.java │ │ │ ├── mapper │ │ │ │ ├── CourseMapper.java │ │ │ │ ├── CourseStudentMapper.java │ │ │ │ └── StudentMapper.java │ │ │ ├── model │ │ │ │ ├── Course.java │ │ │ │ ├── CourseStudent.java │ │ │ │ ├── CourseStudentPk.java │ │ │ │ └── Student.java │ │ │ ├── repository │ │ │ │ ├── CourseRepository.java │ │ │ │ ├── CourseStudentRepository.java │ │ │ │ └── StudentRepository.java │ │ │ ├── rest │ │ │ │ ├── StudentCourseController.java │ │ │ │ └── dto │ │ │ │ │ ├── CourseResponse.java │ │ │ │ │ ├── CourseStudentResponse.java │ │ │ │ │ ├── CreateCourseRequest.java │ │ │ │ │ ├── CreateStudentRequest.java │ │ │ │ │ ├── StudentResponse.java │ │ │ │ │ ├── UpdateCourseRequest.java │ │ │ │ │ ├── UpdateCourseStudentRequest.java │ │ │ │ │ └── UpdateStudentRequest.java │ │ │ └── service │ │ │ │ ├── CourseService.java │ │ │ │ ├── CourseServiceImpl.java │ │ │ │ ├── CourseStudentService.java │ │ │ │ ├── CourseStudentServiceImpl.java │ │ │ │ ├── StudentService.java │ │ │ │ └── StudentServiceImpl.java │ │ ├── simplepk │ │ │ ├── exception │ │ │ │ ├── BookNotFoundException.java │ │ │ │ └── WriterNotFoundException.java │ │ │ ├── mapper │ │ │ │ ├── BookMapper.java │ │ │ │ └── WriterMapper.java │ │ │ ├── model │ │ │ │ ├── Book.java │ │ │ │ └── Writer.java │ │ │ ├── repository │ │ │ │ ├── BookRepository.java │ │ │ │ └── WriterRepository.java │ │ │ ├── rest │ │ │ │ ├── WriterBookController.java │ │ │ │ └── dto │ │ │ │ │ ├── BookResponse.java │ │ │ │ │ ├── CreateBookRequest.java │ │ │ │ │ ├── CreateWriterRequest.java │ │ │ │ │ ├── UpdateBookRequest.java │ │ │ │ │ ├── UpdateWriterRequest.java │ │ │ │ │ └── WriterResponse.java │ │ │ └── service │ │ │ │ ├── BookService.java │ │ │ │ ├── BookServiceImpl.java │ │ │ │ ├── WriterService.java │ │ │ │ └── WriterServiceImpl.java │ │ └── simplepkextracolumn │ │ │ ├── exception │ │ │ ├── ArticleNotFoundException.java │ │ │ ├── CommentNotFoundException.java │ │ │ └── ReviewerNotFoundException.java │ │ │ ├── mapper │ │ │ ├── ArticleMapper.java │ │ │ ├── CommentMapper.java │ │ │ └── ReviewerMapper.java │ │ │ ├── model │ │ │ ├── Article.java │ │ │ ├── Comment.java │ │ │ └── Reviewer.java │ │ │ ├── repository │ │ │ ├── ArticleRepository.java │ │ │ ├── CommentRepository.java │ │ │ └── ReviewerRepository.java │ │ │ ├── rest │ │ │ ├── ReviewerArticleController.java │ │ │ └── dto │ │ │ │ ├── ArticleResponse.java │ │ │ │ ├── CommentResponse.java │ │ │ │ ├── CreateArticleRequest.java │ │ │ │ ├── CreateCommentRequest.java │ │ │ │ ├── CreateReviewerRequest.java │ │ │ │ └── ReviewerResponse.java │ │ │ └── service │ │ │ ├── ArticleService.java │ │ │ ├── ArticleServiceImpl.java │ │ │ ├── CommentService.java │ │ │ ├── CommentServiceImpl.java │ │ │ ├── ReviewerService.java │ │ │ └── ReviewerServiceImpl.java │ │ ├── onetomany │ │ ├── compositepk │ │ │ ├── exception │ │ │ │ ├── PlayerNotFoundException.java │ │ │ │ └── WeaponNotFoundException.java │ │ │ ├── mapper │ │ │ │ ├── PlayerMapper.java │ │ │ │ └── WeaponMapper.java │ │ │ ├── model │ │ │ │ ├── Player.java │ │ │ │ ├── Weapon.java │ │ │ │ └── WeaponPk.java │ │ │ ├── repository │ │ │ │ ├── PlayerRepository.java │ │ │ │ └── WeaponRepository.java │ │ │ ├── rest │ │ │ │ ├── PlayerWeaponController.java │ │ │ │ └── dto │ │ │ │ │ ├── CreatePlayerRequest.java │ │ │ │ │ ├── CreateWeaponRequest.java │ │ │ │ │ ├── PlayerResponse.java │ │ │ │ │ └── WeaponResponse.java │ │ │ └── service │ │ │ │ ├── PlayerService.java │ │ │ │ ├── PlayerServiceImpl.java │ │ │ │ ├── WeaponService.java │ │ │ │ └── WeaponServiceImpl.java │ │ └── simplepk │ │ │ ├── exception │ │ │ ├── DishNotFoundException.java │ │ │ └── RestaurantNotFoundException.java │ │ │ ├── mapper │ │ │ ├── DishMapper.java │ │ │ └── RestaurantMapper.java │ │ │ ├── model │ │ │ ├── Dish.java │ │ │ └── Restaurant.java │ │ │ ├── repository │ │ │ ├── DishRepository.java │ │ │ └── RestaurantRepository.java │ │ │ ├── rest │ │ │ ├── RestaurantDishController.java │ │ │ └── dto │ │ │ │ ├── CreateDishRequest.java │ │ │ │ ├── CreateRestaurantRequest.java │ │ │ │ ├── DishResponse.java │ │ │ │ ├── RestaurantResponse.java │ │ │ │ ├── UpdateDishRequest.java │ │ │ │ └── UpdateRestaurantRequest.java │ │ │ └── service │ │ │ ├── DishService.java │ │ │ ├── DishServiceImpl.java │ │ │ ├── RestaurantService.java │ │ │ └── RestaurantServiceImpl.java │ │ └── onetoone │ │ ├── sharedpk │ │ ├── exception │ │ │ └── PersonNotFoundException.java │ │ ├── mapper │ │ │ └── PersonMapper.java │ │ ├── model │ │ │ ├── Person.java │ │ │ └── PersonDetail.java │ │ ├── repository │ │ │ └── PersonRepository.java │ │ ├── rest │ │ │ ├── PersonDetailController.java │ │ │ └── dto │ │ │ │ ├── CreatePersonDetailRequest.java │ │ │ │ ├── CreatePersonRequest.java │ │ │ │ ├── PersonDetailResponse.java │ │ │ │ ├── PersonResponse.java │ │ │ │ ├── UpdatePersonDetailRequest.java │ │ │ │ └── UpdatePersonRequest.java │ │ └── service │ │ │ ├── PersonService.java │ │ │ └── PersonServiceImpl.java │ │ └── simplepk │ │ ├── exception │ │ └── TeamNotFoundException.java │ │ ├── mapper │ │ └── TeamMapper.java │ │ ├── model │ │ ├── Team.java │ │ └── TeamDetail.java │ │ ├── repository │ │ └── TeamRepository.java │ │ ├── rest │ │ ├── TeamDetailController.java │ │ └── dto │ │ │ ├── CreateTeamDetailRequest.java │ │ │ ├── CreateTeamRequest.java │ │ │ ├── TeamDetailResponse.java │ │ │ ├── TeamResponse.java │ │ │ ├── UpdateTeamDetailRequest.java │ │ │ └── UpdateTeamRequest.java │ │ └── service │ │ ├── TeamService.java │ │ └── TeamServiceImpl.java └── resources │ ├── application.properties │ └── banner.txt └── test └── java └── com └── ivanfranchin └── springdatajparelationships ├── MyContainers.java ├── manytomany ├── compositepkextracolumn │ └── rest │ │ └── StudentCourseControllerTest.java ├── simplepk │ └── rest │ │ └── WriterBookControllerTest.java └── simplepkextracolumn │ └── rest │ └── ReviewerArticleControllerTest.java ├── onetomany ├── compositepk │ └── rest │ │ └── PlayerWeaponControllerTest.java └── simplepk │ └── rest │ └── RestaurantDishControllerTest.java └── onetoone ├── sharedpk └── rest │ └── PersonDetailControllerTest.java └── simplepk └── rest └── TeamDetailControllerTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ivangfr 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### MAC OS ### 36 | *.DS_Store 37 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-data-jpa-relationships 2 | 3 | The goal of this project is to study the JPA relationships: `one-to-one`, `one-to-many` / `many-to-one`, and `many-to-many`. 4 | 5 | ## Proof-of-Concepts & Articles 6 | 7 | On [ivangfr.github.io](https://ivangfr.github.io), I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for. 8 | 9 | ## Additional Readings 10 | 11 | - \[**Medium**\] [**Understanding Relationships in JPA: Introduction**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-introduction-5416c8a7c8a9) 12 | - \[**Medium**\] [**Understanding Relationships in JPA: One-to-One with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-one-with-simple-primary-key-7c32f7e13a6a) 13 | - \[**Medium**\] [**Understanding Relationships in JPA: One-to-One with Shared Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-one-with-shared-primary-key-36596416fe56) 14 | - \[**Medium**\] [**Understanding Relationships in JPA: One-to-Many with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-many-with-simple-primary-key-e2e975c67c31) 15 | - \[**Medium**\] [**Understanding Relationships in JPA: One-to-Many with Composite Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-many-with-composite-primary-key-1d7724a2bf63) 16 | - \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-simple-primary-key-b38209e5c9b4) 17 | - \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Simple Primary Key and Extra Column**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-simple-primary-key-and-extra-column-817e8bdda465) 18 | - \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Composite Primary Key and Extra Column**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-composite-primary-key-and-extra-column-a939b107c7cd) 19 | - \[**Medium**\] [**Mastering JPA Relationships: Practical Examples of Bidirectional Associations**](https://medium.com/@ivangfr/spring-data-jpa-6bb5cd745b46) 20 | 21 | ## Prerequisites 22 | 23 | - [`Java 21`](https://www.oracle.com/java/technologies/downloads/#java21) or higher; 24 | - A containerization tool (e.g., [`Docker`](https://www.docker.com), [`Podman`](https://podman.io), etc.) 25 | 26 | ## Start Environment 27 | 28 | In a terminal and inside the `spring-data-jpa-relationships` root folder, run the following command: 29 | ```bash 30 | docker compose up -d 31 | ``` 32 | 33 | ## Running application using Maven 34 | 35 | In a terminal and inside the `spring-data-jpa-relationships` root folder, run the command below: 36 | ```bash 37 | ./mvnw clean spring-boot:run 38 | ``` 39 | 40 | Once the application is running, you can access its Swagger website at http://localhost:8080/swagger-ui.html. 41 | 42 | ## Useful Commands 43 | 44 | - **Postgres** 45 | ```bash 46 | docker exec -it postgres psql -U postgres -d jparelationshipsdb 47 | \d persons 48 | select * from persons; 49 | ``` 50 | > Type `exit` to exit 51 | 52 | ## Shutdown 53 | 54 | - To stop the application, go to the terminal where it is running and press `Ctrl+C`; 55 | - To stop and remove docker compose containers, network and volumes, go to a terminal and, inside the `spring-data-jpa-relationships` root folder, run the following command: 56 | ```bash 57 | docker compose down -v 58 | ``` 59 | 60 | ## Running Tests 61 | 62 | In a terminal and inside the `spring-data-jpa-relationships` root folder, run the following command: 63 | ```bash 64 | ./mvnw clean test 65 | ``` 66 | 67 | ## JPA relationships 68 | 69 | ### One-to-One with Simple Primary Key 70 | 71 | ![teams_team_details](documentation/teams_team_details.png) 72 | 73 | \[**Medium**\]: [**Understanding Relationships in JPA: One-to-One with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-one-with-simple-primary-key-7c32f7e13a6a) 74 | 75 | ### One-to-One with Shared Primary Key 76 | 77 | ![persons_person_details](documentation/persons_person_details.png) 78 | 79 | \[**Medium**\] [**Understanding Relationships in JPA: One-to-One with Shared Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-one-with-shared-primary-key-36596416fe56) 80 | 81 | ### One-to-Many with Simple Primary Key 82 | 83 | ![restaurants_dishes](documentation/restaurants_dishes.png) 84 | 85 | \[**Medium**\] [**Understanding Relationships in JPA: One-to-Many with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-many-with-simple-primary-key-e2e975c67c31) 86 | 87 | ### One-to-Many with Composite Primary Key 88 | 89 | ![players_weapons](documentation/players_weapons.png) 90 | 91 | \[**Medium**\] [**Understanding Relationships in JPA: One-to-Many with Composite Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-one-to-many-with-composite-primary-key-1d7724a2bf63) 92 | 93 | ### Many-to-Many with Simple Primary Key 94 | 95 | ![writers_books](documentation/writers_books.png) 96 | 97 | \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Simple Primary Key**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-simple-primary-key-b38209e5c9b4) 98 | 99 | ### Many-to-Many with Simple Primary Key and Extra Column 100 | 101 | ![reviewers_articles](documentation/reviewers_comments_articles.png) 102 | 103 | \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Simple Primary Key and Extra Column**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-simple-primary-key-and-extra-column-817e8bdda465) 104 | 105 | ### Many-to-Many with Composite Primary Key and Extra Column 106 | 107 | ![students_courses](documentation/students_courses.png) 108 | 109 | \[**Medium**\] [**Understanding Relationships in JPA: Many-to-Many with Composite Primary Key and Extra Column**](https://medium.com/@ivangfr/understanding-relationships-in-jpa-many-to-many-with-composite-primary-key-and-extra-column-a939b107c7cd) 110 | 111 | ## References 112 | 113 | One-to-One 114 | - https://vladmihalcea.com/the-best-way-to-map-a-onetoone-relationship-with-jpa-and-hibernate 115 | - https://www.callicoder.com/hibernate-spring-boot-jpa-one-to-one-mapping-example/ 116 | 117 | One-to-Many / Many-to-One 118 | - https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate 119 | - https://www.callicoder.com/hibernate-spring-boot-jpa-one-to-many-mapping-example/ 120 | 121 | Many-to-Many 122 | - https://vladmihalcea.com/the-best-way-to-use-the-manytomany-annotation-with-jpa-and-hibernate 123 | - https://www.callicoder.com/hibernate-spring-boot-jpa-many-to-many-mapping-example/ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | postgres: 4 | image: 'postgres:17.2' 5 | container_name: 'postgres' 6 | ports: 7 | - '5432:5432' 8 | environment: 9 | - 'POSTGRES_DB=jparelationshipsdb' 10 | - 'POSTGRES_PASSWORD=postgres' 11 | - 'POSTGRES_USER=postgres' 12 | healthcheck: 13 | test: 'pg_isready -U postgres' 14 | -------------------------------------------------------------------------------- /documentation/persons_person_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/persons_person_details.png -------------------------------------------------------------------------------- /documentation/players_weapons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/players_weapons.png -------------------------------------------------------------------------------- /documentation/restaurants_dishes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/restaurants_dishes.png -------------------------------------------------------------------------------- /documentation/reviewers_comments_articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/reviewers_comments_articles.png -------------------------------------------------------------------------------- /documentation/students_courses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/students_courses.png -------------------------------------------------------------------------------- /documentation/teams_team_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/teams_team_details.png -------------------------------------------------------------------------------- /documentation/writers_books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-data-jpa-relationships/c3d01b64093b56f7647b62da661101a7802ab5b8/documentation/writers_books.png -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.4 9 | 10 | 11 | com.ivanfranchin 12 | spring-data-jpa-relationships 13 | 0.0.1-SNAPSHOT 14 | spring-data-jpa-relationships 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 1.6.3 32 | 0.2.0 33 | 2.8.6 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | 50 | 51 | org.mapstruct 52 | mapstruct 53 | ${org.mapstruct.version} 54 | 55 | 56 | 57 | 58 | org.springdoc 59 | springdoc-openapi-starter-webmvc-ui 60 | ${springdoc-openapi.version} 61 | 62 | 63 | 64 | org.postgresql 65 | postgresql 66 | runtime 67 | 68 | 69 | org.projectlombok 70 | lombok 71 | true 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-test 76 | test 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-testcontainers 81 | test 82 | 83 | 84 | org.testcontainers 85 | junit-jupiter 86 | test 87 | 88 | 89 | org.testcontainers 90 | postgresql 91 | test 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-compiler-plugin 100 | 101 | 102 | 103 | org.projectlombok 104 | lombok 105 | 106 | 107 | org.projectlombok 108 | lombok-mapstruct-binding 109 | ${lombok-mapstruct-binding.version} 110 | 111 | 112 | org.mapstruct 113 | mapstruct-processor 114 | ${org.mapstruct.version} 115 | 116 | 117 | 118 | 119 | 120 | org.springframework.boot 121 | spring-boot-maven-plugin 122 | 123 | 124 | 125 | org.projectlombok 126 | lombok 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/SpringDataJpaRelationshipsApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SpringDataJpaRelationshipsApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringDataJpaRelationshipsApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class SwaggerConfig { 12 | 13 | @Value("${spring.application.name}") 14 | private String applicationName; 15 | 16 | @Bean 17 | OpenAPI customOpenAPI() { 18 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/exception/CourseNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class CourseNotFoundException extends RuntimeException { 8 | 9 | public CourseNotFoundException(Long id) { 10 | super(String.format("Course with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/exception/CourseStudentNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudentPk; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.NOT_FOUND) 8 | public class CourseStudentNotFoundException extends RuntimeException { 9 | 10 | public CourseStudentNotFoundException(CourseStudentPk courseStudentPk) { 11 | super(String.format("CourseStudent with id '%s' not found", courseStudentPk)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/exception/StudentNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class StudentNotFoundException extends RuntimeException { 8 | 9 | public StudentNotFoundException(Long id) { 10 | super(String.format("Student with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/mapper/CourseMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Course; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CourseResponse; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CreateCourseRequest; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateCourseRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface CourseMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "students", ignore = true) 20 | Course toCourse(CreateCourseRequest createCourseRequest); 21 | 22 | CourseResponse toCourseResponse(Course course); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "students", ignore = true) 26 | void updateCourseFromRequest(UpdateCourseRequest updateCourseRequest, @MappingTarget Course course); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/mapper/CourseStudentMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudent; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CourseStudentResponse; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateCourseStudentRequest; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | import org.mapstruct.MappingTarget; 9 | import org.mapstruct.NullValuePropertyMappingStrategy; 10 | 11 | @Mapper( 12 | componentModel = "spring", 13 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 14 | ) 15 | public interface CourseStudentMapper { 16 | 17 | CourseStudentResponse toCourseStudentResponse(CourseStudent courseStudent); 18 | 19 | @Mapping(target = "course", ignore = true) 20 | @Mapping(target = "student", ignore = true) 21 | @Mapping(target = "registrationDate", ignore = true) 22 | void updateCourseStudentFromRequest(UpdateCourseStudentRequest updateCourseStudentRequest, 23 | @MappingTarget CourseStudent courseStudent); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/mapper/StudentMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Student; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CreateStudentRequest; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.StudentResponse; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateStudentRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface StudentMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "courses", ignore = true) 20 | Student toStudent(CreateStudentRequest createStudentRequest); 21 | 22 | StudentResponse toStudentResponse(Student student); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "courses", ignore = true) 26 | void updateStudentFromRequest(UpdateStudentRequest updateStudentRequest, @MappingTarget Student student); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/model/Course.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "students") 20 | @EqualsAndHashCode(exclude = "students") 21 | @Entity 22 | @Table(name = "courses") 23 | public class Course { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "course", cascade = CascadeType.ALL) 30 | private Set students = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String name; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/model/CourseStudent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.IdClass; 7 | import jakarta.persistence.JoinColumn; 8 | import jakarta.persistence.ManyToOne; 9 | import jakarta.persistence.Table; 10 | import lombok.Data; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | 14 | import java.time.Instant; 15 | 16 | @Data 17 | @ToString(exclude = {"course", "student"}) 18 | @EqualsAndHashCode(exclude = {"course", "student"}) 19 | @Entity 20 | @Table(name = "courses_students") 21 | @IdClass(CourseStudentPk.class) 22 | public class CourseStudent { 23 | 24 | @Id 25 | @ManyToOne 26 | @JoinColumn(name = "course_id") 27 | private Course course; 28 | 29 | @Id 30 | @ManyToOne 31 | @JoinColumn(name = "student_id") 32 | private Student student; 33 | 34 | @Column(nullable = false) 35 | private Instant registrationDate = Instant.now(); 36 | 37 | private Short grade; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/model/CourseStudentPk.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public record CourseStudentPk(Long course, Long student) implements Serializable { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/model/Student.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "courses") 20 | @EqualsAndHashCode(exclude = "courses") 21 | @Entity 22 | @Table(name = "students") 23 | public class Student { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "student", cascade = CascadeType.ALL) 30 | private Set courses = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String name; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/repository/CourseRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Course; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CourseRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/repository/CourseStudentRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudent; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudentPk; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface CourseStudentRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/repository/StudentRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Student; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface StudentRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/StudentCourseController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper.CourseMapper; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper.CourseStudentMapper; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.mapper.StudentMapper; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Course; 7 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudent; 8 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Student; 9 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CourseResponse; 10 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CourseStudentResponse; 11 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CreateCourseRequest; 12 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.CreateStudentRequest; 13 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.StudentResponse; 14 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateCourseRequest; 15 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateCourseStudentRequest; 16 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto.UpdateStudentRequest; 17 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service.CourseService; 18 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service.CourseStudentService; 19 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service.StudentService; 20 | import jakarta.validation.Valid; 21 | import lombok.RequiredArgsConstructor; 22 | import org.springframework.http.HttpStatus; 23 | import org.springframework.web.bind.annotation.DeleteMapping; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.PathVariable; 26 | import org.springframework.web.bind.annotation.PostMapping; 27 | import org.springframework.web.bind.annotation.PutMapping; 28 | import org.springframework.web.bind.annotation.RequestBody; 29 | import org.springframework.web.bind.annotation.RequestMapping; 30 | import org.springframework.web.bind.annotation.ResponseStatus; 31 | import org.springframework.web.bind.annotation.RestController; 32 | 33 | @RequiredArgsConstructor 34 | @RestController 35 | @RequestMapping("/api") 36 | public class StudentCourseController { 37 | 38 | private final StudentService studentService; 39 | private final CourseService courseService; 40 | private final CourseStudentService courseStudentService; 41 | private final StudentMapper studentMapper; 42 | private final CourseMapper courseMapper; 43 | private final CourseStudentMapper courseStudentMapper; 44 | 45 | // ------- 46 | // Student 47 | 48 | @GetMapping("/students/{studentId}") 49 | public StudentResponse getStudent(@PathVariable Long studentId) { 50 | Student student = studentService.validateAndGetStudent(studentId); 51 | return studentMapper.toStudentResponse(student); 52 | } 53 | 54 | @ResponseStatus(HttpStatus.CREATED) 55 | @PostMapping("/students") 56 | public StudentResponse createStudent(@Valid @RequestBody CreateStudentRequest createStudentRequest) { 57 | Student student = studentMapper.toStudent(createStudentRequest); 58 | student = studentService.saveStudent(student); 59 | return studentMapper.toStudentResponse(student); 60 | } 61 | 62 | @PutMapping("/students/{studentId}") 63 | public StudentResponse updateStudent(@PathVariable Long studentId, 64 | @Valid @RequestBody UpdateStudentRequest updateStudentRequest) { 65 | Student student = studentService.validateAndGetStudent(studentId); 66 | studentMapper.updateStudentFromRequest(updateStudentRequest, student); 67 | student = studentService.saveStudent(student); 68 | return studentMapper.toStudentResponse(student); 69 | } 70 | 71 | @DeleteMapping("/students/{studentId}") 72 | public StudentResponse deleteStudent(@PathVariable Long studentId) { 73 | Student student = studentService.validateAndGetStudent(studentId); 74 | studentService.deleteStudent(student); 75 | return studentMapper.toStudentResponse(student); 76 | } 77 | 78 | // ------ 79 | // Course 80 | 81 | @GetMapping("/courses/{courseId}") 82 | public CourseResponse getCourse(@PathVariable Long courseId) { 83 | Course course = courseService.validateAndGetCourse(courseId); 84 | return courseMapper.toCourseResponse(course); 85 | } 86 | 87 | @ResponseStatus(HttpStatus.CREATED) 88 | @PostMapping("/courses") 89 | public CourseResponse createCourse(@Valid @RequestBody CreateCourseRequest createCourseRequest) { 90 | Course course = courseMapper.toCourse(createCourseRequest); 91 | course = courseService.saveCourse(course); 92 | return courseMapper.toCourseResponse(course); 93 | } 94 | 95 | @PutMapping("/courses/{courseId}") 96 | public CourseResponse updateCourse(@PathVariable Long courseId, 97 | @Valid @RequestBody UpdateCourseRequest updateCourseRequest) { 98 | Course course = courseService.validateAndGetCourse(courseId); 99 | courseMapper.updateCourseFromRequest(updateCourseRequest, course); 100 | course = courseService.saveCourse(course); 101 | return courseMapper.toCourseResponse(course); 102 | } 103 | 104 | @DeleteMapping("/courses/{courseId}") 105 | public CourseResponse deleteCourse(@PathVariable Long courseId) { 106 | Course course = courseService.validateAndGetCourse(courseId); 107 | courseService.deleteCourse(course); 108 | return courseMapper.toCourseResponse(course); 109 | } 110 | 111 | @ResponseStatus(HttpStatus.CREATED) 112 | @PostMapping("/courses/{courseId}/students/{studentId}") 113 | public CourseStudentResponse enrollStudentInCourse(@PathVariable Long courseId, @PathVariable Long studentId) { 114 | Course course = courseService.validateAndGetCourse(courseId); 115 | Student student = studentService.validateAndGetStudent(studentId); 116 | 117 | CourseStudent courseStudent = new CourseStudent(); 118 | courseStudent.setStudent(student); 119 | courseStudent.setCourse(course); 120 | courseStudent = courseStudentService.saveCourseStudent(courseStudent); 121 | 122 | return courseStudentMapper.toCourseStudentResponse(courseStudent); 123 | } 124 | 125 | @DeleteMapping("/courses/{courseId}/students/{studentId}") 126 | public CourseStudentResponse unregisterStudentOfCourse(@PathVariable Long courseId, @PathVariable Long studentId) { 127 | CourseStudent courseStudent = courseStudentService.validateAndGetCourseStudent(courseId, studentId); 128 | courseStudentService.deleteCourseStudent(courseStudent); 129 | return courseStudentMapper.toCourseStudentResponse(courseStudent); 130 | } 131 | 132 | @PutMapping("/courses/{courseId}/students/{studentId}") 133 | public CourseStudentResponse updateStudentDataInCourse(@PathVariable Long courseId, @PathVariable Long studentId, 134 | @Valid @RequestBody UpdateCourseStudentRequest updateCourseStudentRequest) { 135 | CourseStudent courseStudent = courseStudentService.validateAndGetCourseStudent(courseId, studentId); 136 | courseStudentMapper.updateCourseStudentFromRequest(updateCourseStudentRequest, courseStudent); 137 | courseStudentService.saveCourseStudent(courseStudent); 138 | return courseStudentMapper.toCourseStudentResponse(courseStudent); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/CourseResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | public record CourseResponse(Long id, String name, List students) { 7 | 8 | public record CourseStudent(Student student, Date registrationDate, Short grade) { 9 | 10 | public record Student(Long id, String name) { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/CourseStudentResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import java.util.Date; 4 | 5 | public record CourseStudentResponse(Course course, Student student, Date registrationDate, Short grade) { 6 | 7 | public record Course(Long id, String name) { 8 | } 9 | 10 | public record Student(Long id, String name) { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/CreateCourseRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateCourseRequest(@Schema(example = "Java 8") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/CreateStudentRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateStudentRequest(@Schema(example = "Ivan Franchin") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/StudentResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | public record StudentResponse(Long id, String name, List courses) { 7 | 8 | public record CourseStudent(Course course, Date registrationDate, Short grade) { 9 | 10 | public record Course(Long id, String name) { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/UpdateCourseRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateCourseRequest(@Schema(example = "Java 9") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/UpdateCourseStudentRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Max; 5 | import jakarta.validation.constraints.Min; 6 | import jakarta.validation.constraints.NotNull; 7 | 8 | public record UpdateCourseStudentRequest(@Schema(example = "9") @NotNull @Min(value = 0) @Max(value = 10) Short grade) { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/rest/dto/UpdateStudentRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateStudentRequest(@Schema(example = "Steve Jobs") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/CourseService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Course; 4 | 5 | public interface CourseService { 6 | 7 | Course validateAndGetCourse(Long id); 8 | 9 | Course saveCourse(Course course); 10 | 11 | void deleteCourse(Course course); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/CourseServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception.CourseNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Course; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository.CourseRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class CourseServiceImpl implements CourseService { 12 | 13 | private final CourseRepository courseRepository; 14 | 15 | @Override 16 | public Course validateAndGetCourse(Long id) { 17 | return courseRepository.findById(id).orElseThrow(() -> new CourseNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Course saveCourse(Course course) { 22 | return courseRepository.save(course); 23 | } 24 | 25 | @Override 26 | public void deleteCourse(Course course) { 27 | courseRepository.delete(course); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/CourseStudentService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudent; 4 | 5 | public interface CourseStudentService { 6 | 7 | CourseStudent validateAndGetCourseStudent(Long courseId, Long studentId); 8 | 9 | CourseStudent saveCourseStudent(CourseStudent courseStudent); 10 | 11 | void deleteCourseStudent(CourseStudent courseStudent); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/CourseStudentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception.CourseStudentNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudent; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.CourseStudentPk; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository.CourseStudentRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class CourseStudentServiceImpl implements CourseStudentService { 13 | 14 | private final CourseStudentRepository courseStudentRepository; 15 | 16 | @Override 17 | public CourseStudent validateAndGetCourseStudent(Long courseId, Long studentId) { 18 | CourseStudentPk courseStudentPk = new CourseStudentPk(courseId, studentId); 19 | return courseStudentRepository.findById(courseStudentPk) 20 | .orElseThrow(() -> new CourseStudentNotFoundException(courseStudentPk)); 21 | } 22 | 23 | @Override 24 | public CourseStudent saveCourseStudent(CourseStudent courseStudent) { 25 | return courseStudentRepository.save(courseStudent); 26 | } 27 | 28 | @Override 29 | public void deleteCourseStudent(CourseStudent courseStudent) { 30 | courseStudentRepository.delete(courseStudent); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/StudentService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Student; 4 | 5 | public interface StudentService { 6 | 7 | Student validateAndGetStudent(Long id); 8 | 9 | Student saveStudent(Student student); 10 | 11 | void deleteStudent(Student student); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/compositepkextracolumn/service/StudentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.exception.StudentNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.model.Student; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.compositepkextracolumn.repository.StudentRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class StudentServiceImpl implements StudentService { 12 | 13 | private final StudentRepository studentRepository; 14 | 15 | @Override 16 | public Student validateAndGetStudent(Long id) { 17 | return studentRepository.findById(id).orElseThrow(() -> new StudentNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Student saveStudent(Student student) { 22 | return studentRepository.save(student); 23 | } 24 | 25 | @Override 26 | public void deleteStudent(Student student) { 27 | studentRepository.delete(student); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/exception/BookNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class BookNotFoundException extends RuntimeException { 8 | 9 | public BookNotFoundException(Long id) { 10 | super(String.format("Book with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/exception/WriterNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class WriterNotFoundException extends RuntimeException { 8 | 9 | public WriterNotFoundException(Long id) { 10 | super(String.format("Writer with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/mapper/BookMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.BookResponse; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.UpdateBookRequest; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Book; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.CreateBookRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface BookMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "writers", ignore = true) 20 | Book toBook(CreateBookRequest createBookRequest); 21 | 22 | BookResponse toBookResponse(Book book); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "writers", ignore = true) 26 | void updateBookFromRequest(UpdateBookRequest updateBookRequest, @MappingTarget Book book); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/mapper/WriterMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Writer; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.WriterResponse; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.CreateWriterRequest; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.UpdateWriterRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface WriterMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "books", ignore = true) 20 | Writer toWriter(CreateWriterRequest createWriterRequest); 21 | 22 | WriterResponse toWriterResponse(Writer writer); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "books", ignore = true) 26 | void updateWriterFromRequest(UpdateWriterRequest updateWriterRequest, @MappingTarget Writer writer); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/model/Book.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.JoinTable; 11 | import jakarta.persistence.ManyToMany; 12 | import jakarta.persistence.Table; 13 | import lombok.Data; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.ToString; 16 | 17 | import java.util.LinkedHashSet; 18 | import java.util.Set; 19 | 20 | @Data 21 | @ToString(exclude = "writers") 22 | @EqualsAndHashCode(exclude = "writers") 23 | @Entity 24 | @Table(name = "books") 25 | public class Book { 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 29 | private Long id; 30 | 31 | @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 32 | @JoinTable(name = "books_writers", 33 | joinColumns = @JoinColumn(name = "book_id"), 34 | inverseJoinColumns = @JoinColumn(name = "writer_id") 35 | ) 36 | private Set writers = new LinkedHashSet<>(); 37 | 38 | @Column(nullable = false) 39 | private String name; 40 | 41 | public void addWriter(Writer writer) { 42 | this.writers.add(writer); 43 | writer.getBooks().add(this); 44 | } 45 | 46 | public void removeWriter(Writer writer) { 47 | this.writers.remove(writer); 48 | writer.getBooks().remove(this); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/model/Writer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.ManyToMany; 9 | import jakarta.persistence.Table; 10 | import lombok.Data; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | 14 | import java.util.LinkedHashSet; 15 | import java.util.Set; 16 | 17 | @Data 18 | @ToString(exclude = "books") 19 | @EqualsAndHashCode(exclude = "books") 20 | @Entity 21 | @Table(name = "writers") 22 | public class Writer { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 26 | private Long id; 27 | 28 | @ManyToMany(mappedBy = "writers") 29 | private Set books = new LinkedHashSet<>(); 30 | 31 | @Column(nullable = false) 32 | private String name; 33 | 34 | public void addBook(Book book) { 35 | this.books.add(book); 36 | book.getWriters().add(this); 37 | } 38 | 39 | public void removeBook(Book book) { 40 | this.books.remove(book); 41 | book.getWriters().remove(this); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Book; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface BookRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/repository/WriterRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Writer; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface WriterRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/WriterBookController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.mapper.BookMapper; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Book; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.BookResponse; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.UpdateBookRequest; 7 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service.BookService; 8 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service.WriterService; 9 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.mapper.WriterMapper; 10 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Writer; 11 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.CreateBookRequest; 12 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.CreateWriterRequest; 13 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.UpdateWriterRequest; 14 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto.WriterResponse; 15 | import jakarta.validation.Valid; 16 | import lombok.RequiredArgsConstructor; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.web.bind.annotation.DeleteMapping; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PathVariable; 21 | import org.springframework.web.bind.annotation.PostMapping; 22 | import org.springframework.web.bind.annotation.PutMapping; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.ResponseStatus; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | @RequiredArgsConstructor 29 | @RestController 30 | @RequestMapping("/api") 31 | public class WriterBookController { 32 | 33 | private final WriterService writerService; 34 | private final BookService bookService; 35 | private final WriterMapper writerMapper; 36 | private final BookMapper bookMapper; 37 | 38 | // ------ 39 | // Writer 40 | 41 | @GetMapping("/writers/{writerId}") 42 | public WriterResponse getWriter(@PathVariable Long writerId) { 43 | Writer writer = writerService.validateAndGetWriter(writerId); 44 | return writerMapper.toWriterResponse(writer); 45 | } 46 | 47 | @ResponseStatus(HttpStatus.CREATED) 48 | @PostMapping("/writers") 49 | public WriterResponse createWriter(@Valid @RequestBody CreateWriterRequest createWriterRequest) { 50 | Writer writer = writerMapper.toWriter(createWriterRequest); 51 | writer = writerService.saveWriter(writer); 52 | return writerMapper.toWriterResponse(writer); 53 | } 54 | 55 | @PutMapping("/writers/{writerId}") 56 | public WriterResponse updateWriter(@PathVariable Long writerId, 57 | @Valid @RequestBody UpdateWriterRequest updateWriterRequest) { 58 | Writer writer = writerService.validateAndGetWriter(writerId); 59 | writerMapper.updateWriterFromRequest(updateWriterRequest, writer); 60 | writer = writerService.saveWriter(writer); 61 | return writerMapper.toWriterResponse(writer); 62 | } 63 | 64 | @DeleteMapping("/writers/{writerId}") 65 | public WriterResponse deleteWriter(@PathVariable Long writerId) { 66 | Writer writer = writerService.validateAndGetWriter(writerId); 67 | writer.getBooks().forEach(book -> book.removeWriter(writer)); 68 | writerService.deleteWriter(writer); 69 | return writerMapper.toWriterResponse(writer); 70 | } 71 | 72 | @ResponseStatus(HttpStatus.CREATED) 73 | @PostMapping("/writers/{writerId}/books/{bookId}") 74 | public WriterResponse addWriterBook(@PathVariable Long writerId, @PathVariable Long bookId) { 75 | Writer writer = writerService.validateAndGetWriter(writerId); 76 | Book book = bookService.validateAndGetBook(bookId); 77 | writer.addBook(book); 78 | writer = writerService.saveWriter(writer); 79 | return writerMapper.toWriterResponse(writer); 80 | } 81 | 82 | @DeleteMapping("/writers/{writerId}/books/{bookId}") 83 | public WriterResponse removeWriterBook(@PathVariable Long writerId, @PathVariable Long bookId) { 84 | Writer writer = writerService.validateAndGetWriter(writerId); 85 | Book book = bookService.validateAndGetBook(bookId); 86 | writer.removeBook(book); 87 | writer = writerService.saveWriter(writer); 88 | return writerMapper.toWriterResponse(writer); 89 | } 90 | 91 | // ----- 92 | // Books 93 | 94 | @GetMapping("/books/{bookId}") 95 | public BookResponse getBook(@PathVariable Long bookId) { 96 | Book book = bookService.validateAndGetBook(bookId); 97 | return bookMapper.toBookResponse(book); 98 | } 99 | 100 | @ResponseStatus(HttpStatus.CREATED) 101 | @PostMapping("/books") 102 | public BookResponse createBook(@Valid @RequestBody CreateBookRequest createBookRequest) { 103 | Book book = bookMapper.toBook(createBookRequest); 104 | book = bookService.saveBook(book); 105 | return bookMapper.toBookResponse(book); 106 | } 107 | 108 | @PutMapping("/books/{bookId}") 109 | public BookResponse updateBook(@PathVariable Long bookId, @Valid @RequestBody UpdateBookRequest updateBookRequest) { 110 | Book book = bookService.validateAndGetBook(bookId); 111 | bookMapper.updateBookFromRequest(updateBookRequest, book); 112 | book = bookService.saveBook(book); 113 | return bookMapper.toBookResponse(book); 114 | } 115 | 116 | @DeleteMapping("/books/{bookId}") 117 | public BookResponse deleteBook(@PathVariable Long bookId) { 118 | Book book = bookService.validateAndGetBook(bookId); 119 | book.getWriters().forEach(writer -> writer.removeBook(book)); 120 | bookService.deleteBook(book); 121 | return bookMapper.toBookResponse(book); 122 | } 123 | 124 | @ResponseStatus(HttpStatus.CREATED) 125 | @PostMapping("/books/{bookId}/writers/{writerId}") 126 | public BookResponse addBookWriter(@PathVariable Long bookId, @PathVariable Long writerId) { 127 | Book book = bookService.validateAndGetBook(bookId); 128 | Writer writer = writerService.validateAndGetWriter(writerId); 129 | book.addWriter(writer); 130 | book = bookService.saveBook(book); 131 | return bookMapper.toBookResponse(book); 132 | } 133 | 134 | @DeleteMapping("/books/{bookId}/writers/{writerId}") 135 | public BookResponse removeBookWriter(@PathVariable Long bookId, @PathVariable Long writerId) { 136 | Book book = bookService.validateAndGetBook(bookId); 137 | Writer writer = writerService.validateAndGetWriter(writerId); 138 | book.removeWriter(writer); 139 | book = bookService.saveBook(book); 140 | return bookMapper.toBookResponse(book); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/BookResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import java.util.List; 4 | 5 | public record BookResponse(Long id, String name, List writers) { 6 | 7 | public record Writer(Long id, String name) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/CreateBookRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateBookRequest(@Schema(example = "Introduction to Java 8") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/CreateWriterRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateWriterRequest(@Schema(example = "Ivan Franchin") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/UpdateBookRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateBookRequest(@Schema(example = "Introduction to Springboot") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/UpdateWriterRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateWriterRequest(@Schema(example = "Steve Jobs") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/rest/dto/WriterResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.rest.dto; 2 | 3 | import java.util.List; 4 | 5 | public record WriterResponse(Long id, String name, List books) { 6 | 7 | public record Book(Long id, String name) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/service/BookService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Book; 4 | 5 | public interface BookService { 6 | 7 | Book validateAndGetBook(Long id); 8 | 9 | Book saveBook(Book book); 10 | 11 | void deleteBook(Book book); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/service/BookServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.exception.BookNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Book; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.repository.BookRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class BookServiceImpl implements BookService { 12 | 13 | private final BookRepository bookRepository; 14 | 15 | @Override 16 | public Book validateAndGetBook(Long id) { 17 | return bookRepository.findById(id).orElseThrow(() -> new BookNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Book saveBook(Book book) { 22 | return bookRepository.save(book); 23 | } 24 | 25 | @Override 26 | public void deleteBook(Book book) { 27 | bookRepository.delete(book); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/service/WriterService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Writer; 4 | 5 | public interface WriterService { 6 | 7 | Writer validateAndGetWriter(Long id); 8 | 9 | Writer saveWriter(Writer writer); 10 | 11 | void deleteWriter(Writer writer); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepk/service/WriterServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.exception.WriterNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.model.Writer; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepk.repository.WriterRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class WriterServiceImpl implements WriterService { 12 | 13 | private final WriterRepository writerRepository; 14 | 15 | @Override 16 | public Writer validateAndGetWriter(Long id) { 17 | return writerRepository.findById(id).orElseThrow(() -> new WriterNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Writer saveWriter(Writer writer) { 22 | return writerRepository.save(writer); 23 | } 24 | 25 | @Override 26 | public void deleteWriter(Writer writer) { 27 | writerRepository.delete(writer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/exception/ArticleNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class ArticleNotFoundException extends RuntimeException { 8 | 9 | public ArticleNotFoundException(Long id) { 10 | super(String.format("Article with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/exception/CommentNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class CommentNotFoundException extends RuntimeException { 8 | 9 | public CommentNotFoundException(Long id) { 10 | super(String.format("Comment with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/exception/ReviewerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class ReviewerNotFoundException extends RuntimeException { 8 | 9 | public ReviewerNotFoundException(Long id) { 10 | super(String.format("Reviewer with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/mapper/ArticleMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Article; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.ArticleResponse; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateArticleRequest; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface ArticleMapper { 11 | 12 | @Mapping(target = "id", ignore = true) 13 | @Mapping(target = "comments", ignore = true) 14 | Article toArticle(CreateArticleRequest createArticleRequest); 15 | 16 | ArticleResponse toArticleResponse(Article article); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Comment; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CommentResponse; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateCommentRequest; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service.ArticleService; 7 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service.ReviewerService; 8 | import org.mapstruct.InjectionStrategy; 9 | import org.mapstruct.Mapper; 10 | import org.mapstruct.Mapping; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | uses = {ReviewerService.class, ArticleService.class}, 15 | injectionStrategy = InjectionStrategy.CONSTRUCTOR 16 | ) 17 | public interface CommentMapper { 18 | 19 | @Mapping(target = "id", ignore = true) 20 | @Mapping(target = "reviewer", source = "reviewerId") 21 | @Mapping(target = "article", source = "articleId") 22 | Comment toComment(CreateCommentRequest createCommentRequest); 23 | 24 | CommentResponse toCommentResponse(Comment comment); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/mapper/ReviewerMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Reviewer; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateReviewerRequest; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.ReviewerResponse; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface ReviewerMapper { 11 | 12 | @Mapping(target = "id", ignore = true) 13 | @Mapping(target = "comments", ignore = true) 14 | Reviewer toReviewer(CreateReviewerRequest createReviewerRequest); 15 | 16 | ReviewerResponse toReviewerResponse(Reviewer reviewer); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/model/Article.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "comments") 20 | @EqualsAndHashCode(exclude = "comments") 21 | @Entity 22 | @Table(name = "articles") 23 | public class Article { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "article", cascade = CascadeType.ALL) 30 | private Set comments = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String title; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/model/Comment.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.ManyToOne; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | @Data 16 | @ToString(exclude = {"reviewer", "article"}) 17 | @EqualsAndHashCode(exclude = {"reviewer", "article"}) 18 | @Entity 19 | @Table(name = "comments") 20 | public class Comment { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 24 | private Long id; 25 | 26 | @ManyToOne 27 | @JoinColumn(name = "reviewer_id") 28 | private Reviewer reviewer; 29 | 30 | @ManyToOne 31 | @JoinColumn(name = "article_id") 32 | private Article article; 33 | 34 | @Column(nullable = false) 35 | private String text; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/model/Reviewer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "comments") 20 | @EqualsAndHashCode(exclude = "comments") 21 | @Entity 22 | @Table(name = "reviewers") 23 | public class Reviewer { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "reviewer", cascade = CascadeType.ALL) 30 | private Set comments = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String name; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/repository/ArticleRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Article; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ArticleRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Comment; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CommentRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/repository/ReviewerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Reviewer; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ReviewerRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/ReviewerArticleController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper.ArticleMapper; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper.CommentMapper; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.mapper.ReviewerMapper; 6 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Article; 7 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Comment; 8 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Reviewer; 9 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.ArticleResponse; 10 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CommentResponse; 11 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateArticleRequest; 12 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateCommentRequest; 13 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.CreateReviewerRequest; 14 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto.ReviewerResponse; 15 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service.ArticleService; 16 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service.CommentService; 17 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service.ReviewerService; 18 | import jakarta.validation.Valid; 19 | import lombok.RequiredArgsConstructor; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.web.bind.annotation.DeleteMapping; 22 | import org.springframework.web.bind.annotation.GetMapping; 23 | import org.springframework.web.bind.annotation.PathVariable; 24 | import org.springframework.web.bind.annotation.PostMapping; 25 | import org.springframework.web.bind.annotation.RequestBody; 26 | import org.springframework.web.bind.annotation.RequestMapping; 27 | import org.springframework.web.bind.annotation.ResponseStatus; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | @RequiredArgsConstructor 31 | @RestController 32 | @RequestMapping("/api") 33 | public class ReviewerArticleController { 34 | 35 | private final ArticleService articleService; 36 | private final ReviewerService reviewerService; 37 | private final CommentService commentService; 38 | private final ReviewerMapper reviewerMapper; 39 | private final ArticleMapper articleMapper; 40 | private final CommentMapper commentMapper; 41 | 42 | // -------- 43 | // Reviewer 44 | 45 | @GetMapping("/reviewers/{reviewerId}") 46 | public ReviewerResponse getReviewer(@PathVariable Long reviewerId) { 47 | Reviewer reviewer = reviewerService.validateAndGetReviewer(reviewerId); 48 | return reviewerMapper.toReviewerResponse(reviewer); 49 | } 50 | 51 | @ResponseStatus(HttpStatus.CREATED) 52 | @PostMapping("/reviewers") 53 | public ReviewerResponse createReviewer(@Valid @RequestBody CreateReviewerRequest createReviewerRequest) { 54 | Reviewer reviewer = reviewerMapper.toReviewer(createReviewerRequest); 55 | reviewer = reviewerService.saveReviewer(reviewer); 56 | return reviewerMapper.toReviewerResponse(reviewer); 57 | } 58 | 59 | @DeleteMapping("/reviewers/{reviewerId}") 60 | public ReviewerResponse deleteReviewer(@PathVariable Long reviewerId) { 61 | Reviewer reviewer = reviewerService.validateAndGetReviewer(reviewerId); 62 | reviewerService.deleteReviewer(reviewer); 63 | return reviewerMapper.toReviewerResponse(reviewer); 64 | } 65 | 66 | // ------- 67 | // Article 68 | 69 | @GetMapping("/articles/{articleId}") 70 | public ArticleResponse getArticle(@PathVariable Long articleId) { 71 | Article article = articleService.validateAndGetArticle(articleId); 72 | return articleMapper.toArticleResponse(article); 73 | } 74 | 75 | @ResponseStatus(HttpStatus.CREATED) 76 | @PostMapping("/articles") 77 | public ArticleResponse createArticle(@Valid @RequestBody CreateArticleRequest createArticleRequest) { 78 | Article article = articleMapper.toArticle(createArticleRequest); 79 | article = articleService.createArticle(article); 80 | return articleMapper.toArticleResponse(article); 81 | } 82 | 83 | @DeleteMapping("/articles/{articleId}") 84 | public ArticleResponse deleteArticle(@PathVariable Long articleId) { 85 | Article article = articleService.validateAndGetArticle(articleId); 86 | articleService.deleteArticle(article); 87 | return articleMapper.toArticleResponse(article); 88 | } 89 | 90 | // -------- 91 | // Comments 92 | 93 | @GetMapping("/comments/{commentId}") 94 | public CommentResponse getComment(@PathVariable Long commentId) { 95 | Comment comment = commentService.validateAndGetComment(commentId); 96 | return commentMapper.toCommentResponse(comment); 97 | } 98 | 99 | @ResponseStatus(HttpStatus.CREATED) 100 | @PostMapping("/comments") 101 | public CommentResponse createComment(@Valid @RequestBody CreateCommentRequest createCommentRequest) { 102 | Comment comment = commentMapper.toComment(createCommentRequest); 103 | comment = commentService.saveComment(comment); 104 | return commentMapper.toCommentResponse(comment); 105 | } 106 | 107 | @DeleteMapping("/comments/{commentId}") 108 | public CommentResponse deleteComment(@PathVariable Long commentId) { 109 | Comment comment = commentService.validateAndGetComment(commentId); 110 | commentService.deleteComment(comment); 111 | return commentMapper.toCommentResponse(comment); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | import java.util.Set; 4 | 5 | public record ArticleResponse(Long id, String title, Set comments) { 6 | 7 | public record Comment(Long id, String text, Reviewer reviewer) { 8 | 9 | public record Reviewer(Long id) { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/CommentResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | public record CommentResponse(Long id, Reviewer reviewer, Article article, String text) { 4 | 5 | public record Reviewer(Long id, String name) { 6 | } 7 | 8 | public record Article(Long id, String title) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/CreateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateArticleRequest( 7 | @Schema(example = "Comparison between Springboot and Play Framework") @NotBlank String title) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/CreateCommentRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | public record CreateCommentRequest( 8 | @Schema(example = "2") @NotNull Long reviewerId, 9 | @Schema(example = "1") @NotNull Long articleId, 10 | @Schema(example = "This article is very interesting") @NotBlank String text) { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/CreateReviewerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateReviewerRequest(@Schema(example = "Ivan Franchin") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/rest/dto/ReviewerResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.rest.dto; 2 | 3 | import java.util.Set; 4 | 5 | public record ReviewerResponse(Long id, String name, Set comments) { 6 | 7 | public record Comment(Long id, String text, Article article) { 8 | 9 | public record Article(Long id) { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/ArticleService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Article; 4 | 5 | public interface ArticleService { 6 | 7 | Article validateAndGetArticle(Long id); 8 | 9 | Article createArticle(Article article); 10 | 11 | void deleteArticle(Article article); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/ArticleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception.ArticleNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Article; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository.ArticleRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class ArticleServiceImpl implements ArticleService { 12 | 13 | private final ArticleRepository articleRepository; 14 | 15 | @Override 16 | public Article validateAndGetArticle(Long id) { 17 | return articleRepository.findById(id).orElseThrow(() -> new ArticleNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Article createArticle(Article article) { 22 | return articleRepository.save(article); 23 | } 24 | 25 | @Override 26 | public void deleteArticle(Article article) { 27 | articleRepository.delete(article); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Comment; 4 | 5 | public interface CommentService { 6 | 7 | Comment validateAndGetComment(Long id); 8 | 9 | Comment saveComment(Comment comment); 10 | 11 | void deleteComment(Comment comment); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception.CommentNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Comment; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository.CommentRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class CommentServiceImpl implements CommentService { 12 | 13 | private final CommentRepository commentRepository; 14 | 15 | @Override 16 | public Comment validateAndGetComment(Long id) { 17 | return commentRepository.findById(id).orElseThrow(() -> new CommentNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Comment saveComment(Comment comment) { 22 | return commentRepository.save(comment); 23 | } 24 | 25 | @Override 26 | public void deleteComment(Comment comment) { 27 | commentRepository.delete(comment); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/ReviewerService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Reviewer; 4 | 5 | public interface ReviewerService { 6 | 7 | Reviewer validateAndGetReviewer(Long id); 8 | 9 | Reviewer saveReviewer(Reviewer reviewer); 10 | 11 | void deleteReviewer(Reviewer reviewer); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/manytomany/simplepkextracolumn/service/ReviewerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.exception.ReviewerNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.model.Reviewer; 5 | import com.ivanfranchin.springdatajparelationships.manytomany.simplepkextracolumn.repository.ReviewerRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class ReviewerServiceImpl implements ReviewerService { 12 | 13 | private final ReviewerRepository reviewerRepository; 14 | 15 | @Override 16 | public Reviewer validateAndGetReviewer(Long id) { 17 | return reviewerRepository.findById(id).orElseThrow(() -> new ReviewerNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Reviewer saveReviewer(Reviewer reviewer) { 22 | return reviewerRepository.save(reviewer); 23 | } 24 | 25 | @Override 26 | public void deleteReviewer(Reviewer reviewer) { 27 | reviewerRepository.delete(reviewer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/exception/PlayerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class PlayerNotFoundException extends RuntimeException { 8 | 9 | public PlayerNotFoundException(Long id) { 10 | super(String.format("Player with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/exception/WeaponNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.exception; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.WeaponPk; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.NOT_FOUND) 8 | public class WeaponNotFoundException extends RuntimeException { 9 | 10 | public WeaponNotFoundException(WeaponPk weaponPk) { 11 | super(String.format("Weapon with id '%s' not found", weaponPk)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/mapper/PlayerMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreatePlayerRequest; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.PlayerResponse; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface PlayerMapper { 11 | 12 | @Mapping(target = "id", ignore = true) 13 | @Mapping(target = "weapons", ignore = true) 14 | Player toPlayer(CreatePlayerRequest createPlayerRequest); 15 | 16 | PlayerResponse toPlayerResponse(Player player); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/mapper/WeaponMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreateWeaponRequest; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.WeaponResponse; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface WeaponMapper { 11 | 12 | @Mapping(target = "id", ignore = true) 13 | @Mapping(target = "player", ignore = true) 14 | Weapon toWeapon(CreateWeaponRequest createWeaponRequest); 15 | 16 | WeaponResponse toWeaponResponse(Weapon weapon); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/model/Player.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "weapons") 20 | @EqualsAndHashCode(exclude = "weapons") 21 | @Entity 22 | @Table(name = "players") 23 | public class Player { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "player", cascade = CascadeType.ALL, orphanRemoval = true) 30 | private Set weapons = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String name; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/model/Weapon.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.IdClass; 10 | import jakarta.persistence.JoinColumn; 11 | import jakarta.persistence.ManyToOne; 12 | import jakarta.persistence.Table; 13 | import lombok.Data; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.ToString; 16 | 17 | @Data 18 | @ToString(exclude = "player") 19 | @EqualsAndHashCode(exclude = "player") 20 | @Entity 21 | @Table(name = "weapons") 22 | @IdClass(WeaponPk.class) 23 | public class Weapon { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @Id 30 | @ManyToOne(fetch = FetchType.LAZY) 31 | @JoinColumn(name = "player_id") 32 | private Player player; 33 | 34 | @Column(nullable = false) 35 | private String name; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/model/WeaponPk.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public record WeaponPk(Long id,Long player) implements Serializable { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/repository/PlayerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface PlayerRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/repository/WeaponRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.WeaponPk; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface WeaponRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/PlayerWeaponController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.mapper.PlayerMapper; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.mapper.WeaponMapper; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 7 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreatePlayerRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreateWeaponRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.PlayerResponse; 10 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.WeaponResponse; 11 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service.PlayerService; 12 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service.WeaponService; 13 | import jakarta.validation.Valid; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.web.bind.annotation.DeleteMapping; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.ResponseStatus; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @RequiredArgsConstructor 26 | @RestController 27 | @RequestMapping("/api/players") 28 | public class PlayerWeaponController { 29 | 30 | private final PlayerService playerService; 31 | private final WeaponService weaponService; 32 | private final PlayerMapper playerMapper; 33 | private final WeaponMapper weaponMapper; 34 | 35 | // ------ 36 | // Player 37 | 38 | @GetMapping("/{playerId}") 39 | public PlayerResponse getPlayer(@PathVariable Long playerId) { 40 | Player player = playerService.validateAndGetPlayer(playerId); 41 | return playerMapper.toPlayerResponse(player); 42 | } 43 | 44 | @ResponseStatus(HttpStatus.CREATED) 45 | @PostMapping 46 | public PlayerResponse createPlayer(@Valid @RequestBody CreatePlayerRequest createPlayerRequest) { 47 | Player player = playerMapper.toPlayer(createPlayerRequest); 48 | player = playerService.savePlayer(player); 49 | return playerMapper.toPlayerResponse(player); 50 | } 51 | 52 | @DeleteMapping("/{playerId}") 53 | public PlayerResponse deletePlayer(@PathVariable Long playerId) { 54 | Player player = playerService.validateAndGetPlayer(playerId); 55 | playerService.deletePlayer(player); 56 | return playerMapper.toPlayerResponse(player); 57 | } 58 | 59 | // ------ 60 | // Weapon 61 | 62 | @GetMapping("/{playerId}/weapons/{weaponId}") 63 | public WeaponResponse getWeapon(@PathVariable Long playerId, @PathVariable Long weaponId) { 64 | Weapon weapon = weaponService.validateAndGetWeapon(playerId, weaponId); 65 | return weaponMapper.toWeaponResponse(weapon); 66 | } 67 | 68 | @ResponseStatus(HttpStatus.CREATED) 69 | @PostMapping("/{playerId}/weapons") 70 | public WeaponResponse addWeapon(@PathVariable Long playerId, @Valid @RequestBody CreateWeaponRequest createWeaponRequest) { 71 | Player player = playerService.validateAndGetPlayer(playerId); 72 | Weapon weapon = weaponMapper.toWeapon(createWeaponRequest); 73 | 74 | // to avoid "org.hibernate.HibernateException: No part of a composite identifier may be null" 75 | // in spite of the fact that it's set a fixed value here, hibernate will generate a new value 76 | weapon.setId(1L); 77 | weapon.setPlayer(player); 78 | weapon = weaponService.saveWeapon(weapon); 79 | 80 | return weaponMapper.toWeaponResponse(weapon); 81 | } 82 | 83 | @DeleteMapping("/{playerId}/weapons/{weaponId}") 84 | public WeaponResponse removeWeapon(@PathVariable Long playerId, @PathVariable Long weaponId) { 85 | Weapon weapon = weaponService.validateAndGetWeapon(playerId, weaponId); 86 | weaponService.deleteWeapon(weapon); 87 | return weaponMapper.toWeaponResponse(weapon); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/dto/CreatePlayerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreatePlayerRequest(@Schema(example = "Ivan Franchin") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/dto/CreateWeaponRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateWeaponRequest(@Schema(example = "Machine Gun") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/dto/PlayerResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto; 2 | 3 | import java.util.List; 4 | 5 | public record PlayerResponse(Long id, String name, List weapons) { 6 | 7 | public record Weapon(Long id, String name) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/dto/WeaponResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto; 2 | 3 | public record WeaponResponse(Long id, String name) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/service/PlayerService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 4 | 5 | public interface PlayerService { 6 | 7 | Player validateAndGetPlayer(Long id); 8 | 9 | Player savePlayer(Player player); 10 | 11 | void deletePlayer(Player player); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/service/PlayerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.exception.PlayerNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository.PlayerRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class PlayerServiceImpl implements PlayerService { 12 | 13 | private final PlayerRepository playerRepository; 14 | 15 | @Override 16 | public Player validateAndGetPlayer(Long id) { 17 | return playerRepository.findById(id).orElseThrow(() -> new PlayerNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Player savePlayer(Player player) { 22 | return playerRepository.save(player); 23 | } 24 | 25 | @Override 26 | public void deletePlayer(Player player) { 27 | playerRepository.delete(player); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/service/WeaponService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 4 | 5 | public interface WeaponService { 6 | 7 | Weapon validateAndGetWeapon(Long playerId, Long weaponId); 8 | 9 | Weapon saveWeapon(Weapon weapon); 10 | 11 | void deleteWeapon(Weapon weapon); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/service/WeaponServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.exception.WeaponNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.WeaponPk; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository.WeaponRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class WeaponServiceImpl implements WeaponService { 13 | 14 | private final WeaponRepository weaponRepository; 15 | 16 | @Override 17 | public Weapon validateAndGetWeapon(Long playerId, Long weaponId) { 18 | WeaponPk weaponPk = new WeaponPk(weaponId, playerId); 19 | return weaponRepository.findById(weaponPk).orElseThrow(() -> new WeaponNotFoundException(weaponPk)); 20 | } 21 | 22 | @Override 23 | public Weapon saveWeapon(Weapon weapon) { 24 | return weaponRepository.save(weapon); 25 | } 26 | 27 | @Override 28 | public void deleteWeapon(Weapon weapon) { 29 | weaponRepository.delete(weapon); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/exception/DishNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class DishNotFoundException extends RuntimeException { 8 | 9 | public DishNotFoundException(Long dishId, Long restaurantId) { 10 | super(String.format("Restaurant with id '%s' doesn't have dish with id '%s'", restaurantId, dishId)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/exception/RestaurantNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class RestaurantNotFoundException extends RuntimeException { 8 | 9 | public RestaurantNotFoundException(Long id) { 10 | super(String.format("Restaurant with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/mapper/DishMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Dish; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.CreateDishRequest; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.DishResponse; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.UpdateDishRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface DishMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "restaurant", ignore = true) 20 | Dish toDish(CreateDishRequest createDishRequest); 21 | 22 | DishResponse toDishResponse(Dish dish); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "restaurant", ignore = true) 26 | void updateDishFromRequest(UpdateDishRequest updateDishRequest, @MappingTarget Dish dish); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/mapper/RestaurantMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Restaurant; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.CreateRestaurantRequest; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.RestaurantResponse; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.UpdateRestaurantRequest; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.MappingTarget; 10 | import org.mapstruct.NullValuePropertyMappingStrategy; 11 | 12 | @Mapper( 13 | componentModel = "spring", 14 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 15 | ) 16 | public interface RestaurantMapper { 17 | 18 | @Mapping(target = "id", ignore = true) 19 | @Mapping(target = "dishes", ignore = true) 20 | Restaurant toRestaurant(CreateRestaurantRequest createRestaurantRequest); 21 | 22 | RestaurantResponse toRestaurantResponse(Restaurant restaurant); 23 | 24 | @Mapping(target = "id", ignore = true) 25 | @Mapping(target = "dishes", ignore = true) 26 | void updateRestaurantFromRequest(UpdateRestaurantRequest updateRestaurantRequest, 27 | @MappingTarget Restaurant restaurant); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/model/Dish.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | import lombok.Data; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString(exclude = "restaurant") 18 | @EqualsAndHashCode(exclude = "restaurant") 19 | @Entity 20 | @Table(name = "dishes") 21 | public class Dish { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 25 | private Long id; 26 | 27 | @ManyToOne(fetch = FetchType.LAZY) 28 | @JoinColumn(name = "restaurant_id") 29 | private Restaurant restaurant; 30 | 31 | @Column(nullable = false) 32 | private String name; 33 | 34 | public void addRestaurant(Restaurant restaurant) { 35 | this.restaurant = restaurant; 36 | restaurant.getDishes().add(this); 37 | } 38 | 39 | public void removeRestaurant() { 40 | this.restaurant.getDishes().remove(this); 41 | this.restaurant = null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/model/Restaurant.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToMany; 10 | import jakarta.persistence.Table; 11 | import lombok.Data; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | import java.util.LinkedHashSet; 16 | import java.util.Set; 17 | 18 | @Data 19 | @ToString(exclude = "dishes") 20 | @EqualsAndHashCode(exclude = "dishes") 21 | @Entity 22 | @Table(name = "restaurants") 23 | public class Restaurant { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 27 | private Long id; 28 | 29 | @OneToMany(mappedBy = "restaurant", cascade = CascadeType.ALL, orphanRemoval = true) 30 | private Set dishes = new LinkedHashSet<>(); 31 | 32 | @Column(nullable = false) 33 | private String name; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/repository/DishRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Dish; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface DishRepository extends CrudRepository { 11 | 12 | Optional findByIdAndRestaurantId(Long id, Long restaurantId); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/repository/RestaurantRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Restaurant; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface RestaurantRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/RestaurantDishController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.mapper.DishMapper; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.mapper.RestaurantMapper; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Dish; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Restaurant; 7 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.CreateDishRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.CreateRestaurantRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.DishResponse; 10 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.RestaurantResponse; 11 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.UpdateDishRequest; 12 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto.UpdateRestaurantRequest; 13 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service.DishService; 14 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service.RestaurantService; 15 | import jakarta.validation.Valid; 16 | import lombok.RequiredArgsConstructor; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.web.bind.annotation.DeleteMapping; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PathVariable; 21 | import org.springframework.web.bind.annotation.PostMapping; 22 | import org.springframework.web.bind.annotation.PutMapping; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.ResponseStatus; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | @RequiredArgsConstructor 29 | @RestController 30 | @RequestMapping("/api/restaurants") 31 | public class RestaurantDishController { 32 | 33 | private final RestaurantService restaurantService; 34 | private final DishService dishService; 35 | private final RestaurantMapper restaurantMapper; 36 | private final DishMapper dishMapper; 37 | 38 | //----------- 39 | // Restaurant 40 | 41 | @GetMapping("/{restaurantId}") 42 | public RestaurantResponse getRestaurant(@PathVariable Long restaurantId) { 43 | Restaurant restaurant = restaurantService.validateAndGetRestaurant(restaurantId); 44 | return restaurantMapper.toRestaurantResponse(restaurant); 45 | } 46 | 47 | @ResponseStatus(HttpStatus.CREATED) 48 | @PostMapping 49 | public RestaurantResponse createRestaurant(@Valid @RequestBody CreateRestaurantRequest createRestaurantRequest) { 50 | Restaurant restaurant = restaurantMapper.toRestaurant(createRestaurantRequest); 51 | restaurant = restaurantService.saveRestaurant(restaurant); 52 | return restaurantMapper.toRestaurantResponse(restaurant); 53 | } 54 | 55 | @PutMapping("/{restaurantId}") 56 | public RestaurantResponse updateRestaurant(@PathVariable Long restaurantId, 57 | @Valid @RequestBody UpdateRestaurantRequest updateRestaurantRequest) { 58 | Restaurant restaurant = restaurantService.validateAndGetRestaurant(restaurantId); 59 | restaurantMapper.updateRestaurantFromRequest(updateRestaurantRequest, restaurant); 60 | restaurantService.saveRestaurant(restaurant); 61 | return restaurantMapper.toRestaurantResponse(restaurant); 62 | } 63 | 64 | @DeleteMapping("/{restaurantId}") 65 | public RestaurantResponse deleteRestaurant(@PathVariable Long restaurantId) { 66 | Restaurant restaurant = restaurantService.validateAndGetRestaurant(restaurantId); 67 | restaurantService.deleteRestaurant(restaurant); 68 | return restaurantMapper.toRestaurantResponse(restaurant); 69 | } 70 | 71 | //----- 72 | // Dish 73 | 74 | @GetMapping("/{restaurantId}/dishes/{dishId}") 75 | public DishResponse getDish(@PathVariable Long restaurantId, @PathVariable Long dishId) { 76 | Dish dish = dishService.validateAndGetDish(dishId, restaurantId); 77 | return dishMapper.toDishResponse(dish); 78 | } 79 | 80 | @ResponseStatus(HttpStatus.CREATED) 81 | @PostMapping("/{restaurantId}/dishes") 82 | public DishResponse createDish(@PathVariable Long restaurantId, 83 | @Valid @RequestBody CreateDishRequest createDishRequest) { 84 | Restaurant restaurant = restaurantService.validateAndGetRestaurant(restaurantId); 85 | Dish dish = dishMapper.toDish(createDishRequest); 86 | dish.addRestaurant(restaurant); 87 | dish = dishService.saveDish(dish); 88 | return dishMapper.toDishResponse(dish); 89 | } 90 | 91 | @PutMapping("/{restaurantId}/dishes/{dishId}") 92 | public DishResponse updateDish(@PathVariable Long restaurantId, 93 | @PathVariable Long dishId, 94 | @Valid @RequestBody UpdateDishRequest updateDishRequest) { 95 | Dish dish = dishService.validateAndGetDish(dishId, restaurantId); 96 | dishMapper.updateDishFromRequest(updateDishRequest, dish); 97 | dish = dishService.saveDish(dish); 98 | return dishMapper.toDishResponse(dish); 99 | } 100 | 101 | @DeleteMapping("/{restaurantId}/dishes/{dishId}") 102 | public DishResponse deleteDish(@PathVariable Long restaurantId, @PathVariable Long dishId) { 103 | Dish dish = dishService.validateAndGetDish(dishId, restaurantId); 104 | dish.removeRestaurant(); 105 | dishService.deleteDish(dish); 106 | return dishMapper.toDishResponse(dish); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/CreateDishRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateDishRequest(@Schema(example = "Pizza Salami") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/CreateRestaurantRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateRestaurantRequest(@Schema(example = "Happy Pizza") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/DishResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | public record DishResponse(Long id, String name) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/RestaurantResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | import java.util.List; 4 | 5 | public record RestaurantResponse(Long id, String name, List dishes) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/UpdateDishRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateDishRequest(@Schema(example = "Pizza Fungi") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/rest/dto/UpdateRestaurantRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateRestaurantRequest(@Schema(example = "Happy Burger") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/service/DishService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Dish; 4 | 5 | public interface DishService { 6 | 7 | Dish validateAndGetDish(Long dishId, Long restaurantId); 8 | 9 | Dish saveDish(Dish dish); 10 | 11 | void deleteDish(Dish dish); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/service/DishServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.exception.DishNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Dish; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.repository.DishRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class DishServiceImpl implements DishService { 12 | 13 | private final DishRepository dishRepository; 14 | 15 | @Override 16 | public Dish validateAndGetDish(Long dishId, Long restaurantId) { 17 | return dishRepository.findByIdAndRestaurantId(dishId, restaurantId) 18 | .orElseThrow(() -> new DishNotFoundException(dishId, restaurantId)); 19 | } 20 | 21 | @Override 22 | public Dish saveDish(Dish dish) { 23 | return dishRepository.save(dish); 24 | } 25 | 26 | @Override 27 | public void deleteDish(Dish dish) { 28 | dishRepository.delete(dish); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/service/RestaurantService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Restaurant; 4 | 5 | public interface RestaurantService { 6 | 7 | Restaurant validateAndGetRestaurant(Long id); 8 | 9 | Restaurant saveRestaurant(Restaurant restaurant); 10 | 11 | void deleteRestaurant(Restaurant restaurant); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetomany/simplepk/service/RestaurantServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.exception.RestaurantNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.model.Restaurant; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.simplepk.repository.RestaurantRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class RestaurantServiceImpl implements RestaurantService { 12 | 13 | private final RestaurantRepository restaurantRepository; 14 | 15 | @Override 16 | public Restaurant validateAndGetRestaurant(Long id) { 17 | return restaurantRepository.findById(id).orElseThrow(() -> new RestaurantNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Restaurant saveRestaurant(Restaurant restaurant) { 22 | return restaurantRepository.save(restaurant); 23 | } 24 | 25 | @Override 26 | public void deleteRestaurant(Restaurant restaurant) { 27 | restaurantRepository.delete(restaurant); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/exception/PersonNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class PersonNotFoundException extends RuntimeException { 8 | 9 | public PersonNotFoundException(Long id) { 10 | super(String.format("Person with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/mapper/PersonMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.PersonDetail; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonDetailRequest; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonRequest; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.PersonResponse; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonDetailRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonRequest; 10 | import org.mapstruct.Mapper; 11 | import org.mapstruct.Mapping; 12 | import org.mapstruct.MappingTarget; 13 | import org.mapstruct.NullValuePropertyMappingStrategy; 14 | 15 | @Mapper( 16 | componentModel = "spring", 17 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 18 | ) 19 | public interface PersonMapper { 20 | 21 | @Mapping(target = "id", ignore = true) 22 | @Mapping(target = "personDetail", ignore = true) 23 | Person toPerson(CreatePersonRequest createPersonRequest); 24 | 25 | PersonResponse toPersonResponse(Person person); 26 | 27 | @Mapping(target = "id", ignore = true) 28 | @Mapping(target = "personDetail", ignore = true) 29 | void updatePersonFromRequest(UpdatePersonRequest updatePersonRequest, @MappingTarget Person person); 30 | 31 | @Mapping(target = "id", ignore = true) 32 | @Mapping(target = "person", ignore = true) 33 | PersonDetail toPersonDetail(CreatePersonDetailRequest createPersonDetailRequest); 34 | 35 | @Mapping(target = "id", ignore = true) 36 | @Mapping(target = "person", ignore = true) 37 | void updatePersonDetailFromRequest(UpdatePersonDetailRequest updatePersonDetailRequest, 38 | @MappingTarget PersonDetail personDetail); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/model/Person.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.FetchType; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.OneToOne; 11 | import jakarta.persistence.Table; 12 | import lombok.Data; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString(exclude = "personDetail") 18 | @EqualsAndHashCode(exclude = "personDetail") 19 | @Entity 20 | @Table(name = "persons") 21 | public class Person { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 25 | private Long id; 26 | 27 | @OneToOne(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 28 | private PersonDetail personDetail; 29 | 30 | @Column(nullable = false) 31 | private String name; 32 | 33 | public void addPersonDetail(PersonDetail personDetail) { 34 | this.personDetail = personDetail; 35 | personDetail.setPerson(this); 36 | } 37 | 38 | public void removePersonDetail() { 39 | this.personDetail.setPerson(null); 40 | this.personDetail = null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/model/PersonDetail.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.MapsId; 8 | import jakarta.persistence.OneToOne; 9 | import jakarta.persistence.Table; 10 | import lombok.Data; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | 14 | @Data 15 | @ToString(exclude = "person") 16 | @EqualsAndHashCode(exclude = "person") 17 | @Entity 18 | @Table(name = "person_details") 19 | public class PersonDetail { 20 | 21 | @Id 22 | private Long id; 23 | 24 | @OneToOne(fetch = FetchType.LAZY) 25 | @MapsId 26 | private Person person; 27 | 28 | @Column(nullable = false) 29 | private String description; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface PersonRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/PersonDetailController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.mapper.PersonMapper; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.PersonDetail; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonDetailRequest; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.PersonResponse; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonDetailRequest; 10 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonRequest; 11 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.service.PersonService; 12 | import jakarta.validation.Valid; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.web.bind.annotation.DeleteMapping; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.bind.annotation.PutMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.ResponseStatus; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @RequiredArgsConstructor 26 | @RestController 27 | @RequestMapping("/api/persons") 28 | public class PersonDetailController { 29 | 30 | private final PersonService personService; 31 | private final PersonMapper personMapper; 32 | 33 | @GetMapping("/{personId}") 34 | public PersonResponse getPerson(@PathVariable Long personId) { 35 | Person person = personService.validateAndGetPerson(personId); 36 | return personMapper.toPersonResponse(person); 37 | } 38 | 39 | @ResponseStatus(HttpStatus.CREATED) 40 | @PostMapping 41 | public PersonResponse createPerson(@Valid @RequestBody CreatePersonRequest createPersonRequest) { 42 | Person person = personMapper.toPerson(createPersonRequest); 43 | person = personService.savePerson(person); 44 | return personMapper.toPersonResponse(person); 45 | } 46 | 47 | @PutMapping("/{personId}") 48 | public PersonResponse updatePerson(@PathVariable Long personId, 49 | @Valid @RequestBody UpdatePersonRequest updatePersonRequest) { 50 | Person person = personService.validateAndGetPerson(personId); 51 | personMapper.updatePersonFromRequest(updatePersonRequest, person); 52 | person = personService.savePerson(person); 53 | return personMapper.toPersonResponse(person); 54 | } 55 | 56 | @DeleteMapping("/{personId}") 57 | public PersonResponse deletePerson(@PathVariable Long personId) { 58 | Person person = personService.validateAndGetPerson(personId); 59 | personService.deletePerson(person); 60 | return personMapper.toPersonResponse(person); 61 | } 62 | 63 | @PostMapping("/{personId}/person-details") 64 | public PersonResponse addPersonDetail(@PathVariable Long personId, 65 | @Valid @RequestBody CreatePersonDetailRequest createPersonDetailRequest) { 66 | Person person = personService.validateAndGetPerson(personId); 67 | PersonDetail personDetail = personMapper.toPersonDetail(createPersonDetailRequest); 68 | person.addPersonDetail(personDetail); 69 | person = personService.savePerson(person); 70 | return personMapper.toPersonResponse(person); 71 | } 72 | 73 | @PutMapping("/{personId}/person-details") 74 | public PersonResponse updatePersonDetail(@PathVariable Long personId, 75 | @Valid @RequestBody UpdatePersonDetailRequest updatePersonDetailRequest) { 76 | Person person = personService.validateAndGetPerson(personId); 77 | PersonDetail personDetail = person.getPersonDetail(); 78 | personMapper.updatePersonDetailFromRequest(updatePersonDetailRequest, personDetail); 79 | person = personService.savePerson(person); 80 | return personMapper.toPersonResponse(person); 81 | } 82 | 83 | // Hibernate doesn't allow to delete the person-details 84 | @DeleteMapping("/{personId}/person-details") 85 | public PersonResponse deletePersonDetail(@PathVariable Long personId) { 86 | Person person = personService.validateAndGetPerson(personId); 87 | person.removePersonDetail(); 88 | person = personService.savePerson(person); 89 | return personMapper.toPersonResponse(person); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/CreatePersonDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreatePersonDetailRequest( 7 | @Schema(example = "More information about the person") @NotBlank String description) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/CreatePersonRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreatePersonRequest(@Schema(example = "Ivan Franchin") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/PersonDetailResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | public record PersonDetailResponse(Long id, String description) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/PersonResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | public record PersonResponse(Long id, String name, PersonDetailResponse personDetail) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/UpdatePersonDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdatePersonDetailRequest(@Schema(example = "New information about the person") String description) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/dto/UpdatePersonRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdatePersonRequest(@Schema(example = "Ivan Franchin 2") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/service/PersonService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 4 | 5 | public interface PersonService { 6 | 7 | Person validateAndGetPerson(Long id); 8 | 9 | Person savePerson(Person person); 10 | 11 | void deletePerson(Person person); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/service/PersonServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.exception.PersonNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.repository.PersonRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class PersonServiceImpl implements PersonService { 12 | 13 | private final PersonRepository personRepository; 14 | 15 | @Override 16 | public Person validateAndGetPerson(Long id) { 17 | return personRepository.findById(id).orElseThrow(() -> new PersonNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Person savePerson(Person person) { 22 | return personRepository.save(person); 23 | } 24 | 25 | @Override 26 | public void deletePerson(Person person) { 27 | personRepository.delete(person); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/exception/TeamNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class TeamNotFoundException extends RuntimeException { 8 | 9 | public TeamNotFoundException(Long id) { 10 | super(String.format("Team with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/mapper/TeamMapper.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.mapper; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.TeamDetail; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamDetailRequest; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamRequest; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.TeamResponse; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamDetailRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamRequest; 10 | import org.mapstruct.Mapper; 11 | import org.mapstruct.Mapping; 12 | import org.mapstruct.MappingTarget; 13 | import org.mapstruct.NullValuePropertyMappingStrategy; 14 | 15 | @Mapper( 16 | componentModel = "spring", 17 | nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE 18 | ) 19 | public interface TeamMapper { 20 | 21 | @Mapping(target = "id", ignore = true) 22 | @Mapping(target = "teamDetail", ignore = true) 23 | Team toTeam(CreateTeamRequest createTeamRequest); 24 | 25 | TeamResponse toTeamResponse(Team team); 26 | 27 | @Mapping(target = "id", ignore = true) 28 | @Mapping(target = "teamDetail", ignore = true) 29 | void updateTeamFromRequest(UpdateTeamRequest updateTeamRequest, @MappingTarget Team team); 30 | 31 | @Mapping(target = "id", ignore = true) 32 | @Mapping(target = "team", ignore = true) 33 | TeamDetail toTeamDetail(CreateTeamDetailRequest createTeamDetailRequest); 34 | 35 | @Mapping(target = "id", ignore = true) 36 | @Mapping(target = "team", ignore = true) 37 | void updateTeamDetailFromRequest(UpdateTeamDetailRequest updateTeamDetailRequest, 38 | @MappingTarget TeamDetail teamDetail); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/model/Team.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.FetchType; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.OneToOne; 11 | import jakarta.persistence.Table; 12 | import lombok.Data; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString(exclude = "teamDetail") 18 | @EqualsAndHashCode(exclude = "teamDetail") 19 | @Entity 20 | @Table(name = "teams") 21 | public class Team { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 25 | private Long id; 26 | 27 | @OneToOne(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 28 | private TeamDetail teamDetail; 29 | 30 | @Column(nullable = false) 31 | private String name; 32 | 33 | public void addTeamDetail(TeamDetail teamDetail) { 34 | this.teamDetail = teamDetail; 35 | teamDetail.setTeam(this); 36 | } 37 | 38 | public void removeTeamDetail() { 39 | this.teamDetail.setTeam(null); 40 | this.teamDetail = null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/model/TeamDetail.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.OneToOne; 11 | import jakarta.persistence.Table; 12 | import lombok.Data; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString(exclude = "team") 18 | @EqualsAndHashCode(exclude = "team") 19 | @Entity 20 | @Table(name = "team_details") 21 | public class TeamDetail { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 25 | private Long id; 26 | 27 | @OneToOne(fetch = FetchType.LAZY) 28 | @JoinColumn(name = "team_id") 29 | private Team team; 30 | 31 | @Column(nullable = false) 32 | private String description; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/repository/TeamRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.repository; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface TeamRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/TeamDetailController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.mapper.TeamMapper; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.TeamDetail; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamDetailRequest; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.TeamResponse; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamDetailRequest; 10 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamRequest; 11 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.service.TeamService; 12 | import jakarta.validation.Valid; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.web.bind.annotation.DeleteMapping; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.bind.annotation.PutMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.ResponseStatus; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @RequiredArgsConstructor 26 | @RestController 27 | @RequestMapping("/api/teams") 28 | public class TeamDetailController { 29 | 30 | private final TeamService teamService; 31 | private final TeamMapper teamMapper; 32 | 33 | @GetMapping("/{teamId}") 34 | public TeamResponse getTeam(@PathVariable Long teamId) { 35 | Team team = teamService.validateAndGetTeam(teamId); 36 | return teamMapper.toTeamResponse(team); 37 | } 38 | 39 | @ResponseStatus(HttpStatus.CREATED) 40 | @PostMapping 41 | public TeamResponse createTeam(@Valid @RequestBody CreateTeamRequest createTeamRequest) { 42 | Team team = teamMapper.toTeam(createTeamRequest); 43 | team = teamService.saveTeam(team); 44 | return teamMapper.toTeamResponse(team); 45 | } 46 | 47 | @PutMapping("/{teamId}") 48 | public TeamResponse updateTeam(@PathVariable Long teamId, @Valid @RequestBody UpdateTeamRequest updateTeamRequest) { 49 | Team team = teamService.validateAndGetTeam(teamId); 50 | teamMapper.updateTeamFromRequest(updateTeamRequest, team); 51 | teamService.saveTeam(team); 52 | return teamMapper.toTeamResponse(team); 53 | } 54 | 55 | @DeleteMapping("/{teamId}") 56 | public TeamResponse deleteTeam(@PathVariable Long teamId) { 57 | Team team = teamService.validateAndGetTeam(teamId); 58 | teamService.deleteTeam(team); 59 | return teamMapper.toTeamResponse(team); 60 | } 61 | 62 | @ResponseStatus(HttpStatus.CREATED) 63 | @PostMapping("/{teamId}/team-details") 64 | public TeamResponse addTeamDetail(@PathVariable Long teamId, 65 | @Valid @RequestBody CreateTeamDetailRequest createTeamDetailRequest) { 66 | Team team = teamService.validateAndGetTeam(teamId); 67 | TeamDetail teamDetail = teamMapper.toTeamDetail(createTeamDetailRequest); 68 | team.addTeamDetail(teamDetail); 69 | team = teamService.saveTeam(team); 70 | return teamMapper.toTeamResponse(team); 71 | } 72 | 73 | @PutMapping("/{teamId}/team-details") 74 | public TeamResponse updateTeamDetail(@PathVariable Long teamId, 75 | @Valid @RequestBody UpdateTeamDetailRequest updateTeamDetailRequest) { 76 | Team team = teamService.validateAndGetTeam(teamId); 77 | TeamDetail teamDetail = team.getTeamDetail(); 78 | teamMapper.updateTeamDetailFromRequest(updateTeamDetailRequest, teamDetail); 79 | team = teamService.saveTeam(team); 80 | return teamMapper.toTeamResponse(team); 81 | } 82 | 83 | @DeleteMapping("/{teamId}/team-details") 84 | public TeamResponse deleteTeamDetail(@PathVariable Long teamId) { 85 | Team team = teamService.validateAndGetTeam(teamId); 86 | team.removeTeamDetail(); 87 | team = teamService.saveTeam(team); 88 | return teamMapper.toTeamResponse(team); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/CreateTeamDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateTeamDetailRequest(@Schema(example = "This team is awesome") @NotBlank String description) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/CreateTeamRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateTeamRequest(@Schema(example = "Team White") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/TeamDetailResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | public record TeamDetailResponse(Long id, String description) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/TeamResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | public record TeamResponse(Long id, String name, TeamDetailResponse teamDetail) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/UpdateTeamDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateTeamDetailRequest(@Schema(example = "This team is excellent") String description) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/dto/UpdateTeamRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateTeamRequest(@Schema(example = "Black Team") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/service/TeamService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 4 | 5 | public interface TeamService { 6 | 7 | Team validateAndGetTeam(Long id); 8 | 9 | Team saveTeam(Team team); 10 | 11 | void deleteTeam(Team team); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/service/TeamServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.service; 2 | 3 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.exception.TeamNotFoundException; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.repository.TeamRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class TeamServiceImpl implements TeamService { 12 | 13 | private final TeamRepository teamRepository; 14 | 15 | @Override 16 | public Team validateAndGetTeam(Long id) { 17 | return teamRepository.findById(id).orElseThrow(() -> new TeamNotFoundException(id)); 18 | } 19 | 20 | @Override 21 | public Team saveTeam(Team team) { 22 | return teamRepository.save(team); 23 | } 24 | 25 | @Override 26 | public void deleteTeam(Team team) { 27 | teamRepository.delete(team); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=spring-data-jpa-relationships 2 | 3 | spring.jpa.hibernate.ddl-auto=update 4 | 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/jparelationshipsdb 6 | spring.datasource.username=postgres 7 | spring.datasource.password=postgres 8 | 9 | springdoc.swagger-ui.disable-swagger-default-url=true 10 | 11 | #logging.level.org.hibernate.SQL=DEBUG 12 | #logging.level.org.hibernate.orm.jdbc.bind=TRACE 13 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _ _ _ _ _ _ 2 | ___ _ __ _ __(_)_ __ __ _ __| | __ _| |_ __ _ (_)_ __ __ _ _ __ ___| | __ _| |_(_) ___ _ __ ___| |__ (_)_ __ ___ 3 | / __| '_ \| '__| | '_ \ / _` |_____ / _` |/ _` | __/ _` |_____| | '_ \ / _` |_____| '__/ _ \ |/ _` | __| |/ _ \| '_ \/ __| '_ \| | '_ \/ __| 4 | \__ \ |_) | | | | | | | (_| |_____| (_| | (_| | || (_| |_____| | |_) | (_| |_____| | | __/ | (_| | |_| | (_) | | | \__ \ | | | | |_) \__ \ 5 | |___/ .__/|_| |_|_| |_|\__, | \__,_|\__,_|\__\__,_| _/ | .__/ \__,_| |_| \___|_|\__,_|\__|_|\___/|_| |_|___/_| |_|_| .__/|___/ 6 | |_| |___/ |__/|_| |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /src/test/java/com/ivanfranchin/springdatajparelationships/MyContainers.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships; 2 | 3 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 4 | import org.testcontainers.containers.PostgreSQLContainer; 5 | import org.testcontainers.junit.jupiter.Container; 6 | 7 | public interface MyContainers { 8 | 9 | @Container 10 | @ServiceConnection 11 | PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:17.2"); 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/ivanfranchin/springdatajparelationships/onetomany/compositepk/rest/PlayerWeaponControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.MyContainers; 4 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Player; 5 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.Weapon; 6 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.model.WeaponPk; 7 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository.PlayerRepository; 8 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.repository.WeaponRepository; 9 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreatePlayerRequest; 10 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.CreateWeaponRequest; 11 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.PlayerResponse; 12 | import com.ivanfranchin.springdatajparelationships.onetomany.compositepk.rest.dto.WeaponResponse; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.web.client.TestRestTemplate; 18 | import org.springframework.boot.testcontainers.context.ImportTestcontainers; 19 | import org.springframework.http.HttpMethod; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.http.ResponseEntity; 22 | 23 | import java.util.Optional; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | @SpringBootTest( 28 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 29 | properties = "spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true" 30 | ) 31 | @ImportTestcontainers(MyContainers.class) 32 | class PlayerWeaponControllerTest implements MyContainers { 33 | 34 | @Autowired 35 | private TestRestTemplate testRestTemplate; 36 | 37 | @Autowired 38 | private PlayerRepository playerRepository; 39 | 40 | @Autowired 41 | private WeaponRepository weaponRepository; 42 | 43 | @BeforeEach 44 | void setUp() { 45 | playerRepository.deleteAll(); 46 | weaponRepository.deleteAll(); 47 | } 48 | 49 | @Test 50 | void testGetPlayer() { 51 | Player player = playerRepository.save(getDefaultPlayer()); 52 | 53 | String url = String.format(API_PLAYERS_PLAYER_ID_URL, player.getId()); 54 | ResponseEntity responseEntity = testRestTemplate.getForEntity(url, PlayerResponse.class); 55 | 56 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 57 | assertThat(responseEntity.getBody()).isNotNull(); 58 | assertThat(responseEntity.getBody().id()).isEqualTo(player.getId()); 59 | assertThat(responseEntity.getBody().name()).isEqualTo(player.getName()); 60 | assertThat(responseEntity.getBody().weapons().size()).isEqualTo(0); 61 | } 62 | 63 | @Test 64 | void testCreatePlayer() { 65 | CreatePlayerRequest createPlayerRequest = new CreatePlayerRequest("Ivan Franchin"); 66 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 67 | API_PLAYERS_URL, createPlayerRequest, PlayerResponse.class); 68 | 69 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 70 | assertThat(responseEntity.getBody()).isNotNull(); 71 | assertThat(responseEntity.getBody().id()).isNotNull(); 72 | assertThat(responseEntity.getBody().name()).isEqualTo(createPlayerRequest.name()); 73 | assertThat(responseEntity.getBody().weapons().size()).isEqualTo(0); 74 | 75 | Optional playerOptional = playerRepository.findById(responseEntity.getBody().id()); 76 | assertThat(playerOptional.isPresent()).isTrue(); 77 | playerOptional.ifPresent(p -> assertThat(p.getName()).isEqualTo(createPlayerRequest.name())); 78 | } 79 | 80 | @Test 81 | void testDeletePlayer() { 82 | Player player = playerRepository.save(getDefaultPlayer()); 83 | 84 | String url = String.format("/api/players/%s", player.getId()); 85 | ResponseEntity responseEntity = testRestTemplate.exchange( 86 | url, HttpMethod.DELETE, null, PlayerResponse.class); 87 | 88 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 89 | assertThat(responseEntity.getBody()).isNotNull(); 90 | assertThat(responseEntity.getBody().id()).isEqualTo(player.getId()); 91 | assertThat(responseEntity.getBody().name()).isEqualTo(player.getName()); 92 | assertThat(responseEntity.getBody().weapons().size()).isEqualTo(0); 93 | 94 | Optional playerOptional = playerRepository.findById(player.getId()); 95 | assertThat(playerOptional.isPresent()).isFalse(); 96 | } 97 | 98 | @Test 99 | void testGetWeapon() { 100 | Player player = playerRepository.save(getDefaultPlayer()); 101 | 102 | Weapon weapon = getDefaultWeapon(); 103 | weapon.setPlayer(player); 104 | weapon = weaponRepository.save(weapon); 105 | 106 | String url = String.format(API_PLAYERS_PLAYER_ID_WEAPONS_WEAPON_ID_URL, player.getId(), weapon.getId()); 107 | ResponseEntity responseEntity = testRestTemplate.getForEntity(url, WeaponResponse.class); 108 | 109 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 110 | assertThat(responseEntity.getBody()).isNotNull(); 111 | assertThat(responseEntity.getBody().id()).isEqualTo(weapon.getId()); 112 | assertThat(responseEntity.getBody().name()).isEqualTo(weapon.getName()); 113 | } 114 | 115 | @Test 116 | void testAddWeapon() { 117 | Player player = playerRepository.save(getDefaultPlayer()); 118 | CreateWeaponRequest createWeaponRequest = new CreateWeaponRequest("Machine Gun"); 119 | 120 | String url = String.format(API_PLAYERS_PLAYER_ID_WEAPONS_URL, player.getId()); 121 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 122 | url, createWeaponRequest, WeaponResponse.class); 123 | 124 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 125 | assertThat(responseEntity.getBody()).isNotNull(); 126 | assertThat(responseEntity.getBody().id()).isNotNull(); 127 | assertThat(responseEntity.getBody().name()).isEqualTo(createWeaponRequest.name()); 128 | 129 | Optional weaponOptional = weaponRepository.findById( 130 | new WeaponPk(responseEntity.getBody().id(), player.getId())); 131 | assertThat(weaponOptional.isPresent()).isTrue(); 132 | weaponOptional.ifPresent(w -> assertThat(w.getPlayer().getId()).isEqualTo(player.getId())); 133 | 134 | Optional playerOptional = playerRepository.findById(player.getId()); 135 | assertThat(playerOptional.isPresent()).isTrue(); 136 | playerOptional.ifPresent(p -> { 137 | assertThat(p.getWeapons().size()).isEqualTo(1); 138 | weaponOptional.ifPresent(w -> assertThat(p.getWeapons().contains(w)).isTrue()); 139 | }); 140 | } 141 | 142 | @Test 143 | void testRemoveWeapon() { 144 | Player player = playerRepository.save(getDefaultPlayer()); 145 | 146 | Weapon weaponAux = getDefaultWeapon(); 147 | weaponAux.setPlayer(player); 148 | final Weapon weapon = weaponRepository.save(weaponAux); 149 | 150 | String url = String.format(API_PLAYERS_PLAYER_ID_WEAPONS_WEAPON_ID_URL, player.getId(), weapon.getId()); 151 | ResponseEntity responseEntity = testRestTemplate.exchange( 152 | url, HttpMethod.DELETE, null, WeaponResponse.class); 153 | 154 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 155 | assertThat(responseEntity.getBody()).isNotNull(); 156 | assertThat(responseEntity.getBody().id()).isEqualTo(weapon.getId()); 157 | assertThat(responseEntity.getBody().name()).isEqualTo(weapon.getName()); 158 | 159 | Optional weaponOptional = weaponRepository.findById(new WeaponPk(player.getId(), weapon.getId())); 160 | assertThat(weaponOptional.isPresent()).isFalse(); 161 | 162 | Optional playerOptional = playerRepository.findById(player.getId()); 163 | assertThat(playerOptional.isPresent()).isTrue(); 164 | playerOptional.ifPresent(p -> { 165 | assertThat(p.getWeapons().size()).isEqualTo(0); 166 | assertThat(p.getWeapons().contains(weapon)).isFalse(); 167 | }); 168 | } 169 | 170 | private Player getDefaultPlayer() { 171 | Player player = new Player(); 172 | player.setName("Ivan Franchin"); 173 | return player; 174 | } 175 | 176 | private Weapon getDefaultWeapon() { 177 | Weapon weapon = new Weapon(); 178 | weapon.setId(1L); 179 | weapon.setName("Machine Gun"); 180 | return weapon; 181 | } 182 | 183 | private static final String API_PLAYERS_URL = "/api/players"; 184 | private static final String API_PLAYERS_PLAYER_ID_URL = "/api/players/%s"; 185 | private static final String API_PLAYERS_PLAYER_ID_WEAPONS_URL = "/api/players/%s/weapons"; 186 | private static final String API_PLAYERS_PLAYER_ID_WEAPONS_WEAPON_ID_URL = "/api/players/%s/weapons/%s"; 187 | 188 | } -------------------------------------------------------------------------------- /src/test/java/com/ivanfranchin/springdatajparelationships/onetoone/sharedpk/rest/PersonDetailControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.MyContainers; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.Person; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.model.PersonDetail; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.repository.PersonRepository; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonDetailRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.CreatePersonRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.PersonResponse; 10 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonDetailRequest; 11 | import com.ivanfranchin.springdatajparelationships.onetoone.sharedpk.rest.dto.UpdatePersonRequest; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Disabled; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.web.client.TestRestTemplate; 18 | import org.springframework.boot.testcontainers.context.ImportTestcontainers; 19 | import org.springframework.http.HttpEntity; 20 | import org.springframework.http.HttpMethod; 21 | import org.springframework.http.HttpStatus; 22 | import org.springframework.http.ResponseEntity; 23 | 24 | import java.util.Optional; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 29 | @ImportTestcontainers(MyContainers.class) 30 | class PersonDetailControllerTest implements MyContainers { 31 | 32 | @Autowired 33 | private TestRestTemplate testRestTemplate; 34 | 35 | @Autowired 36 | private PersonRepository personRepository; 37 | 38 | @BeforeEach 39 | void setUp() { 40 | personRepository.deleteAll(); 41 | } 42 | 43 | @Test 44 | void testGetPerson() { 45 | Person person = personRepository.save(getDefaultPerson()); 46 | 47 | String url = String.format(API_PERSONS_PERSON_ID_URL, person.getId()); 48 | ResponseEntity responseEntity = testRestTemplate.getForEntity(url, PersonResponse.class); 49 | 50 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 51 | assertThat(responseEntity.getBody()).isNotNull(); 52 | assertThat(responseEntity.getBody().id()).isEqualTo(person.getId()); 53 | assertThat(responseEntity.getBody().name()).isEqualTo(person.getName()); 54 | assertThat(responseEntity.getBody().personDetail()).isNull(); 55 | } 56 | 57 | @Test 58 | void testCreatePerson() { 59 | CreatePersonRequest createPersonRequest = new CreatePersonRequest("Ivan Franchin"); 60 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 61 | API_PERSONS_URL, createPersonRequest, PersonResponse.class); 62 | 63 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 64 | assertThat(responseEntity.getBody()).isNotNull(); 65 | assertThat(responseEntity.getBody().id()).isNotNull(); 66 | assertThat(responseEntity.getBody().name()).isEqualTo(createPersonRequest.name()); 67 | assertThat(responseEntity.getBody().personDetail()).isNull(); 68 | 69 | Optional personOptional = personRepository.findById(responseEntity.getBody().id()); 70 | assertThat(personOptional.isPresent()).isTrue(); 71 | personOptional.ifPresent(p -> assertThat(p.getName()).isEqualTo(createPersonRequest.name())); 72 | } 73 | 74 | @Test 75 | void testUpdatePerson() { 76 | Person person = personRepository.save(getDefaultPerson()); 77 | UpdatePersonRequest updatePersonRequest = new UpdatePersonRequest("Ivan Franchin 2"); 78 | 79 | HttpEntity requestUpdate = new HttpEntity<>(updatePersonRequest); 80 | String url = String.format(API_PERSONS_PERSON_ID_URL, person.getId()); 81 | ResponseEntity responseEntity = testRestTemplate.exchange( 82 | url, HttpMethod.PUT, requestUpdate, PersonResponse.class); 83 | 84 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 85 | assertThat(responseEntity.getBody()).isNotNull(); 86 | assertThat(responseEntity.getBody().name()).isEqualTo(updatePersonRequest.name()); 87 | 88 | Optional personOptional = personRepository.findById(person.getId()); 89 | assertThat(personOptional.isPresent()).isTrue(); 90 | personOptional.ifPresent(p -> assertThat(p.getName()).isEqualTo(updatePersonRequest.name())); 91 | } 92 | 93 | @Test 94 | void testDeletePerson() { 95 | Person person = personRepository.save(getDefaultPerson()); 96 | 97 | String url = String.format(API_PERSONS_PERSON_ID_URL, person.getId()); 98 | ResponseEntity responseEntity = testRestTemplate.exchange( 99 | url, HttpMethod.DELETE, null, PersonResponse.class); 100 | 101 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 102 | assertThat(responseEntity.getBody()).isNotNull(); 103 | assertThat(responseEntity.getBody().id()).isEqualTo(person.getId()); 104 | assertThat(responseEntity.getBody().name()).isEqualTo(person.getName()); 105 | assertThat(responseEntity.getBody().personDetail()).isNull(); 106 | 107 | Optional personOptional = personRepository.findById(person.getId()); 108 | assertThat(personOptional.isPresent()).isFalse(); 109 | } 110 | 111 | @Test 112 | void testAddPersonDetail() { 113 | Person person = personRepository.save(getDefaultPerson()); 114 | CreatePersonDetailRequest createPersonDetailRequest = new CreatePersonDetailRequest("More information about the person"); 115 | 116 | String url = String.format(API_PERSONS_PERSON_ID_PERSON_DETAILS_URL, person.getId()); 117 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 118 | url, createPersonDetailRequest, PersonResponse.class); 119 | 120 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 121 | assertThat(responseEntity.getBody()).isNotNull(); 122 | assertThat(responseEntity.getBody().personDetail()).isNotNull(); 123 | assertThat(responseEntity.getBody().personDetail().id()).isEqualTo(person.getId()); 124 | assertThat(responseEntity.getBody().personDetail().description()) 125 | .isEqualTo(createPersonDetailRequest.description()); 126 | 127 | Optional personOptional = personRepository.findById(responseEntity.getBody().id()); 128 | assertThat(personOptional.isPresent()).isTrue(); 129 | personOptional.ifPresent(p -> { 130 | assertThat(p.getPersonDetail().getId()).isEqualTo(person.getId()); 131 | assertThat(p.getPersonDetail().getDescription()).isEqualTo(createPersonDetailRequest.description()); 132 | }); 133 | } 134 | 135 | @Test 136 | void testUpdatePersonDetail() { 137 | Person person = getDefaultPerson(); 138 | person.addPersonDetail(getDefaultPersonDetail()); 139 | person = personRepository.save(person); 140 | 141 | UpdatePersonDetailRequest updatePersonDetailRequest = new UpdatePersonDetailRequest("New information about the person"); 142 | 143 | HttpEntity requestUpdate = new HttpEntity<>(updatePersonDetailRequest); 144 | String url = String.format(API_PERSONS_PERSON_ID_PERSON_DETAILS_URL, person.getId()); 145 | ResponseEntity responseEntity = testRestTemplate.exchange( 146 | url, HttpMethod.PUT, requestUpdate, PersonResponse.class); 147 | 148 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 149 | assertThat(responseEntity.getBody()).isNotNull(); 150 | assertThat(responseEntity.getBody().personDetail()).isNotNull(); 151 | assertThat(responseEntity.getBody().personDetail().description()) 152 | .isEqualTo(updatePersonDetailRequest.description()); 153 | 154 | Optional personOptional = personRepository.findById(person.getId()); 155 | assertThat(personOptional.isPresent()).isTrue(); 156 | personOptional.ifPresent(p -> { 157 | assertThat(p.getPersonDetail()).isNotNull(); 158 | assertThat(p.getPersonDetail().getDescription()).isEqualTo(updatePersonDetailRequest.description()); 159 | }); 160 | } 161 | 162 | @Disabled 163 | // Hibernate doesn't allow to delete the person-details 164 | @Test 165 | void testDeletePersonDetail() { 166 | Person person = getDefaultPerson(); 167 | PersonDetail personDetail = getDefaultPersonDetail(); 168 | person.addPersonDetail(personDetail); 169 | person = personRepository.save(person); 170 | 171 | String url = String.format(API_PERSONS_PERSON_ID_PERSON_DETAILS_URL, person.getId()); 172 | ResponseEntity responseEntity = testRestTemplate.exchange( 173 | url, HttpMethod.DELETE, null, PersonResponse.class); 174 | 175 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 176 | assertThat(responseEntity.getBody()).isNotNull(); 177 | assertThat(responseEntity.getBody().personDetail()).isNull(); 178 | 179 | Optional personOptional = personRepository.findById(person.getId()); 180 | assertThat(personOptional.isPresent()).isTrue(); 181 | personOptional.ifPresent(p -> assertThat(p.getPersonDetail()).isNull()); 182 | } 183 | 184 | private Person getDefaultPerson() { 185 | Person person = new Person(); 186 | person.setName("Ivan Franchin"); 187 | return person; 188 | } 189 | 190 | private PersonDetail getDefaultPersonDetail() { 191 | PersonDetail personDetail = new PersonDetail(); 192 | personDetail.setDescription("More information about the person"); 193 | return personDetail; 194 | } 195 | 196 | private static final String API_PERSONS_URL = "/api/persons"; 197 | private static final String API_PERSONS_PERSON_ID_URL = "/api/persons/%s"; 198 | private static final String API_PERSONS_PERSON_ID_PERSON_DETAILS_URL = "/api/persons/%s/person-details"; 199 | 200 | } -------------------------------------------------------------------------------- /src/test/java/com/ivanfranchin/springdatajparelationships/onetoone/simplepk/rest/TeamDetailControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest; 2 | 3 | import com.ivanfranchin.springdatajparelationships.MyContainers; 4 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.Team; 5 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.model.TeamDetail; 6 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.repository.TeamRepository; 7 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamDetailRequest; 8 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.CreateTeamRequest; 9 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.TeamResponse; 10 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamDetailRequest; 11 | import com.ivanfranchin.springdatajparelationships.onetoone.simplepk.rest.dto.UpdateTeamRequest; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.boot.test.web.client.TestRestTemplate; 17 | import org.springframework.boot.testcontainers.context.ImportTestcontainers; 18 | import org.springframework.http.HttpEntity; 19 | import org.springframework.http.HttpMethod; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.http.ResponseEntity; 22 | 23 | import java.util.Optional; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 28 | @ImportTestcontainers(MyContainers.class) 29 | class TeamDetailControllerTest implements MyContainers { 30 | 31 | @Autowired 32 | private TestRestTemplate testRestTemplate; 33 | 34 | @Autowired 35 | private TeamRepository teamRepository; 36 | 37 | @BeforeEach 38 | void setUp() { 39 | teamRepository.deleteAll(); 40 | } 41 | 42 | @Test 43 | void testGetTeam() { 44 | Team team = teamRepository.save(getDefaultTeam()); 45 | 46 | String url = String.format(API_TEAMS_TEAM_ID_URL, team.getId()); 47 | ResponseEntity responseEntity = testRestTemplate.getForEntity(url, TeamResponse.class); 48 | 49 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 50 | assertThat(responseEntity.getBody()).isNotNull(); 51 | assertThat(responseEntity.getBody().id()).isEqualTo(team.getId()); 52 | assertThat(responseEntity.getBody().name()).isEqualTo(team.getName()); 53 | assertThat(responseEntity.getBody().teamDetail()).isNull(); 54 | } 55 | 56 | @Test 57 | void testCreateTeam() { 58 | CreateTeamRequest createTeamRequest = new CreateTeamRequest("White Team"); 59 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 60 | API_TEAMS_URL, createTeamRequest, TeamResponse.class); 61 | 62 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 63 | assertThat(responseEntity.getBody()).isNotNull(); 64 | assertThat(responseEntity.getBody().id()).isNotNull(); 65 | assertThat(responseEntity.getBody().name()).isEqualTo(createTeamRequest.name()); 66 | assertThat(responseEntity.getBody().teamDetail()).isNull(); 67 | 68 | Optional teamOptional = teamRepository.findById(responseEntity.getBody().id()); 69 | assertThat(teamOptional.isPresent()).isTrue(); 70 | teamOptional.ifPresent(t -> assertThat(t.getName()).isEqualTo(createTeamRequest.name())); 71 | } 72 | 73 | @Test 74 | void testUpdateTeam() { 75 | Team team = teamRepository.save(getDefaultTeam()); 76 | UpdateTeamRequest updateTeamRequest = new UpdateTeamRequest("Black Team"); 77 | 78 | HttpEntity requestUpdate = new HttpEntity<>(updateTeamRequest); 79 | String url = String.format(API_TEAMS_TEAM_ID_URL, team.getId()); 80 | ResponseEntity responseEntity = testRestTemplate.exchange( 81 | url, HttpMethod.PUT, requestUpdate, TeamResponse.class); 82 | 83 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 84 | assertThat(responseEntity.getBody()).isNotNull(); 85 | assertThat(responseEntity.getBody().name()).isEqualTo(updateTeamRequest.name()); 86 | 87 | Optional teamOptional = teamRepository.findById(team.getId()); 88 | assertThat(teamOptional.isPresent()).isTrue(); 89 | teamOptional.ifPresent(t -> assertThat(t.getName()).isEqualTo(updateTeamRequest.name())); 90 | } 91 | 92 | @Test 93 | void testDeleteTeam() { 94 | Team team = teamRepository.save(getDefaultTeam()); 95 | 96 | String url = String.format(API_TEAMS_TEAM_ID_URL, team.getId()); 97 | ResponseEntity responseEntity = testRestTemplate.exchange( 98 | url, HttpMethod.DELETE, null, TeamResponse.class); 99 | 100 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 101 | assertThat(responseEntity.getBody()).isNotNull(); 102 | assertThat(responseEntity.getBody().id()).isEqualTo(team.getId()); 103 | assertThat(responseEntity.getBody().name()).isEqualTo(team.getName()); 104 | assertThat(responseEntity.getBody().teamDetail()).isNull(); 105 | 106 | Optional teamOptional = teamRepository.findById(team.getId()); 107 | assertThat(teamOptional.isPresent()).isFalse(); 108 | } 109 | 110 | @Test 111 | void testAddTeamDetail() { 112 | Team team = teamRepository.save(getDefaultTeam()); 113 | CreateTeamDetailRequest createTeamDetailRequest = new CreateTeamDetailRequest("This team is awesome"); 114 | 115 | String url = String.format(API_TEAMS_TEAM_ID_TEAMS_DETAILS_URL, team.getId()); 116 | ResponseEntity responseEntity = testRestTemplate.postForEntity( 117 | url, createTeamDetailRequest, TeamResponse.class); 118 | 119 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 120 | assertThat(responseEntity.getBody()).isNotNull(); 121 | assertThat(responseEntity.getBody().teamDetail()).isNotNull(); 122 | assertThat(responseEntity.getBody().teamDetail().description()) 123 | .isEqualTo(createTeamDetailRequest.description()); 124 | 125 | Optional teamOptional = teamRepository.findById(team.getId()); 126 | assertThat(teamOptional.isPresent()).isTrue(); 127 | teamOptional.ifPresent(t -> { 128 | assertThat(t.getTeamDetail()).isNotNull(); 129 | assertThat(t.getTeamDetail().getDescription()).isEqualTo(createTeamDetailRequest.description()); 130 | }); 131 | } 132 | 133 | @Test 134 | void testUpdateTeamDetail() { 135 | Team team = getDefaultTeam(); 136 | team.addTeamDetail(getDefaultTeamDetail()); 137 | team = teamRepository.save(team); 138 | 139 | UpdateTeamDetailRequest updateTeamDetailRequest = new UpdateTeamDetailRequest("This team is excellent"); 140 | 141 | HttpEntity requestUpdate = new HttpEntity<>(updateTeamDetailRequest); 142 | String url = String.format(API_TEAMS_TEAM_ID_TEAMS_DETAILS_URL, team.getId()); 143 | ResponseEntity responseEntity = testRestTemplate.exchange( 144 | url, HttpMethod.PUT, requestUpdate, TeamResponse.class); 145 | 146 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 147 | assertThat(responseEntity.getBody()).isNotNull(); 148 | assertThat(responseEntity.getBody().teamDetail()).isNotNull(); 149 | assertThat(responseEntity.getBody().teamDetail().description()) 150 | .isEqualTo(updateTeamDetailRequest.description()); 151 | 152 | Optional teamOptional = teamRepository.findById(team.getId()); 153 | assertThat(teamOptional.isPresent()).isTrue(); 154 | teamOptional.ifPresent(t -> { 155 | assertThat(t.getTeamDetail()).isNotNull(); 156 | assertThat(t.getTeamDetail().getDescription()).isEqualTo(updateTeamDetailRequest.description()); 157 | }); 158 | } 159 | 160 | @Test 161 | void testDeleteTeamDetail() { 162 | Team team = getDefaultTeam(); 163 | team.addTeamDetail(getDefaultTeamDetail()); 164 | team = teamRepository.save(team); 165 | 166 | String url = String.format(API_TEAMS_TEAM_ID_TEAMS_DETAILS_URL, team.getId()); 167 | ResponseEntity responseEntity = testRestTemplate.exchange( 168 | url, HttpMethod.DELETE, null, TeamResponse.class); 169 | 170 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 171 | assertThat(responseEntity.getBody()).isNotNull(); 172 | assertThat(responseEntity.getBody().teamDetail()).isNull(); 173 | 174 | Optional teamOptional = teamRepository.findById(team.getId()); 175 | assertThat(teamOptional.isPresent()).isTrue(); 176 | teamOptional.ifPresent(t -> assertThat(t.getTeamDetail()).isNull()); 177 | } 178 | 179 | private Team getDefaultTeam() { 180 | Team team = new Team(); 181 | team.setName("White Team"); 182 | return team; 183 | } 184 | 185 | private TeamDetail getDefaultTeamDetail() { 186 | TeamDetail teamDetail = new TeamDetail(); 187 | teamDetail.setDescription("This team is awesome"); 188 | return teamDetail; 189 | } 190 | 191 | private static final String API_TEAMS_URL = "/api/teams"; 192 | private static final String API_TEAMS_TEAM_ID_URL = "/api/teams/%s"; 193 | private static final String API_TEAMS_TEAM_ID_TEAMS_DETAILS_URL = "/api/teams/%s/team-details"; 194 | 195 | } --------------------------------------------------------------------------------