Home > Net >  Spring Boot Rest API, JPA Entities, DTOs, what is the best approach?
Spring Boot Rest API, JPA Entities, DTOs, what is the best approach?

Time:09-09

I was given this assignment, just for practice, it became very long and challenging, but it has taught me a lot, on lambdas and JPA mainly.

It is a basic Rest API, which is used to create Hotels, Rooms, Guests, Reservations, types of guests, types of rooms, etc.

My initial problem was learning about JPA relations, OneToOne, OneToMany, etc., unidirectional, bidirectional, and what not.

I'm also using PostgreSQL, using "sping.jpa.hibernate.ddl-auto=create-drop(or update)", change as needed, when I want to recreate the DB for whatever reason.

So I'm very happy and excited using my new @Annotations to relate my Entities, and fetch back lists of whatever information I needed, came across multiple problems, read many many questions here, solved my problems, but now I have come across a new problem, but then, started questioning my approach, maybe I should not leave everything to JPA.

Let me show you what I mean. I'm going to keep my classes short to show only relevant information.

I have my reservation entity.

    @Data
    @Entity
    @Table(name = "reservation")
    public class Reservation {
      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      private Long id;
      @OneToOne(cascade = CascadeType.ALL)
      @JoinColumn(name = "guest", referencedColumnName = "id")
      @JsonManagedReference
      @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
      private Guest guest;
      @OneToOne(cascade = CascadeType.ALL)
      @JoinColumn(name = "room", referencedColumnName = "id")
      private Room room;
      @ManyToMany(fetch = FetchType.LAZY,
          cascade = CascadeType.ALL)
      @JoinTable(name = "reservation_rooms",
          joinColumns = { @JoinColumn(name = "reservation_id" )},
          inverseJoinColumns = { @JoinColumn(name = "room_id") }
      )
      @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
      private List<ReservationRoom> roomList = new ArrayList<>();
    
      private LocalDate start_date;
      private LocalDate end_date;
      private Boolean check_in;
      private Boolean check_out;
    
      public void addRoom(Room room) {
        this.roomList.add(room);
      }
    
      public void removeRoom(Long id) {
        Room room = this.roomList.stream().filter(g -> g.getId() == id).findFirst().orElse(null);
        if (room != null) {
          this.roomList.remove(room);
        }
      }
    
    }

This is my Room entity.

    @Data
    @Entity
    @Table(name = "room")
    public class Room {
      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      private Long id;
      private String name;
      private String description;
      private Integer floor;
      @JsonProperty("max_guests")
      private Integer maxGuests;
      @ManyToOne(fetch = FetchType.LAZY)
      @JsonBackReference
      private Hotel hotel;
      @ManyToOne(fetch = FetchType.LAZY)
      @JsonProperty("type")
      @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
      private RoomType roomType;
    
      @Override
      public boolean equals(Object o) {
        if (this == o) {
          return true;
        }
        if (!(o instanceof Room)) {
          return false;
        }
        return id != null && id.equals(((Room) o).getId());
      }
    
      @Override
      public int hashCode() {
        return getClass().hashCode();
      }
    }

And this is my Guest entity.


    @Data
    @Entity
    @Table(name = "guest")
    public class Guest {
      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      private Long id;
      private String first_name;
      private String last_name;
      private String email;
      @ManyToOne(fetch = FetchType.LAZY)
      @JsonProperty("type")
      @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
      private GuestType guest_type;
      @ManyToMany(fetch = FetchType.LAZY,
          cascade =  {
              CascadeType.PERSIST,
              CascadeType.MERGE
          },
          mappedBy = "guestList"
      )
      @JsonBackReference
      @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
      private List<Reservation> reservationList = new ArrayList<>();
    
      public Guest(){}
    
      public Guest(Long id) {
        this.id = id;
      }
    
      public List<Reservation> getReservationList() {
        return reservationList;
      }
    
      public void setReservationList(List<Reservation> reservationList) {
        this.reservationList = reservationList;
      }
    }

At the beginning a reservation could only have 1 room, but the requirement changed and it can have multiple rooms now. So now, the guest list needs to be linked to the room linked to the reservation, and not directly to the reservation. (I know I have a Guest and a Room, and also the List of both, this is because I'm using the single Guest as the name for the reservation, and the single Room, as the "Main" room, but don't mind that please).

