로직은 다음과 같다.
- stockName으로 Stock Entity를 찾는다.
- 찾은 Stock Entity와 연결된 모든 FinancialStatement Entity를 가져온다.
- FinancialStatement Entity의 연도가 3년 이내인지 체크하고, 3년 이내면 DTO로 변환해서 리스트(fsDTO)에 저장한다.
- fsDTO를 반환한다.
로직은 간단한데, 뜯어 고칠 것이 매우 많았다.
사실 위의 두개가 맞물려서 돌아가는거라 한꺼번에 설명하겠다.
이전 포스트에서 언급했듯이, FK에는 builder 패턴을 적용하지 않는다.
FinancialStatement Entity를 한번 보자.
@Id @GeneratedValue
@Column(name = "financial_statement_id")
private Long id; // PK
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stockId", referencedColumnName = "stockId")
private Stock stock; // FK
private String content;
private int year; // 재무제표 연도
private int quarter; // 재무제표 분기
@Builder
public FinancialStatement(String content, int year, int quarter) {
this.content = content;
this.year = year;
this.quarter = quarter;
}
public FinancialStatement() {
}
// 재무제표 Entity를 만들 때, 어떤 주식을 위한 재무제표인지 stock 객체를 넣어줘야 한다. 이 때 사용하는 메소드이다.
// 양방향 연관관계이므로 Stock의 FinancialStatmentlist에도 이 재무제표 객체를 넣어줘야 한다.
public void changeStock(Stock stock) {
this.stock = stock;
stock.getFinancialStatementlist().add(this);
}
자동으로 증가하는 id와, FK인 stock을 빼고 빌더 패턴을 적용했다.
그럼 FinancialStatementDTO는 어떻게 만들어야 할까?
DTO는 "데이터 전달"을 위해 필요한 객체이다.
재무제표 DTO를 전달할 때, 우리는 주식의 모든 정보를 전달하지 않는다.
주식의 id값만 재무제표 DTO에 넣어주면 된다.
따라서, DTO를 다음과 같이 수정했다.
@Getter
@Setter
public class FinancialStatementDTO {
private Long stockId; // FK
private String content; // 재무제표 내용
private int year; // 재무제표 연도
private int quarter; // 재무제표 분기
@Builder
public FinancialStatementDTO(Long stockId, String content, int year, int quarter) {
this.stockId = stockId;
this.content = content;
this.year = year;
this.quarter = quarter;
}
}
toEntity()
메소드를 삭제해버렸다.
이 질문글에서 김영한 개발자님이 언급하셨듯이, FK가 엮여있는 경우는 toEntity()
메소드를 사용하지 않는 것이 좋다.
toEntity
메소드가 없으면 저장을 어떻게 하지?
리포지토리.save() 를 호출하기 위해서는 인수로 Entity를 줘야한다.
@Transactional
public Long save(FinancialStatementDTO financialStatementDTO){
// financialStatementDTO에 존재하는 stockId로 stockEntity를 조회한다.
Stock foundStock = stockRepository.findById(financialStatementDTO.getStockId()).get();
// financialStatementDTO를 Entity로 변환한다.
// 이때, Stock과 관련된 정보는 포함되지 않는다.
FinancialStatement financialStatement = FinancialStatement.builder()
.content(financialStatementDTO.getContent())
.year(financialStatementDTO.getYear())
.quarter(financialStatementDTO.getQuarter())
.build();
// Stock에 대한 정보를 financialStatement Entity에 저장한다.
// setter와 비슷한 역할을 하는 changeStock() 메소드를 사용했다.
financialStatement.changeStock(foundStock);
financialStatementRepository.save(financialStatement);
return financialStatement.getId();
}
중요한 것은 changeStock()
메소드이다.
재무제표 Entity를 만들 때, Stock 객체를 주입해야 한다.
이 재무제표가 어떤 주식과 연결되어 있는지 알기 위해서 이 작업을 수행해야 한다.
재무제표 Entity에서 Stock은 FK이기 때문에 빌더패턴을 사용할 수 없다.
따라서, setter 역할을 하는 changeStock()
메소드를 사용한다.
changeStock()
메소드는 setter 역할과 다른 한가지 임무를 수행한다.
그 임무는, Stock 테이블에 이 재무제표가 연결되었음을 알리는 것이다.
Stock과 FinancialStatement는 양방향이기 때문에 반드시 Stock 테이블에도 새로운 재무제표가 등록된 것을 알려줘야 한다.
(20241003) 그냥 양방향을 쓰지 말자..
구현하고자 했던 것은 주식 이름으로 3년치 재무제표를 가져오는 것이다.
public List<FinancialStatementDTO> find3yearsFinancialStatementByStockName(String stockName){
Stock foundStock = stockRepository.findByStockName(stockName); // stockName으로 Stock Entity 찾기
List<FinancialStatement> financialStatements = financialStatementRepository.findAllByStock(foundStock); // Stock으로 재무제표 모두 가져오기
List<FinancialStatementDTO> fsIn3years = new ArrayList<>();
LocalDate today = LocalDate.now();
for(FinancialStatement fs : financialStatements){
if(fs.getYear() > (today.getYear() - 3)){
// 3년 내의 재무제표면 일단 fsIn3years 리스트에 넣기
FinancialStatementDTO fsDTO = FinancialStatementDTO.builder()
.stockId(fs.getId())
.year(fs.getYear())
.quarter(fs.getQuarter())
.content(fs.getContent())
.build();
fsIn3years.add(fsDTO);
}
}
return fsIn3years;
}
구현 끝!
관계형 DB에 대한 이해가 너무 부족한 것 같다.
TIP : 외래 키가 있는 곳을 연관 관계의 주인으로 설정한다.
연관관계 주인이 외래키를 관리하고, 연관관계가 아닌 곳은 단순히 조회만 할 수 있다.
주인: @joinColumn(name = "주인이 아닌쪽 필드명")
주인이 아닌 객체: mappedBy="주인쪽 필드명"