Search code examples
spring-bootrestspring-data-jpaone-to-manyhibernate-onetomany

One to Many Relationship in spring boot REST Api


I am using spring boot to create a REST API. In this API, I have a One to Many relationship between check-in and Guests. I created a controller for check-in and use that save function of spring JPA. The save method is updating both checkin and guest tables but for the guest table, the check-in foreign key in the guests table is not getting added instead showing as null. Please someone help me. I need to create both guests and checkin simultaneously.

Check-in Model

@Data
@Entity
public class Checkin {

    @Id
    private Long id;

    private Integer no_of_guests;

    @OneToMany(mappedBy = "checkin", cascade = CascadeType.ALL)
    private List<Guest> guests;
}

Guests Model

@Data
@Entity
public class Guest {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long guest_id;

    private String name;

    private String mobile_no;

    private String address;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "guest_checkin_id", nullable = false )
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private Checkin checkin;

}

Check-in Controller

@RestController
@RequestMapping("/checkin")
public class CheckinController {

    private final CheckinRepository checkinRepo;
    private final GuestRepository guestRepo;

    public CheckinController(CheckinRepository checkinRepo,GuestRepository guestRepo){
        this.checkinRepo = checkinRepo;
        this.guestRepo = guestRepo;
    }

    @PostMapping("/add")
    ResponseEntity<Object> roomCheckin(@RequestBody Checkin checkin){
         if(checkinRepo.save(checkin) != null){
            return ResponseEntity.accepted().body("Checkin Successfull");
        }
        return ResponseEntity.unprocessableEntity().body("Failed to Checkin");
    }
}

Solution

  • Using entity classes as view model classes may be a little bit tricky, especially in this case when there is a bi-directional one-to-many relation between Checkin and Guest.

    Let's start with verifying that the entity classes and repository is working as depicted. In order to make test run, I had to add @GeneratedValue for id field in class Checkin.

    Other changes:

    I've added a test class CheckinRepositoryTest below in order to verify the code.


    As mentioned earlier, using entity classes as view model classes can be tricky, hence next step will be to introduce two new view model classes: CheckinVM and GuestVM, and a new service class GuestService that will be responsible for saving Checkin with Guest instances. Code is shown below.

    Please note that I added just the skeleton for GuestService class, I leave it to you to implement it. Code inside CheckinRepositoryTest indicates how to implement it.

    With view model classes in place, @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) should be removed.


    With all pieces up and running, you should add validation to your new view model classes, and some integration tests that verifies the behaviour. If you are following TDD, you'll add the tests on your way.

    You should also have a look at your controller method and use appropriate status codes for success and failure situations.


    Guest class

    package no.mycompany.myapp.misc;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.Getter;
    import lombok.Setter;
    import javax.persistence.*;
    
    @Getter
    @Setter
    @Entity
    public class Guest {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long guest_id;
    
        private String name;
    
        private String mobile_no;
    
        private String address;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "guest_checkin_id", nullable = false)
        @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
        private Checkin checkin;
    }
    

    GuestVM class

    package no.mycompany.myapp.misc;
    
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    public class GuestVM {
    
        private long id;
        private String name;
        private String mobile_no;
        private String address;
    
        public GuestVM(Guest guest) {
            this.id = guest.getGuest_id();
            this.name = guest.getName();
            this.mobile_no = guest.getMobile_no();
            this.address = guest.getAddress();
        }
    }
    

    Checkin class

    package no.mycompany.myapp.misc;
    
    import lombok.Getter;
    import lombok.Setter;
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    @Getter
    @Setter
    @Entity
    public class Checkin {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private Integer no_of_guests;
    
        @OneToMany(mappedBy = "checkin", cascade = CascadeType.ALL)
        private Set<Guest> guests = new HashSet<>();
    }
    

    CheckinVM class

    package no.mycompany.myapp.misc;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.NoArgsConstructor;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Getter
    @Setter
    @NoArgsConstructor
    public class CheckinVM {
    
        private long id;
        private List<GuestVM> guests;
    
        public CheckinVM(Checkin checkin) {
            this.id = checkin.getId();
            this.guests = checkin.getGuests().stream().map(GuestVM::new).collect(Collectors.toList());
        }
    }
    

    Checkin repository

    package no.mycompany.myapp.misc;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface CheckinRepository extends JpaRepository<Checkin, Long> { }
    

    CheckinRepositoryTest

    package no.mycompany.myapp.misc;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
    import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
    
    import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
    
    @DataJpaTest
    public class CheckinRepositoryTest {
    
        @Autowired
        TestEntityManager testEntityManager;
    
        @Autowired
        CheckinRepository checkinRepository;
    
        @Test
        public void test() {
    
            // create instances
            var checkinInDb = new Checkin();
            var guestInDb = new Guest();
    
            // add relations
            guestInDb.setCheckin(checkinInDb);
            checkinInDb.getGuests().add(guestInDb);
    
            // save check-in
            checkinRepository.save(checkinInDb);
    
            // verify that check-in has one guest
            var checkin = testEntityManager.find(Checkin.class, checkinInDb.getId());
            assertThat(checkin.getGuests().size()).isEqualTo(1);
    
            // verify that guest is connected to a check-in
            var guest = testEntityManager.find(Guest.class, guestInDb.getGuest_id());
            assertThat(guest.getCheckin()).isNotNull();
        }
    }
    

    CheckinService class: I leave this to you to implement

    package no.mycompany.myapp.misc;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    
    @Service
    @RequiredArgsConstructor
    public class CheckinService {
    
        private final CheckinRepository checkinRepository;
    
        public CheckinVM saveCheckin(CheckinVM checkin) {
            return null; // TODO: implement this
        }
    }
    

    Controller class

    package no.mycompany.myapp.misc;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/checkin")
    @RequiredArgsConstructor
    public class CheckinController {
    
        private final CheckinService checkinService;
    
        @PostMapping("/add")
        ResponseEntity<Object> roomCheckin(@RequestBody CheckinVM checkin) {
            if (checkinService.saveCheckin(checkin) != null) {
                return ResponseEntity.accepted().body("Checkin Successful");
            }
            return ResponseEntity.unprocessableEntity().body("Failed to Checkin");
        }
    }