-
[Spring Boot] mysql 연동 , DB 연관관계 고민Backend/개발 2021. 8. 4. 01:24반응형
TIL35일차 개인프로젝트 기록
mysql
휘발성인 h2가 아닌 mysql DB에 데이터를 저장하고 로직을 구현해야할 것 같아서 mysql 연동 설정을 해주었다.
mysql dependency 추가
% mysql --version mysql Ver 8.0.23 for osx10.15 on x86_64 (Homebrew)
implementation('org.springframework.boot:spring-boot-starter-data-jpa') implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.23'
application.yml 설정 관련
Datasource1
서로 다른 Database connection pool 구현체 연동을 위해 만든 표준 인터페이스
DBCP : Database connection pool
DBCP 기본 option : maxActive, maxIdle, minIdle, initialSize
Spring boot 2.4 이후 spring.config.activate.on-profile
하나의 application.yml 파일에 여러 환경의 설정 정보를 저장할때 profile를 사용하고, ---로 구분한다.
그리고 java -jar myapp.jar --spring.profiles.active=prod 이런식이나
인텔리제이에서 Edit Configuration에서 Active profile값을 지정해주면 그 프로파일로 실행할 수 있다.
나는 주석처리하고 다시 돌리고,,이랬는데 테스트 DB나 환경이 다를때 프로파일로 구분하면 되는 것 같다.
spring.output.ansi.enabled: ALWAYS
콘솔에서 뜨는 로그에 색이 입혀진다.
hibernate show_sql, format_sql , ddl
spring: jpa: properties: hibernate: ddl-auto: update # 변경된 스키마 적용 -> 배포용 파일엔 부적합 show_sql: true # 콘솔에 쿼리를 보여줌 format_sql: true # 보기좋게 포맷팅해줌 ( 줄바꿈 , 탭 ) generate-ddl: true # @Entity가 명시된 클래스를 찾아 ddl 생성하고 실행
logging.level.org.hibernate.type.descriptor.sql
logging: level: org: hibernate: type: descriptor: sql: trace
바인드되는 파라미터값이 출력된다. insert value(?,?,?)에 뭐가들어가는지 등
결과
프로젝트 처음 실행시
# generate-ddl : true, hibernate.ddl-auto:update라서 생성되었고 # show_sql이라서 로그를 볼 수 있고 # format_sql이라서 줄바꿈되어 이쁘게 출력된다 Hibernate: create table participants ( id bigint not null auto_increment, created_date datetime(6), modified_date datetime(6), comment varchar(255), current_tier varchar(15) not null, highest_tier varchar(15) not null, main_position varchar(255) not null, point bigint, sub_positions varchar(255), summoner_name varchar(255) not null, primary key (id) ) engine=InnoDB
값 추가시
Hibernate: insert into participants (created_date, modified_date, comment, current_tier, highest_tier, main_position, point, sub_positions, summoner_name) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
values에 무슨 값을 입력했는지 볼 수 있다.
35명의 참가자를 DB에 등록했다.
아무래도 등록하는과정이 굳이 번거로울 필요는 없는 것 같고,, 등록 구현한건 연습한거 정도로 치고 나중에는 디비에 그냥 입력하는것도 생각해봐야겠다.
추가로 comment에 값이 안들어가서 봤더니 html에 name="comment"값이 빠져있어서 넣어주었다.
Team - Participant 관계 설정
김영한님 JPA강좌를 참고해서 쓴 블로그들이 많고, 거기서 team-member관계를 예로 들어서 참고하기 좋았다.
참고한 블로그들 - 두고두고 읽어두면 좋을 것 같다.
//Teams @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name ="TEAM_ID") private Long id; @OneToMany(mappedBy = "team", cascade = CascadeType.ALL) private List<ParticipantsEntity> participants = new ArrayList<>();
//Participants @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "TEAM_ID") private TeamsEntity team;
객체의 두 관계중 하나의 연관관계를 주인으로 지정해 연관관계의 주인 만이 외래키를 관리(등록, 수정) 한다.
주인이 아닌 쪽은 읽기만 가능하고 mappedBy속성으로 주인을 지정하며, 주인은 mappedBy 속성을 지정하지 않는다.
mappedBy : 나는 By에 의해 매핑되었다 라는 의미이다
@OneToMany(mappedBy=team) : Participants 객체의 team 변수에 의해서 관리가 된다.
@JoinColumn(name=Team_id) : team을 관리할 것이다.
외래키가 있는 곳을 주인으로 정하기 때문에 Participants.team이 연관관계의 주인이다.
이때 성능이슈가 발생할 수 있다.
- Participants의 경우 insert쿼리만 하면 되지만 Team의 경우 insert+update 가 둘다 일어난다
DB 입장에서는 외래키가 있는 곳이 무조건 N
(외래키가 있는 곳 = N이 있는 곳 = 주인 = @ManyToOne)
주의해야할 점
연관관계의 주인에 값을 입력하지 않는 실수
toString, lombok, json 생성시 주의
그리고 Member와 team을 조회할때 member를 가져올 때 member만 가져올지, team을 가져올지 고민해야한다.
테이블 생성 결과
Hibernate: alter table participants add column team_id bigint Hibernate: create table teams ( team_id bigint not null auto_increment, leader_id bigint, points bigint, team_name varchar(255), primary key (team_id) ) engine=InnoDB Hibernate: alter table participants add constraint FKd9fj9qgl0laglppk9oxdrjwkp foreign key (team_id) references teams (team_id)
테이블을 생성했다.
Team-participants 연결 테스트
테스트 코드
@AfterEach public void cleanAll() { teamsRepository.deleteAll(); } @BeforeEach public void setup() { log.info("setup"); List<TeamsEntity> teams = new ArrayList<>(); for (int i = 1; i <= 5; i++) { TeamsEntity teamsEntity = TeamsEntity.builder() .teamName(i + "팀") .build(); teamsEntity.addParticipants(ParticipantsEntity.builder() .summonerName("감귤or가씨") .currentTier("silver2") .highestTier("silver2") .mainPosition("SUP") .point(200l) .build() ); teams.add(teamsEntity); teamsRepository.save(teamsEntity); } } @Test public void 팀조회() { // given List<String> participantsNames = teamsService.findAllPariticipantsNames(); // then assertThat(participantsNames.size()).isEqualTo(5); }
Error: LazyInitializationException
테스트에서는 새로 생성한다음 넣어주었지만, 실제로는 미리 저장되어있는 participants중에 가져와 미리 생성된 team에 등록해줄 것이라 생각해서 이렇게 코드를 짰었다.
teamsEntity.addParticipants(participantsRepository.getOne((long) i));
could not initialize proxy [com.project.auction.lol.domain.ParticipantsEntity#1] - no Session org.hibernate.LazyInitializationException: could not initialize proxy [com.project.auction.lol.domain.ParticipantsEntity#1] - no Session at com.project.auction.lol.domain.ParticipantsEntity$HibernateProxy$19hrRstI.updateTeam(Unknown Source) at com.project.auction.lol.domain.TeamsEntity.addParticipants(TeamsEntity.java:49)
LazyInitializationException 오류가 났다.
repository의 get과 findById의 차이가 findById는 바로 가져오는거고, get은 지연로딩인데 get은 프록시로 가져와서 테스트할때 값 지정이 안되서 그러는 것 같다. 다음 코드로 변경해주었다.
teamsEntity.addParticipants(participantsRepository.findById((long)i).get());
테스트코드에서는 굳이 이럴 필요가 없는 것 같아서 Participants.builder를 이용해 새로 생성한 값을 넣어주었다.
Error: cascade
assertThat(participantsNames.size()).isEqualTo(5);
테스트 코드가 실제는 0 기대는 5로 실패했다.teamsRepository.save(teamsEntity);
원래는 이 코드에서Team생성 - team에 participant넣고 save하는데 participants가 저장이 안돼서 team.getparticipants를 해도 불러와지지가 않았는데, cascade를 지정해주어야 하는 것이었다.
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL) private List<ParticipantsEntity> participants = new ArrayList<>();
Cascade란
Entity의 사이태 변화를 전파시키는 옵션이다.
Entity에 상태 변화가 있을 때 연관되어 있는 (OneToMany, ManyToOne) Entity에도 상태 변화를 전이시키는 옵션이다. (default : none)
onetomany 관계일때 부모->여러 자식 전파를 위해 one에 cascade 옵션을 넣어주는 것 같다.
Fetch, N+1 문제 이 예제와 연관하여 생각해보기 -- 정리 안됨..
위의 테스트 코드가 JPA n+1 예제를 생각하면서 따라해본 예제인데, 로그를 봐도 이해가 잘 되지 않아서 다시 천천히 알아보려고 한다.
N+1이라는 말이 나타내는 의미는 다음과 같다.
1: 처음에 team 조회 쿼리
N : team에 연결된 participanst를 가져오기 위한 추가 쿼리
만약에 참가자가 100만이라면? 팀1개조회하는데 N(100만)개의 쿼리가 추가로 불려지는 문제가 생긴다.
일반적인 N+1조회
fetchjoin으로 한 쿼리에 해결
@Query("select t from teams t join fetch t.participants")
해결 방법 - Lazy, fetchjoin
fetchjoin : runtime에 동적으로 원하는 것만 선택해서 가져올 수 있다.
하지만 한계는 fetch join을 하면 페이징과 둘 이상의 컬렉션을 패치할 수는 없다.
그리고 보통 @ManyToOne에는 fetch lazy를 준다고 해서 넣어주었다.(아직 이부분도 이해가 잘 안됨..)
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "TEAM_ID") private TeamsEntity team;
Stream
다음 코드는 team에서 participants중 첫번째( 테스트상 하나만 넣어놔서 그냥 get(0)을 한 상황이다) 를 불러와 summonername을 리턴해주는 코드이다. 예제에서 stream, map, collect를 썼는데 이해가 안돼서 같은 의미로 동작하는 코드를 짜보았는데, 이참에 무슨 의미인지 공부하고 비교를 해보려고 한다.
List<String> result = new ArrayList<>(); for( TeamsEntity entity: teams){ result.add(entity.getParticipants().get(0).getSummonerName()); } return result; return teams.stream() .map(t-> t.getParticipants().get(0).getSummonerName()) .collect(Collectors.toList());
- map : 변환메소드 / 해당 스트림의 요소를 주어진 함수에 인수로 전달하여 ,그 반환값들로 이루어진 새로운 스트림을 반환한다
- collect는 stream의 아이템을 원하는 자료형으로 변환할 수 있다.
즉 map으로 teams의 하나의 요소인 teamsEntity를 전달해서, 반환값인 summonerName으로 이루어진 스트림을 만든다 ( String의 stream일것 ) -> 이것을 toList()해서 List<String>으로 만들어준 것이다.
정리
mysql 연동 해봤고, Team-participant 연관관계 고려해서 테이블 생성을 해봤다.
아직 n+1 문제 , fetch 로 해결하는 문제는 잘 모르겠다. 더 정리해서 올리도록 하겠담
실제 기능 구현 코드를 슬슬 짜봐야겠다...
출처
'Backend > 개발' 카테고리의 다른 글
[Spring Boot] 우승 멤버 별표표시 , @ElementCollection (2) 2021.08.05 [Spring Boot] 팀 생성, DB 슬라이스 테스트 (0) 2021.08.05 [Spring Boot] JPA (4) 2021.08.03 [Spring Boot] 프로젝트 gradle 버전 변경 , h2 (2) 2021.08.03 [Spring Boot] 참가자 등록 구현, DTO (0) 2021.08.02