Letting JPA aside, because every challenge I have faced I would ask my self "how to do it JPAish?", and then research how to do it with JPA (that's how I learned about the @ManyToMany, etc. annotations).

What I would do is just create a new table, to relate the reservations to the room (which is already done in my entities with JPA), and then add also de guest id.

So, this new table, would have a PK with reservation_id, room_id and guest_id. Very easy, then create my Reservation model, which have a List of Room, and this Room model, would have a List of Guest. Easy.

But I don't want to add a List of Guest in my current Room entity, because I have an endpoint and maybe a couple of other functions, which retrieves my Room entity, and I don't want to add a List of Guest, because as the time passes, this list would grow bigger and bigger, and it is information you don't need to be passing around.

So I did some research and found that I can extend my entity with @Inheritance or @MappedSuperclass, and I could create maybe a Reservation_Room model, which includes a List of Guest and add a List of Reservation_Room instead of a List of Room in my Reservation Entity, which I really wouldn't know if it is even possible.

Having said that, and before I keep researching and start making modifications to my code, it got me wondering, if this would be the right approach? Or if I'm forcing JPA too much on this? What would be the best approach for this? Can a 3 id relation table be easily implemented/mapped on JPA?

The main goal would be to have my Room entity exposed as it is, but when a Room is added to a Reservation, this Room would also have a List of Guest. Can I do this JPAish? Or should I create a new model and fill with the information as needed? This wouldn't exempt me from creating my 3 ids table.

CodePudding user response:

Based on what you wrote here, I think you might be at a point where you are realizing that the persistence model doesn't always match the presentation model, which you use in your HTTP endpoints. This is usually the point where people discover DTOs, which you also seem to have heard of.

DTOs should be adapted/created to the needs of the representation of an endpoint. If you don't want to expose certain state, then simply don't declare a getter/field for that data in a DTO. The persistence model should simply be designed in a way, so that you can persist and query data the way you need it. Translation between DTOs and entities is a separate thing, for which I can only recommend you to give Blaze-Persistence Entity Views a try.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(Reservation.class)
public interface ReservationDto {
    @IdMapping
    Long getId();
    GuestDto getGuest();
    List<RoomDto> getRooms();
}
@EntityView(Guest.class)
public interface GuestDto {
    @IdMapping
    Long getId();
    String getName();
}
@EntityView(Room.class)
public interface RoomDto {
    @IdMapping
    Long getId();
    String getName();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

ReservationDto a = entityViewManager.find(entityManager, ReservationDto.class, id);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<ReservationDto> findAll(Pageable pageable);

The best part is, it will only fetch the state that is actually necessary!

CodePudding user response:

I would say that you need to add a layer between persistence and the endpoints. So, you will have Controllers/Services/Repositories (in the Spring world). You should use entities as return type from Repositories (so used them in Services as well), but return DTOs to Controllers. In this way, you will decouple any modification that you do between them (e.g. you may lose interest to return a field stored in an entity, or you may want to add more information to the dto from other sources).

In this particular case, I would create 4 tables: Reservations, Guests, Rooms, GuestsForReservation.

  • Guests will contain id guests data (name, phone number, etc)
  • Rooms will contain id room data
  • GuestsForReservation will contain id reservationId guestId (so you can get the list of guests for each reservation). FK for reservationId and guestId, PK for synthetic id mentioned.
  • Reservations will contain id (synthetic), room id, date from, date to, potentially main guest id (it could be the person paying the bill, if it makes sense for you). No link to the GuestForReservation table, or you can have a list of GuestForReservation if you need to.

When you want to reserve a room, you have a ReservationRequest object, which will go to the ReservationService, here you are going to query the ReservationRepository by roomId and dates. If nothing is returned, you create the various entities and persist them in ReservationRepository and GuestForReservation repository.

By using the service and the combination of various repositories, you should be able to get all the information that you need (list of guests per room, list of guests per date, etc). At the service level, you then map the data you need to a DTO and pass it to the controller (in the format that you need), or even to other services (depending on your needs).

For what concern the mapping between entities and DTOs, there are different options, you could simply create a Component called ReservationMapper (for example) and do it yourself (take an entity and build a DTO with what you need); implements Converter from the Springframework; use MapStruct (cumbersome in my opinion); etc.

If you want to represent in JPA an id made of multiple columns, usually @Embeddable classes are used (you should mark them as EmbeddedId when you use them), you can google them for more info.

  • Related