4 Stay Management Implementation
4 Stay Management Implementation
Goal
- Implement stay management service.
Model Creation
Let’s recap the data model of the backend service first. For stay management, we need a stay table, a stay reserved date table, and a stay image table.
- Open your staybooking project in Intellij and go to the
com.eve.staybooking.model
package. Create a new class namedStay
.
- Add some private fields to the
Stay.class
. As you can see in the IDE, the StayReservedDate class is not available.
package com.eve.staybooking.model;
public class Stay {
private Long id;
private String name;
private String description;
private String address;
private int guestNumber;
private User host;
private List<StayReservedDate> reservedDates;
}
- Under the same
com.eve.staybooking.model
package, create theStayReservedDate.class
.
- For the three columns in the StayReservedDate table, we choose to create the composite primary key based on
stay_id
anddate
. So create another class calledStayReservedDateKey
under thecom.eve.staybooking.model
package.
- Add both
stay_id
and date as the private field ofStayReservedDateKey.class
.
package com.eve.staybooking.model;
import java.time.LocalDate;
public class StayReservedDateKey {
private Long stay_id;
private LocalDate date;
}
- Add the public getters, setters, and constructors to
StayReservedDateKey class
.
package com.eve.staybooking.model;
import java.time.LocalDate;
public class StayReservedDateKey {
private Long stay_id;
private LocalDate date;
public StayReservedDateKey() {}
public StayReservedDateKey(Long stay_id, LocalDate date) {
this.stay_id = stay_id;
this.date = date;
}
public Long getStay_id() {
return stay_id;
}
public StayReservedDateKey setStay_id(Long stay_id) {
this.stay_id = stay_id;
return this;
}
public LocalDate getDate() {
return date;
}
public StayReservedDateKey setDate(LocalDate date) {
this.date = date;
return this;
}
}
- Since we’re going to use
StayReservedDateKey
as the primary key of the StayReservedDate table, we need to mark it as@Embeddable
, add a serialVersionUID and implement both hashCode() and equals().
package com.eve.staybooking.model;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Objects;
@Embeddable
public class StayReservedDateKey implements Serializable {
private static final long serialVersionUID = 1L;
private Long stay_id;
private LocalDate date;
public StayReservedDateKey() {}
public StayReservedDateKey(Long stay_id, LocalDate date) {
this.stay_id = stay_id;
this.date = date;
}
public Long getStay_id() {
return stay_id;
}
public StayReservedDateKey setStay_id(Long stay_id) {
this.stay_id = stay_id;
return this;
}
public LocalDate getDate() {
return date;
}
public StayReservedDateKey setDate(LocalDate date) {
this.date = date;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StayReservedDateKey that = (StayReservedDateKey) o;
return stay_id.equals(that.stay_id) && date.equals(that.date);
}
@Override
public int hashCode() {
return Objects.hash(stay_id, date);
}
}
- Go back to the
StayReservedDate.class
, add the following code.
package com.eve.staybooking.model;
public class StayReservedDate {
private StayReservedDateKey id;
private Stay stay;
public StayReservedDate() {}
public StayReservedDate(StayReservedDateKey id, Stay stay) {
this.id = id;
this.stay = stay;
}
public StayReservedDateKey getId() {
return id;
}
public Stay getStay() {
return stay;
}
}
One point for the details
- Public setters are not added since won’t need them in the code. All the stay reserved date information is read from the database, so we don’t need to update them in the Java code.
- Next, annotate the class and private field to make it supported by Hibernate.
package com.eve.staybooking.model;
import jakarta.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "stay_reserved_date")
public class StayReservedDate implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private StayReservedDateKey id;
@MapsId("stay_id")
@ManyToOne
private Stay stay;
public StayReservedDate() {}
public StayReservedDate(StayReservedDateKey id, Stay stay) {
this.id = id;
this.stay = stay;
}
public StayReservedDateKey getId() {
return id;
}
public Stay getStay() {
return stay;
}
}
-
Finally, go back to the Stay class and add necessary getters, setters, and constructors.
package com.eve.staybooking.model; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public class Stay { private Long id; private String name; private String description; private String address; private int guestNumber; private User host; private List<StayReservedDate> reservedDates; public Stay() {} private Stay(Builder builder) { this.id = builder.id; this.name = builder.name; this.description = builder.description; this.address = builder.address; this.guestNumber = builder.guestNumber; this.host = builder.host; this.reservedDates = builder.reservedDates; } public Long getId() { return id; } public String getName() { return name; } public String getDescription() { return description; } public String getAddress() { return address; } public int getGuestNumber() { return guestNumber; } public User getHost() { return host; } public List<StayReservedDate> getReservedDates() { return reservedDates; } public static class Builder { @JsonProperty("id") private Long id; @JsonProperty("name") private String name; @JsonProperty("description") private String description; @JsonProperty("address") private String address; @JsonProperty("guest_number") private int guestNumber; @JsonProperty("host") private User host; @JsonProperty("dates") private List<StayReservedDate> reservedDates; public Builder setId(Long id) { this.id = id; return this; } public Builder setName(String name) { this.name = name; return this; } public Builder setDescription(String description) { this.description = description; return this; } public Builder setAddress(String address) { this.address = address; return this; } public Builder setGuestNumber(int guestNumber) { this.guestNumber = guestNumber; return this; } public Builder setHost(User host) { this.host = host; return this; } public Builder setReservedDates(List<StayReservedDate> reservedDates) { this.reservedDates = reservedDates; return this; } public Stay build() { return new Stay(this); } } }
-
Add a few Jackson related annotations.
package com.eve.staybooking.model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.util.List; @JsonDeserialize(builder = Stay.Builder.class) public class Stay { private Long id; private String name; private String description; private String address; @JsonProperty("guest_number") private int guestNumber; private User host; @JsonIgnore private List<StayReservedDate> reservedDates; public Stay() {} private Stay(Builder builder) { this.id = builder.id; this.name = builder.name; this.description = builder.description; this.address = builder.address; this.guestNumber = builder.guestNumber; this.host = builder.host; this.reservedDates = builder.reservedDates; } public Long getId() { return id; } public String getName() { return name; } public String getDescription() { return description; } public String getAddress() { return address; } public int getGuestNumber() { return guestNumber; } public User getHost() { return host; } public List<StayReservedDate> getReservedDates() { return reservedDates; } public static class Builder { @JsonProperty("id") private Long id; @JsonProperty("name") private String name; @JsonProperty("description") private String description; @JsonProperty("address") private String address; @JsonProperty("guest_number") private int guestNumber; @JsonProperty("host") private User host; @JsonProperty("dates") private List<StayReservedDate> reservedDates; public Builder setId(Long id) { this.id = id; return this; } public Builder setName(String name) { this.name = name; return this; } public Builder setDescription(String description) { this.description = description; return this; } public Builder setAddress(String address) { this.address = address; return this; } public Builder setGuestNumber(int guestNumber) { this.guestNumber = guestNumber; return this; } public Builder setHost(User host) { this.host = host; return this; } public Builder setReservedDates(List<StayReservedDate> reservedDates) { this.reservedDates = reservedDates; return this; } public Stay build() { return new Stay(this); } } }
A few explanations for the annotation:
@JsonDeserialize
makes sure the Jackson library will use the Builder class to convert JSON format data to the Stay object.@JsonProperty
makes sure to map guestNumber field to the guest_number key in JSON format data.@JsonIgnore
makes sure we don’t return reserved date information when returning the stay information in JSON format because, in our design, the front end doesn’t need to show the details about a stay’s reserved dates.
-
At last, annotate the Stay class with Hibernate-related annotations.
package com.eve.staybooking.model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.persistence.*; import java.io.Serializable; import java.util.List; @Entity @Table(name = "stay") @JsonDeserialize(builder = Stay.Builder.class) public class Stay implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private String description; private String address; @JsonProperty("guest_number") private int guestNumber; @ManyToOne @JoinColumn(name = "user_id") private User host; @JsonIgnore @OneToMany(mappedBy = "stay", cascade = CascadeType.ALL, fetch=FetchType.LAZY) private List<StayReservedDate> reservedDates; public Stay() {} private Stay(Builder builder) { this.id = builder.id; this.name = builder.name; this.description = builder.description; this.address = builder.address; this.guestNumber = builder.guestNumber; this.host = builder.host; this.reservedDates = builder.reservedDates; } public Long getId() { return id; } public String getName() { return name; } public String getDescription() { return description; } public String getAddress() { return address; } public int getGuestNumber() { return guestNumber; } public User getHost() { return host; } public List<StayReservedDate> getReservedDates() { return reservedDates; } public static class Builder { @JsonProperty("id") private Long id; @JsonProperty("name") private String name; @JsonProperty("description") private String description; @JsonProperty("address") private String address; @JsonProperty("guest_number") private int guestNumber; @JsonProperty("host") private User host; @JsonProperty("dates") private List<StayReservedDate> reservedDates; public Builder setId(Long id) { this.id = id; return this; } public Builder setName(String name) { this.name = name; return this; } public Builder setDescription(String description) { this.description = description; return this; } public Builder setAddress(String address) { this.address = address; return this; } public Builder setGuestNumber(int guestNumber) { this.guestNumber = guestNumber; return this; } public Builder setHost(User host) { this.host = host; return this; } public Builder setReservedDates(List<StayReservedDate> reservedDates) { this.reservedDates = reservedDates; return this; } public Stay build() { return new Stay(this); } } }
Define Stay Repository
- Go to the
com.eve.staybooking.repository
package and create a new classStayRepository
.
package com.eve.staybooking.repository;
import com.eve.staybooking.model.Stay;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StayRepository extends JpaRepository<Stay, Long> {
}
- Let’s check the backend diagram for stay management services. As we mentioned before, common methods like save, deleteById, findById are defined in the JpaRepositry. So we only need to define our method findByHost and findByIdAndHost.
package com.eve.staybooking.repository;
import com.eve.staybooking.model.Stay;
import com.eve.staybooking.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StayRepository extends JpaRepository<Stay, Long> {
List<Stay> findByHost(User user);
Stay findByIdAndHost(Long id, User host);
}
Stay Service Implementation
- Go to the
com.eve.staybooking.exception
package and create a new exception class StayNotExistException.
package com.eve.staybooking.exception;
public class StayNotExistException extends RuntimeException {
public StayNotExistException(String message) {
super(message);
}
}
- Go to the
com.eve.staybooking.service
package and create a new classStayService
.
package com.eve.staybooking.service;
import org.springframework.stereotype.Service;
@Service
public class StayService {
}
- Add the
StayRepository
as a private field and create a constructor for initialization.
package com.eve.staybooking.service;
import com.eve.staybooking.repository.StayRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StayService {
private StayRepository stayRepository;
@Autowired
public StayService(StayRepository stayRepository) {
this.stayRepository = stayRepository;
}
}
- Implement the methods for stay save, delete by id, list by the user and get by id.
package com.eve.staybooking.service;
import com.eve.staybooking.exception.StayNotExistException;
import com.eve.staybooking.model.Stay;
import com.eve.staybooking.model.User;
import com.eve.staybooking.repository.StayRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class StayService {
private StayRepository stayRepository;
@Autowired
public StayService(StayRepository stayRepository) {
this.stayRepository = stayRepository;
}
public List<Stay> listByUser(String username) {
return stayRepository.findByHost(new User.Builder().setUsername(username).build());
}
public Stay findByIdAndHost(Long stayId, String username) throws StayNotExistException {
Stay stay = stayRepository.findByIdAndHost(stayId, new User.Builder().setUsername(username).build());
if (stay == null) {
throw new StayNotExistException("Stay doesn't exist");
}
return stay;
}
public void add(Stay stay) {
stayRepository.save(stay);
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void delete(Long stayId, String username) throws StayNotExistException {
Stay stay = stayRepository.findByIdAndHost(stayId, new User.Builder().setUsername(username).build());
if (stay == null) {
throw new StayNotExistException("Stay doesn't exist");
}
stayRepository.deleteById(stayId);
}
}
Stay Controller Implementation
- Go to the
com.eve.service.controller
package and create a new classStayController
.
package com.eve.staybooking.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StayController {
}
- Add the StayService as a private field and create the constructor.
package com.eve.staybooking.controller;
import com.eve.staybooking.service.StayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StayController {
private StayService stayService;
@Autowired
public StayController(StayService stayService) {
this.stayService = stayService;
}
}
- Implement the methods for all stay management APIs.
package com.eve.staybooking.controller;
import com.eve.staybooking.model.Stay;
import com.eve.staybooking.service.StayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class StayController {
private StayService stayService;
@Autowired
public StayController(StayService stayService) {
this.stayService = stayService;
}
@GetMapping(value = "/stays")
public List<Stay> listStays(@RequestParam(name = "host") String hostName) {
return stayService.listByUser(hostName);
}
@GetMapping(value = "/stays/id")
public Stay getStay(
@RequestParam(name = "stay_id") Long stayId,
@RequestParam(name = "host") String hostName) {
return stayService.findByIdAndHost(stayId, hostName);
}
@PostMapping("/stays")
public void addStay(@RequestBody Stay stay) {
stayService.add(stay);
}
@DeleteMapping("/stays")
public void deleteStay(
@RequestParam(name = "stay_id") Long stayId,
@RequestParam(name = "host") String hostName) {
stayService.delete(stayId, hostName);
}
}
- Go to the
CustomExceptionHandler.class
and add the function to handle the StayNotExistException.
package com.eve.staybooking.controller;
import com.eve.staybooking.exception.StayNotExistException;
import com.eve.staybooking.exception.UserAlreadyExistException;
import com.eve.staybooking.exception.UserNotExistException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(UserAlreadyExistException.class)
public final ResponseEntity<String> handleUserAlreadyExistExceptions(Exception ex, WebRequest request){
return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
}
@ExceptionHandler(UserNotExistException.class)
public final ResponseEntity<String> handleUserNotExistExceptions(Exception ex, WebRequest request){
return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(StayNotExistException.class)
public final ResponseEntity<String> handleStayNotExistExceptions(Exception ex, WebRequest request) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- Go to the
SecurityConfig.class
to allow stay management APIs without authentication. We should require users to log in before using stay management APIs, but let’s postpone the implementation to next.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/register/*").permitAll()
.antMatchers(HttpMethod.POST, "/authenticate/*").permitAll()
.antMatchers("/stays").permitAll()
.antMatchers("/stays/*").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.disable();
}
...
}
Test Your Service
- Save all your changes and start your project. Make sure there’s no error in the log.
- Use the Stay Upload sample request in the Postman collection to save a new stay to the database. Make sure there’s no error in the response.
- Use the List Stays by Host request to verify the stay information can be successfully returned from the backend.
- Use the Get Stay by ID request to verify that you can see the stay you’ve just uploaded. Remember to replace the stay_id argument with the real ID returned in the last step.
- Finally, use the Delete Stay by ID request to make sure you can delete the stay you’ve just uploaded in stay 2. Again, remember to
- replace the stay_id with the real Id returned in step 3.
- (Optional) Redo the test process with different arguments to make sure there are no bugs in the backend.