@Service
public class SaveDiaryService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final WriterRepository writerRepository;
private final DiaryRepository diaryRepository;
private final DietRepository dietRepository;
private final FoodRepository foodRepository;
public SaveDiaryService(WriterRepository writerRepository, DiaryRepository diaryRepository, DietRepository dietRepository, FoodRepository foodRepository) {
this.writerRepository = writerRepository;
this.diaryRepository = diaryRepository;
this.dietRepository = dietRepository;
this.foodRepository = foodRepository;
}
//작성자 id 생성 메서드
public EntityId<Writer> getNextIdOfWriter() {
Long count = writerRepository.findCountOfId();
Long writerId;
if (count == 0) {
writerId = 0L;
} else {
writerId = writerRepository.findMaxOfId();
}
return new EntityId<>(writerId + 1);
}
//일지 id 생성 메서드 (트랜잭션 필수)
public EntityId<DiabetesDiary> getNextIdOfDiary() {
Long count = diaryRepository.findCountOfId();
Long diaryId;
if (count == 0) {
diaryId = 0L;
} else {
diaryId = diaryRepository.findMaxOfId();
}
return new EntityId<>(diaryId + 1);
}
//식단 id 생성 메서드 (트랜잭션 필수)
public EntityId<Diet> getNextIdOfDiet() {
Long count = dietRepository.findCountOfId();
Long dietId;
if (count == 0) {
dietId = 0L;
} else {
dietId = dietRepository.findMaxOfId();
}
return new EntityId<>(dietId + 1);
}
//음식 id 생성 메서드 (트랜잭션 필수)
public EntityId<Food> getNextIdOfFood() {
Long count = foodRepository.findCountOfId();
Long foodId;
if (count == 0) {
foodId = 0L;
} else {
foodId = foodRepository.findMaxOfId();
}
return new EntityId<>(foodId + 1);
}
// getIdOfXXX()의 경우 트랜잭션 처리 안하면 다른 스레드가 껴들어 올 경우 id 값이 중복될 수 있어 기본키 조건을 위배할 수도 있다. 레이스 컨디션 반드시 예방해야 함.
@Transactional
public Writer saveWriter(String name, String email, Role role) {
logger.info("saveWriter");
Writer writer = new Writer(getNextIdOfWriter(), name, email, role);
writerRepository.save(writer);
return writer;
}
@Transactional
public DiabetesDiary saveDiary(Writer writer, int fastingPlasmaGlucose, String remark, LocalDateTime writtenTime) {
logger.info("saveDiary");
DiabetesDiary diary = new DiabetesDiary(getNextIdOfDiary(), writer, fastingPlasmaGlucose, remark, writtenTime);
writer.addDiary(diary);
writerRepository.save(writer);
return diary;
}
@Transactional
public Diet saveDiet(Writer writer, DiabetesDiary diary, EatTime eatTime, int bloodSugar) {
logger.info("saveDiet");
Diet diet = new Diet(getNextIdOfDiet(), diary, eatTime, bloodSugar);
diary.addDiet(diet);
writer.addDiary(diary);
writerRepository.save(writer);
return diet;
}
@Transactional
public Food saveFood(Writer writer, Diet diet, String foodName) {
logger.info("saveFood");
Food food = new Food(getNextIdOfFood(), diet, foodName);
diet.addFood(food);
writerRepository.save(writer);
return food;
}
}
엔티티 식별자를 "복합키"로 만들었고, 복합키 중 일부분을 "대리키" 로 "직접 할당"하는 방식으로 구현했다. 이 부분에서 주의할 점은 두 가지다.
1.대리키를 할당하는 메서드 (getIdOfxxx)호출 시 다른 엔티티의 id를 호출할 수 있는 불상사가 일어날 수 있다.
(리턴 타입이 단순 Long형인 경우에 발생할 수 있는 문제다.)
2.대리키 직접 할당할 때 다른 스레드가 끼어들어 레이스 컨디션이 발생할 수 있는 문제다.
이 경우 중복 대리키가 발생해서 기본키 조건에 위배된다.
EntityId<>라는 대리키 지급용 클래스를 따로 만들었다.
이렇게 하면 컴파일 타임에서 잘못된 메소드 호출을 막을 수 있다.
(제네릭의 사용 상 이점이다.)
getIdOfxxx()는 다른 스레드가 못 끼어들도록 반드시 하나의 트랜잭션 내에 존재하도록 해야 한다.
@Query(value = "FROM DiabetesDiary diary INNER JOIN diary.writer w WHERE diary.writer.writerId = :writer_id AND diary.diaryId = :diary_id")
Optional<DiabetesDiary> findDiabetesDiaryOfWriter(@Param("writer_id") Long writerId, @Param("diary_id") Long diaryId);
위 코드의 문제점은 역시 Long 파라미터이다. 헷갈릴 수 있기 때문에 컴파일 타임에 잡아낼 수 있도록 강제할 필요가 있다.
find용 Service에서 파라미터로 EntityId<>를 받고 이를 Long으로 변환해주는 작업을 한다.
@Transactional(readOnly = true)
public Optional<DiabetesDiary> getDiabetesDiaryOfWriter(EntityId<Writer>writerEntityId,EntityId<DiabetesDiary>diabetesDiaryEntityId){
logger.info("getDiabetesDiaryOfWriter");
return diaryRepository.findDiabetesDiaryOfWriter(writerEntityId.getId(),diabetesDiaryEntityId.getId());
}