ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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-2

    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)

    35-3

    테이블을 생성했다.

     


    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조회

    35-3

    fetchjoin으로 한 쿼리에 해결

    @Query("select t from teams t join fetch t.participants")

    35-3

    해결 방법 - 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 로 해결하는 문제는 잘 모르겠다. 더 정리해서 올리도록 하겠담

    실제 기능 구현 코드를 슬슬 짜봐야겠다...

    출처

    스프링 부트 Auto-configuration과 JPA(하이버네이트) SQL문 로깅

    JPA-엔티티상태-Cascade

    JPA Lazy Evaluation

    jojoldu JPA N+1문제 및 해결 방안

    workbench로 erd 추출

    쟈미님블로그

    댓글

Designed by Tistory.