Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions _PRA/PRA03.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# PRA03: Implementing ManyToMany Relationships in JPA

## CIFO La Violeta - FullStack IFCD0021-24 MF01-02-03

In this practical exercise, you will enhance the `RestaurantManager` project by implementing a `ManyToMany` relationship between `Menu` and `MenuItem` entities using JPA and Spring Boot.

<mark>You'll design, implement, and test this relationship using an in-memory H2 database </mark>and `JpaRepository`, with a focus on the automatic creation of a<mark> join table</mark> using `@JoinTable`.

## Objectives

Implement and test a `ManyToMany` relationship between `Menu` and `MenuItem` entities in the `RestaurantManager` project using **Spring Data JPA**, exploring both <mark>unidirectional and bidirectional relationships.</mark>

### Project Base

- Existing Repository: [Restaurant Manager](https://github.com/AlbertProfe/restaurantManager/commits/master/)
- Reference Lab: [Spring Boot Lab 8.3](https://albertprofe.dev/springboot/sblab8-3.html)
- Reference Lab: [Spring Boot Lab 8.4](https://albertprofe.dev/springboot/sblab8-4.html)

### Tasks

Summary tasks:

- [ ] Design Entity Classes
- [ ] Implement JPA Repositories
- [ ] Create Service Layer
- [ ] Develop REST Controllers
- [ ] Configure H2 Database
- [ ] Write Unit Tests
- [ ] Test with Swagger API

Tasks:

1. **Design Entity Classes**

- Create or update the `Menu` and `MenuItem` entity classes.
- Implement a ManyToMany relationship between `Menu` and `MenuItem`.
- Use appropriate JPA annotations such as `@ManyToMany` and `@JoinTable`.
- <mark>Implement both unidirectional and bidirectional</mark> cases when it is possible.
- Address the <mark>infinite recursion issue</mark> that can lead to `StackOverflowError`.

2. **Implement JPA Repositories**

- Create JPA repository interfaces for both `Menu` and `MenuItem`.
- ~~Add any necessary custom query methods.~~

3. **Create Service Layer**

- Develop service interfaces and implementations for both entities.
- Include methods to handle the relationship between `Menu` and `MenuItem`.
- Implement CRUD operations ~~and any additional business logic.~~

4. **Develop REST Controllers**

- Create REST controllers for `Menu` and `MenuItem`.
- Implement endpoints that demonstrate the relationship between the entities.
- Use appropriate HTTP methods for each operation.

5. **Configure H2 Database**

- Set up the H2 in-memory database in your `application.properties` or `application.yml` file.
- Configure Hibernate to create the schema automatically.

6. **Write Unit Tests**

- Create unit tests for your repositories, services, and controllers.
- Test the `ManyToMany` relationships thoroughly.
- Verify that CRUD operations work correctly with the relationship in place.

7. **Test with Swagger API**

- Configure Swagger for your Spring Boot application.
- Use Swagger UI to test all implemented REST endpoints.
- Verify that the relationship between `Menu` and `MenuItem` is correctly represented and functional.

### Example MenuItem Entity

Here's a proposed example for the `MenuItem` entity:

```java
@Entity
public class MenuItem {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private String id;

private String name;
private String description;
private boolean isSpicy;
private boolean hasGluten;
private boolean isAvailable;

@Enumerated(EnumType.STRING)
private CourseType courseType;

@ManyToMany(mappedBy = "menuItems")
private Set<Menu> menus = new HashSet<>();

// Default constructor
public MenuItem() {
this.id = UUID.randomUUID().toString();
}

// Constructors, getters, and setters
}

public enum CourseType {
STARTER,
MAIN,
DESSERT
}
```

### Unidirectional vs Bidirectional ManyToMany

<mark>Explore the differences between unidirectional and bidirectional </mark>`ManyToMany` relationships:

- **Unidirectional**: Only one entity maintains the relationship.
- **Bidirectional**: Both entities are aware of the relationship.

Address the infinite recursion issue in bidirectional relationships that can lead to StackOverflowError when serializing to JSON.

Implement a solution using `@JsonIgnore`,`@JsonManagedReference` and `@JsonBackReference` or by customizing the JSON serialization process.



**Unidirectional Relationship**

- Only one entity maintains the relationship
- Simpler to implement and manage
- Less coupling between entities
- May be sufficient for basic use cases

**Bidirectional Relationship**

- Both entities are aware of the relationship
- Provides more flexibility in querying and navigating the relationship
- Requires careful management to maintain consistency
- More suitable for complex domain models

**Addressing Infinite Recursion**

Bidirectional relationships can lead to infinite recursion when serializing to JSON, causing a StackOverflowError. This occurs because each entity references the other, creating an endless loop during serialization.

Solutions

1. **@JsonIgnore Annotation**

- Apply `@JsonIgnore` to one side of the relationship
- Prevents the annotated property from being serialized
- Simple but may limit data access in some scenarios



```java
public class Menu {

@ManyToMany
@JsonIgnore
private Set<MenuItem> menuItems;
}
```

2. **Custom Serialization**

- Implement custom serializer classes
- Provides fine-grained control over JSON output
- More complex but offers greater flexibility

3. **DTO Pattern**

- Create Data Transfer Objects (DTOs) for API responses
- Allows explicit control over what data is transferred
- Separates API representation from domain model

4. **Lazy Loading**

- Use lazy loading for related entities
- Prevents automatic loading of related entities unless explicitly requested
- Requires careful handling in service layer

By implementing one or a combination of these solutions, you can effectively manage bidirectional relationships while avoiding infinite recursion issues in your Spring Boot application.

### Submission Guidelines

- Fork the existing [Restaurant Manager](https://github.com/AlbertProfe/restaurantManager/commits/master/) repository and clone it to your local environment.
- Create a new branch named `PRA03-YourName` from the latest commit.
- Commit your changes with clear, descriptive messages.
- Push your branch to your forked repository.
- Create a pull request to the AlbertProfe repository with a summary of your changes, titled:
- `PRA03-YourName-ManyToManyRelationshipImplementation`

### Evaluation Criteria

- Correct implementation of the ManyToMany relationship with automatic join table creation.
- Proper use of JPA annotations and Spring Data JPA features.
- Functionality of the service layer and controllers in handling the relationship.
- Quality and coverage of unit tests.
- Successful configuration and use of the H2 in-memory database.
- Clarity and completeness of Swagger API documentation and tests.
- Proper handling of bidirectional relationship and infinite recursion issues.
- Overall code quality, organization, and documentation.

Good luck with your implementation! Remember to test thoroughly and document your code clearly.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.example.restaurantManager;

import dev.example.restaurantManager.utilities.CustomerDataLoader;
import dev.example.restaurantManager.utilities.MenuDataLoader;
import dev.example.restaurantManager.utilities.TableDataLoader;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -14,11 +16,15 @@ public class RestaurantManagerApplication {
public static void main(String[] args) {
SpringApplication.run(RestaurantManagerApplication.class, args);
}
/*

@Bean
public ApplicationRunner dataLoader(CustomerDataLoader customerDataLoader) {
return args -> customerDataLoader.createFakeCustomers();

/* @Bean
public ApplicationRunner dataLoader(CustomerDataLoader customerDataLoader, TableDataLoader tableDataLoader, MenuDataLoader menuDataLoader) {
return args -> {
customerDataLoader.createFakeCustomers();
tableDataLoader.createFakeTables();
menuDataLoader.createFakeMenus();
};
}
*/

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package dev.example.restaurantManager.controller;


import dev.example.restaurantManager.model.EatInOrderRestaurant;
import dev.example.restaurantManager.service.EatInService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.List;

@RequestMapping("/api/v1/eatinorder")
@RestController
public class EatInOrderController {

@Autowired
private EatInService eatInService;

//manage request by ResponseEntity with all EatInOrder
@GetMapping("/alleatinorders")

public ResponseEntity<List<EatInOrderRestaurant>> getAllEatInOrders(){
List<EatInOrderRestaurant> eatinorders = eatInService.getAllEatInOrders();
HttpHeaders headers = getCommonHeaders("Get alls Eat In Restaurant Orders");
return eatinorders !=null && !eatinorders.isEmpty()
? new ResponseEntity<>(eatinorders,headers, HttpStatus.OK)
: new ResponseEntity<>(headers,HttpStatus.NOT_FOUND);
}

@PostMapping

public ResponseEntity<EatInOrderRestaurant> createEatInOrder(@RequestBody EatInOrderRestaurant eatinorder) {
EatInOrderRestaurant createdEatInOrder = eatInService.createEatInOrders(eatinorder);
HttpHeaders headers = getCommonHeaders("Create Eat In Order Restaurant");
return createdEatInOrder !=null
?new ResponseEntity<>(createdEatInOrder, headers, HttpStatus.CREATED)
: new ResponseEntity<>(headers,HttpStatus.BAD_REQUEST);
}

@PutMapping("/{id}")

public ResponseEntity<EatInOrderRestaurant> updateEatInOrder(@PathVariable String id, @RequestBody EatInOrderRestaurant eatInOrderDetails){
EatInOrderRestaurant updatedEatInOrder = eatInService.updateEatInOrders(id, eatInOrderDetails);
HttpHeaders headers = getCommonHeaders("Update Eat In Restaurant Order");
return updatedEatInOrder != null
? new ResponseEntity<>(updatedEatInOrder,headers,HttpStatus.OK)
: new ResponseEntity<>(headers,HttpStatus.NOT_FOUND);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEatInOrder(@PathVariable String id){
boolean deleted = eatInService.deleteEatInOrders(id);
HttpHeaders headers = getCommonHeaders("Delete Eat In Order Restaurant");
return deleted
?new ResponseEntity<>(headers,HttpStatus.NO_CONTENT)
: new ResponseEntity<>(headers, HttpStatus.NOT_FOUND);

}

@GetMapping("{id}")
public ResponseEntity<EatInOrderRestaurant> getEatInOrderById(@PathVariable String id){
EatInOrderRestaurant eatinorder = eatInService.getEatInOrdersById(id);
HttpHeaders headers = getCommonHeaders("Get a Eat In Order by ID");
return eatinorder !=null
? new ResponseEntity<>(eatinorder, headers, HttpStatus.OK)
: new ResponseEntity<>(headers,HttpStatus.NOT_FOUND);

}




private HttpHeaders getCommonHeaders(String description) {
HttpHeaders headers = new HttpHeaders();
headers.add("desc", description);
headers.add("content-type", "application/json");
headers.add("date", new Date().toString());
headers.add("server", "H2 Database");
headers.add("version", "1.0.0");
headers.add("eatInOrders-count",String.valueOf(eatInService.countEatInOrders()));
headers.add("object", "EatInOrders");
return headers;
}

}
Loading