You will be fine

<Spring & JPA 웹서비스 만들기 > 구현 (5) - 달력 만들기

by BFine
반응형

누가 추첨됐는지 결과를 표시하는 달력기능을 추가해보았다..

 

가. Table 구성

 a. 엔티티 관계도

  -  일반 달력테이블을 만들고 달력테이블의 PK를 참조하는 달력데이터 테이블을 만들었다.

  -  JPA로 Calendar를 불러오면 Data 한번에 가져올수 있도록 해보았다.

 

나. JPA Entity

 a. Calendar 

  -  @IdClass를 활용해 year, month, day 를 복합키로 구성했다.

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "Calendar")
@IdClass(CalendarPK.class)
public class Calendar {

    @Id
    private int year;
    
    @Id
    private int month;
    
    @Id
    private int day;

    
    @MapsId
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumns({
        @JoinColumn(name = "year",referencedColumnName = "year"),
        @JoinColumn(name = "month",referencedColumnName = "month"),
        @JoinColumn(name = "day",referencedColumnName = "day")
    })
    List<CalendarData> calendarDataList;

}

 

 b. CalendarData

  -  결과가 저장되며 Calendar의 PK를 FK로 가지도록 만들었다.

  -  Postgresql은 시퀀스를 생성할 수 있어서 시퀀스전략으로 PK를 생성했다.

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@IdClass(CalendarDataPK.class)
@SequenceGenerator(
    name = "CALENDAR_DATA_GENERATOR",
    sequenceName = "CALENDAR_DATA_SEQUENCE",
    initialValue = 1,
    allocationSize = 1)
@Table(name = "CalendarData")
public class CalendarData {


    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CALENDAR_DATA_SEQUENCE")
    
    private long seq;
    
    private int year;
    
    private int month;
    
    private int day;

    private String name;

    private long userId;

    private String title;

    @ColumnDefault("0")
    private int bet;

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

 

다. 로직

 a. JPQL Fetch Join

@Transactional
public List<Calendar> getAll(CalendarDTO calendarDTO){

  /*  String jpql = "select distinct c, cd from Calendar c left join c.calendarDataList cd"
    + " on c.year = cd.year and c.month = cd.month and c.day = cd.day"
    + " where c.year=:year and c.month=:month";  */ //테스트의 흔적...
            
    String jpql = "select distinct c from Calendar c"
      +"left join fetch c.calendarDataList" 
      +" where  c.year=:year and c.month=:month"
      +" order by c.day asc";

      Query createQuery = em.createQuery(jpql);
      createQuery.setParameter("year", calendarDTO.getYear());
      createQuery.setParameter("month", calendarDTO.getMonth());

      return (List<Calendar>) createQuery.getResultList();
 }

  -  처음에는 그냥 left Join 으로 하면 되겠지라는 생각을 했다...

        => 단순 left Join으로 할 경우 결과가 Object로 나오기 때문에 ClassCastException이 발생한다.. 

  -  공부했었는데 막상 이렇게 만들때 Fetch Join이 생각이 나지않았다... 이래서 실제로 만들어야 하나보다..

  -  Fetch Join을 사용하면 모든 참조 객체들을 가져오는데 left Join의 특성상 중복결과가 발생할 수 있으니

      distinct 키워드를 추가해야한다.

 

 b. 달력 생성

  -  달력을 만드는데 유의사항이 있다.

         1. 윤달, 윤년

         2. 첫번째 시작 요일 (최초 1일이 시작되는 시점)

  -  찾아보니 둘다 Java에서 만들어준 훌륭한 API를 사용하면 된다.

  public CalendarDTO addCalendar(int year, int month){
        if(year == DEFAULT){
            year = LocalDateTime.now().getYear();
            month = LocalDateTime.now().getMonth().getValue();   
        }

       if(calendarRepository.getCount(year, month) == 0){
          List<Calendar> calendarList = new ArrayList<>();
          int lengthOfMonth = YearMonth.of(year, month).lengthOfMonth();
         
         for(int day = 1; day <= lengthOfMonth; day++){
            Calendar calendar = Calendar.builder()
            .year(year)
            .month(month)
            .day(day)
            .build();
            
            calendarList.add(calendar);
         }
         calendarRepository.saveAll(calendarList);
       }

       CalendarDTO calendarDTO = new CalendarDTO();
       calendarDTO.setStartDay(LocalDate.of(year, month, 1).getDayOfWeek().getValue());
       calendarDTO.setYear(year);
       calendarDTO.setMonth(month);
       return calendarDTO;
    }

 

라. 트러블 슈팅

 a. FK 인식 문제

  -  ERROR LOG DETAIL: (year, month, day)= (2021, 2, 10) 키가 "calendar" 에 없습니다.

        => Calendar 테이블에 데이터가 있는데도 FK 제약 오류가 발생했었다... 

  -  원인은 @JoinColumnreferencedColumnName를 추가하지 않아서 발생했다.

        => 추가없이 사용할 경우 랜덤한 순서로 PK가 생성되어 테이블간 맵핑이 정확하게 되지않았다. 

  -  Hibernate 로그를 확인했더니 순서가 다른걸 확인 할수있었다.

## 추가전 

Hibernate: create table calendar (day int4 not null, month int4 not null, year int4 not null, 
primary key (day, month, year))
...
Hibernate: alter table if exists calendar_data add constraint FKd5kpvvc1pj0r9rifkqnf89ui9 
foreign key (year, month, day) references calendar

## 추가후

Hibernate: create table calendar (day int4 not null, month int4 not null, year int4 not null, 
primary key (day, month, year))
...
Hibernate: alter table if exists calendar_data add constraint FKd5kpvvc1pj0r9rifkqnf89ui9 
foreign key (day, month, year) references calendar

 

마. 실행 

 a. 화면 렌더링

  -  thymeleaf의 th를 활용해서 받은 데이터를 렌더링 해주었다.

  -  모양을 잡기위해 badge는 Id값을 나눈 나머지로 유저별로 동일하게 가져가도록 부여했다.

<li th:each="num :${#numbers.sequence(1,{calendarDTO.startDay})}">-</li>
  <th:block th:each="calendar: ${calendarDTO.calendarList}">
      <li>[[${calendar.day}]]
         <th:block th:each=" data : ${calendar.calendarDataList}">
              <th:block th:switch="${data.userId%4}">
                  <div th:case="0" class="badge-info">[[${data.name}]]</div>
                  <div th:case="1" class="badge-success">[[${data.name}]]</div>
                  <div th:case="2" class="badge-danger">[[${data.name}]]</div>
                  <div th:case="3" class="badge-primary">[[${data.name}]]</div>
              </th:bock>
          </th:block>
      </li>
   </th:block>
</li>

 b. 테스트

  -  결과가 달력에 입력되는지 테스트 해보면...

 

 

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기