네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'전체 글'에 해당되는 글 194건


반응형

테이블의 PK가 복합키로 이루어져 있다면 엔티티를 설계할 때 고려해야 한다.

 

* 복합키 설정 방법은 두가지가 존재한다.

1. @Embeddable 이용 

2. @IdClass 이용

 

@Embeddable 이용

CREATE TABLE year_user_score (
    year CHAR(4) NOT NULL,
    user_id BIGINT NOT NULL,
    score INTEGER,
    PRIMARY KEY (year, user_id)
);

year_user_score 테이블은 PK는 year와 user_id 두 개의 복합키로 이루어져 있다.

 

@EmbededId를 이용하여 엔티티를 설계할 때는 우선 Serializable 인터페이스를 구현한 클래스를 선언하고 필드에 복합키로 사용되는 칼럼을 선언하면 된다.

@Embeddable
public class ScoreId implements Serializable {
    
    @Column(name = "year")
    private String year;
    
    @Column(name = "user_id")
    private Long userId;
}

복합키 클래스를 생성했으니 엔티티와 결합해준다.

@Table(name = "year_user_score")
@Entity
public class Score {
    
    @EmbededId
    private ScoreId id;
    
    private int score;
}

 

@IdClass 이용

위와 같은 테이블이 존재한다.

 

Serializable 인터페이스를 구현한 PK 클래스를 선언하고 필드를 정의한다.

public class ScoreId implements Serializable {
    
    private String year;
    private Long userId;
}

엔티티 클래스에 @IdClass(ScoreId.class) 설정해준다.

@Table(name = "year_user_score")
@Entity
@IdClass(ScoreId.class)
public class Score {
    
    @Id
    @Column(name = "year")
    private String year; // ScoreId의 필드 이름이 동일해야 함.
    
    @Id
    @Column(name = "user_id")
    private Long id;
    
    private int score;
}
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Batch Insert 

여러 건의 데이터를 입력할 때 아래와 같이 insert 구문을 실행하지 않고,

INSERT INTO test_data (user_id, uuid, created_at)
values ('jiyoon', '2efd2159-471d-4727-8eb0-20ae5a9810d3', '2022-03-19 04:41:12.59');
INSERT INTO test_data (user_id, uuid, created_at)
values ('nero', '49294b38-8afe-48a0-ab4b-4975ac333e56', '2022-02-05 09:14:00.04');

아래와 같이 멀티라인 insert 구문이 훨씬 효율적이다. 이를 Batch Insert라고 한다.

INSERT INTO test_data (user_id, uuid, created_at) values
('jiyoon', '2efd2159-471d-4727-8eb0-20ae5a9810d3', '2022-03-19 04:41:12.59'),
('nero', '49294b38-8afe-48a0-ab4b-4975ac333e56', '2022-02-05 09:14:00.04');

Batch Insert With JPA

- Hibernate의 Batch Insert 제약 사항

식별자 생성에 IDENTITY 방식을 사용하면 Hibernate가 JDBC 수준에서 batch insert를 비활성화한다.

Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.
출처: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch-insert

비활성화를 진행하는 이유는 새로 할당할 Key 값을 미리 알 수 없는 IDENTITY 방식을 사용할 때 Batch Support를 지원하면 Hibernate가 채택한 flush 방식인 ' Transactional Write Behind'와 충돌하기 때문에 IDENTITY방식에서는 Batch Insert는 동작하지 않는다.

 

그렇다고 Batch Insert를 적용하기 위해 IDENTITY 방식말고 섣불리 SEQUENCE 방식이나 TABLE 방식을 잘못 사용하면 더 나쁜 결과를 불러올 수 있다. 채번에 따른 부하가 상당히 큰 SEQUENCE 방식이나 TABLE 방식을 별다른 조치 없이 사용하면 Batch Insert를 쓸 수 없는 IDENTITY 방식보다 더 느리다. (참고: JPA GenerationType에 따른 INSERT 성능 차이)

 

Spring Data JDBC

- jdbcTemplate.batchUpdate()

JdbcTemplate에는 Batch를 지원하는 batchUpdate() 메서드가 마련되어 있다. 여러가지로 Overloading 되어있어 편리한 메서드를 골라서 사용하면 된다. 여기서는 batch 크기를 지정할 수 있는 BatchPreparedStatementSetter를 사용하는 아래 메서드를 구현해보자.

batchUpdate(String sql, BatchPreparedStatementSetter pss);

 

- ItemJdbc 객체를 ITEM_JDBC 테이블에 Batch Insert로 저장한다고 가정하자.

- batchSize 변수를 통해 배치 크기를 지정하고 전체 데이터를 배치 크기로 나눠서 Batch Insert를 실행하자.

 

ItemJdbcRepository.java

public interface TestDataJdbcRepository {
    void saveAll(List<TestData> dataList);
}

 

ItemJdbcRepositoryImpl.java

@Repository
@RequiredArgsConstructor
public class TestDataJdbcRepositoryImpl implements TestDataJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    @Value("${batchSize}")
    private int batchSize;

    @Override
    public void saveAll(List<TestData> dataList) {
        int batchCount = 0;
        List<TestData> subItems = new ArrayList<>();
        for (int i = 0; i < dataList.size(); i++) {
            subItems.add(items.get(i));
            if ((i + 1) % batchSize == 0) {
                batchCount = batchInsert(batchSize, batchCount, subItems);
            }
        }
        
        // 나머지 subItems를 insert
        if (!subItems.isEmpty()) {
            batchCount = batchInsert(batchSize, batchCount, subItems);
        }
        
        System.out.println("batchCount: " + batchCount);
    }

    private int batchInsert(int batchSize, int batchCount, List<TestData> subItems) {
        // batchUpdate(String sql, BatchPreparedStatementSetter pss) 사용
        jdbcTemplate.batchUpdate("INSERT INTO TEST_DATA (`USER_ID`, `UUID`) VALUES (?, ?)",
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, subItems.get(i).getUserId());
                        ps.setString(2, subItems.get(i).getUuid());
                    }
                    @Override
                    public int getBatchSize() {
                        return subItems.size();
                    }
                });
                
        subItems.clear();
        batchCount++;
        return batchCount;
    }
}

 

요약

- 많은 데이터를 batch insert 하고 싶을 때는, Spring Data JDBC의 batchUpdate()를 활용하자.

- Sprind Data JPA를 사용해야 한다면 IDENTITY 방식 말고 SEQUENCE 방식을 사용하는 것이 좋다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Fetch Join 


JPQL(Java Persistence Query Language)에는 경로 표현식이 있다.

경로 표현식이란 점(.)으로 객체 그래프를 탐색하는 것이다.

SELECT m.name FROM Member m  // 상태 필드
SELECT m.team FROM Member t  // 단일 값 연관 경로
SELECT t.members FROM Team t  // 컬렉션 값 연관 경로

* 상태 필드(state field): 단순하게 값을 저장하는 필드

* 연관 필드: 연관 관계를 위한 필드

    - 단일 값 연관 필드: @ManyToOne, @OneToOne 처럼 xxxToOne 관계, 대상이 엔티티

    - 컬렉션 값 연관 필드: @ManyToMany, @OneToMany 처럼 xxxToMany 관계, 대상이 컬렉션

 

[경로 표현식 특징]

* 상태 필드: 경로 탐색의 끝으로, 더이상 탐색이 불가능하다.

* 단일 값 연관 경로: 묵시적 내부 조인이 발생하며 추가적인 경로 표현식으로 탐색이 가능

* 컬렉션 값 연관 경로: 묵시적 내부 조인이 발생하며 더이상 탐색이 불가하다.

    - From 절에서 명시적 조인을 통해 별칭(alias)을 얻으면 별칭을 통해 탐색이 가능하다.

 

** 되도록 묵시적 내부 조인이 되도록 JPQL을 작성하지 않도록 JPQL을 작성하지 말 것.

묵시적 내부 조인이 발생하면 의도치 않은 조인 쿼리가 발생할 수 있으며 발견하기 어렵다. 성능과 연관이 있을 수 있기 때문에 명시적 조인을 사용한다.

 

[상태 필드 경로 탐색]

List<Integer> members = em.createQuery("SELECT m.age FROM Member m", Integer.class).getResultList();

* Hibernate

select m.age from Member m

* SQL

select member0_.age as col_0_0_ from Member member0_

 

- 상태 필드는 추가적인 조인이 없다.

 

[단일 값 연관 경로 탐색]

List<Team> teams = em.createQuery("SELECT m.team FROM Member m", Team.class).getResultList();

* Hibernate

select m.team from Member m

* SQL

select

    team1_.team.id as team_id1_3_, team1_.name as name2_3_

from

    Member member0_

    inner join

        Team team1_ on member0_.team_id = team1_.team_id

 

- 단일 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생한다.

 

[컬렉션 값 연관 경로 탐색]

// 컬렉션 값을 조회 시, TypedQuery 제네릭을 Collection으로 지정해야 한다.
List<Collection> resultList = em.createQuery("SELECT t.members FROM Team t", Collection.class)
                                .getResultList();

* Hibernate

select t.members from Team t

* SQL

select 

    members1_.member_id as member_i1_0_, members1_.age as age2_0_, members1_.team_id as team_id4_0_, members1_.username as username3_0_

from 

    Team team0_

    inner join

        Member members1_ on team0_team_id = members1_.team_id

 

- 컬렉션 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생하며 값을 받는 클래스 타입을 'Collection'으로 지정해야 한다.

 

** 묵시적 조인 시 주의사항

- 항상 내부 조인이 일어난다.

- 컬렉션은 경로 탐색의 끝으로, 더이상 점을 찍어 탐색이 불가하다. alias를 사용하여 경로 탐색을 계속 할 수 있다.

- 경로 탐색은 주로 select, where 절에서 사용되나, 묵시적 조인으로 인해 SQL의 FROM(Join)에 영향을 준다.

 

 


Fetch Join (페치 조인)

JPQL에서 지원하는 기능으로 '성능 최적화'를 위해 사용한다.

연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.

JOIN FETCH 명령어를 사용한다.

SELECT m FROM Member m JOIN FETCH m.team

 

- 즉시 로딩처럼 동작한다.

 

[페치 조인과 일반 조인 차이점]

 

 가정  Member:Team = N:1 다대일 관계일 때, fetch 전략을 EAGER (즉시 로딩)으로 설정했다

Member member = em.find(Member.class, 5L);
membe.getTeam().getName();

* SQL

select 

    member0_.member_id as member_i1_0_0_, member0_.age as age2_0_0_, member0_.team_id as team_id4_0_0_, member0_.username as username3_0_0_, team1_.team_id as team_id1_3_1_, team1_.name as name2_3_1_

from

    Member member0_

left outer join Team team1_

on member0_.team_id = team1_.team_id

where member0_.member_id = ?

 

=> 즉시 로딩으로 설정했기 때문에 em.find()할 때 Member에 연관된 Team까지 조회해서 영속성 컨텍스트에 저장한다.

 

 문제  일반 JOIN

Member member = em.createQuery("SELECT m FROM Member m JOIN m.team WHERE m.id = :id", Member.class)
                  .setParameter("id", 5L)
                  .getSingleResult();
                  
member.getTeam().getName();

* Hibernate

SELECT m FROM Member m JOIN m.team WHERE m.id = :id

 

* SQL

select 

    member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_

from

    Member member0_

left outer join Team team1_

on member0_.team_id = team1_.team_id

where member0_.member_id = ?

 

select

    team0_.team_id as team_id1_3_0_, team0_.name as name2_3_0_

from

    Team team0_

where

    team0_team_id = ?

 

JPQL로 명시적 조인을 사용하여 Member 와 Team을 한번에 가져왔다. Member와 Team은 즉시로딩으로 설정되어 있다.

하지만, SQL이 두 번 출력되는 문제가 발생한다.

 

JPQL에서 Member만 select 하고 Team은 select 하지 않았기 때문에 Member 엔티티에만 값이 채워져 있고 Team 엔티티는 비어있는 상태다.

team을 구하고 실제로 값을 사용할 때(위 코드에서 member.getTeam().getName()에서)

select 쿼리로 Team 엔티티를 가져온다.

=> 쿼리가 2번이 나간다 => N+1 문제가 발생한다.

 

 해결  페치 조인 사용 또는 명시적 조인

페치 조인

SELECT m from Member m JOIN FETCH m.team WHERE m.id = :id

명시적 조인

SELECT m, t FROM Member m JOIN m.team t WHERE m.id = :id

 

 결론 

일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.

페치 조인 실행 시 연관된 엔티티를 함께 조회한다.

 

[페치 조인을 사용하는 이유]

- 대부분 엔티티 간의 연관관계는 지연로딩(LAZY)로 되어있다. 하지만 상황에 따라 연관된 엔티티를 한번에 함께 끌어외야 할 상황이 존재하기 때문에, 그럴 때 Fetch Join을 사용하여 즉시 로딩(EAGER)처럼 동작하게 한다. (*LAZY를 적용해도 페치 조인이 우선시 된다.)

 

[컬렉션 페치 조인]

- 컬렉션 페치 조인은 일대다 관계에서 사용할 수 있으며 데이터가 많아질 수 있다.

=> DISTINCT로 중복 제거가 가능하다.

 

* SQL의 DISTINCT

- ROW가 완벽히 일치해야 중복 제거

* JPQL의 DISTINCT

- SQL의 DISTINCT 기능 뿐만 아니라, 동일한 엔티티면 중복 제거(Application단에서 엔티티 중복을 제거한다.)

즉, 같은 식별자를 가진 Entity는 중복 제거가 가능하다.

 

[페치 조인의 특징과 한계]

 문제 

- 페치 조인의 대상에는 원칙상 별칭을 줄 수 없다.

    * Hibernate는 가능하지만, 가급적 사용을 지양한다.

- 둘 이상의 컬렉션은 페치 조인을 할 수 없다.

- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

    * 일대일, 다대일같은 단일 값 연관 필드들은 페치 조인을 해도 페이징이 가능하다. (데이터 증복이 없음)

    * Hibernate는 경고로그를 남기며 메모리에서 페이징을 한다.

      일단, 전체 데이터를 가져와 메모리에 올려놓고, 메모리에서 N개씩 데이터를 결과로 보여준다. 자칫 OutOfMemory가 발생할 수 있고, 성능에 영향을 준다.

 

 해결 

1) @BatchSize

N+1 문제를 테이블 수 + 1로 줄인다. SQL의 IN 쿼리를 사용한다.

2) JPQL에서 DTO로 조회한다. SELECT new com.jiyoon.dto.TestDTO(...) from Test t ...

 

 결론 

- 모든 것을 페치 조인으로 해결할 수는 없다. QueryDSL을 사용하는 것이 바람직한 경우가 많다.

- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.

- 여러 테이블을 조인해 엔티티가 가진 모양이 다른 결과를 내야한다면, 페치 조인보다 일반 조인이 효과적이고, 필요한 데이터만 조회해 DTO로 반환하는 것이 좋다.

    * FETCH JOIN으로 엔티티 조회

    * Entity를 조회해 DTO로 변환

    * JPQL에서 바로 DTO를 조회

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# 아이템27. 비검사 경고를 제거하라

 

제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게될 것이다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이다.
Set<String> set = new HashSet();
$ javac WarningTest.java -Xlint:unchecked
WarningTest.java:8: [unchecked] unchecked conversion
Set<String> set = new HashSet();
                      ^
required: Set<String>
found: HashSet
1 warning

 

* javac 명령 인수에 -Xlint:uncheck 를 추가하면 해당 에러를 볼 수 있다.
 
컴파일러가 알려준 대로 수정하면 경고가 사라진다. 사실 컴파일러가 알려준 타입 매개변수를 명시하지 않고, 자바 7부터 지원하는 다이아몬드 연산자(<>)만으로 해결할 수 있다. 그러면 컴파일러가 올바른 실제 타입 매개변수를 추론해준다.
Set<String> set = new HashSet<>();
제거하기 훨씬 어려운 경고도 있다. 할 수 있는 한 모든 비검사 경고를 제거하라. 모두 제거한다면 그 코드는 타입 안정성이 보장된다. 즉, 런타임에 ClassCastException이 발생할 일이 없고, 여러분이 의도한 대로 잘 동작하리라 확신할 수 있다.
경고를 제거할 수는 없지만 타입이 안전하다고 확신할 수 있다면 @SuppressWarning("unchecked") 어노테이션을 달아 경고를 숨기자.
단, 타입 안전함을 검증하지 않은 채 경고를 숨기면 스스로에게 잘못된 보안 인식을 심어주는 꼴이다. 그 코드는 경고없이 컴파일이 되겠지만, 런타임에는 여전히 ClassCastException을 던질 수 있다. 한편 안전하다고 검증된 비검사 경고를 그대로 두면, 진짜 문제를 알리는 새로운 경고가 나와도 눈치채지 못할 수 있다. 제거하지 않은 수많은 거짓 경고 속에 새로운 경고가 파묻힐 것이기 때문이다.
@SuppressWarnings 어노테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 @SuppressWarnings 어노테이션은 항상 가능한 좁은 범위에 적용하자. 보통은 변수 선언, 아주 짧은 메서드, 혹은 생성자가 될 것이다. 자칫 심각한 경고를 놓칠 수 있으니 절대로 클래스 전체에 적용해서는 안된다.
한줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 어노테이션을 발견하면 지역변수 선언 쪽으로 옮기자.
 

## 지역변수를 추가해 @SuppressWarning의 범위를 좁힌다.

public <T> T[] toArray(T[] a) {
  if (a.length < size) {
    // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환이다.
    @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
  }

  System.arraycopy(elements, 0, 1, 0, size);
  if (a.length > size)
    a[size] = null;
  return a;
}
이 코드는 깔끔하게 컴파일되고 비검사 경고를 숨기는 범위로 좁혔다.
@SuppressWarnings("unchecked") 어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

JPA 특징 중에 하나인 Criteria Queries

- raw SQL문 없이 객체 지향적으로 쿼리를 작성할 수 있다.

1. Maven Dependencies

pom.xml 파일에 Hibernate 관련 의존성을 추가해준다.

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>   
    <version>5.3.2.Final</version>
</dependency>

참고로 spring-booy-starter-data-jpa에는 hibernate관련 의존성이 들어있다. spring data jpa를 사용하는 경우 hibernate 관련 의존성을 추가하지 않아도 된다.
또, spring boot에서 의존성을 추가할 때는 버전을 입력하지 않아도 spring boot에서 자동으로 버전 관리를 해준다.

spring-boot-starter-data-jpa에 hibernate 관련 의존성이 있다.

2. 예제

Item.java

@Getter
@Setter
public class Item implements Serializable {
    
    private Integer itemId;
    private String itemName;
    private String itemDescription;
    private Integer itemPrice;
}


데이터베이스에서 Item의 모든 row를 가져오는 간단한 criteria query를 작성해보자.

Session session = HibernateUtil.getHibernateSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Item> cr = cb.createQuery(Item.class);
Root<Item> root = cr.from(Item.class);
cr.select(root);

Query<Item> query = session.createQuery(cr);
List<Item> results = query.getResultList();


[where() 메서드를 사용하여 구체적인 조건을 작성]
1. 가격이 1000 초과인 Item을 가져오기

cr.select(root).where(cb.gt(root.get("itemPrice"), 1000)); // 가격이 1000 초과인 Item
cr.select(root).where(cb.lt(root.get("itemPrice"), 1000)); // 가격이 1000 미만인 Item
cr.select(root).where(cb.like(root.get("itemName"), "%chair%")); // 이름에 chair를 포함하는 Item
cr.select(root).where(cb.between(root.get("itemPrice"), 100, 200)); // 가격이 100과 200 사이인 Item
cr.select(root).where(root.get("itemName").in("Skate Board", "Paint", "Glue")); // 이름이 Skate Board 거나 Paint 거나 Glue인 Item
cr.select(root).where(cb.isNull(root.get("itemDescription"))); // 설명이

2. 가격이 1000 미만인 Item 가져오기

cr.select(root).where(cb.lt(root.get("itemPrice"), 1000));

3. 가격이 100과 200 사이인 Item 가져오기

cr.select(root).where(cb.between(root.get("itemPrice"), 100, 200));

4. 이름이 Skate Board, Paint, Glue인 Item 가져오기

cr.select(root).where(root.get("itemName").in("Skate Board", "Paint", "Glue"));

5. 두개 이상의 조건이 있을 땐 Predicate를 사용한다.

Predicate[] predicates = new Predicate[2];
predicates[0] = cb.isNull(root.get("itemDescription"));
predicates[1] = cb.like(root.get("itemName"), "%chair%");
cr.select(root).where(predicates);
Predicate greaterThanPrice = cb.gt(root.get("itemPrice"), 1000));
Predicate chairItems = cb.like(root.get("itemName"), "%chair%");

cr.select(root).where(cb.or(greaterThanPrice, chairItems));
cr.select(root).where(cb.and(greaterThanPrice, chairItems));

[orderBy(), asc(), desc() 메서드로 sorting]

cr.orderBy(cb.asc(root.get("itemName")), cb.desc(root.get("itemPrice")));

[projections, aggregates, grouping function]

CriteriaQuery<Long> cr = cb.createQuery(Long.class);
Root<Item> root = cr.from(Item.class);
cr.select(cb.count(root));
Query<Long> query = session.createQuery(cr);
List<Long> itemProjected = query.getResultList();


CriteriaUpdate(JPA 2.1부터)

CriteriaUpdate<Item> criteriaUpdate = cb.createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
criteriaUpdate.set("itemPrice", newPrice);
criteriaUpdate.where(cb.equal(root.get("itemPrice"), oldPrice));

Transaction transaction = session.beginTransaction();
session.createQuery(criteriaUpdate).executeUpdate();
transactionl.commit();

set() 메서드를 사용해서 새로운 값을 넣어준다.

CriteriaDelete

CriteriaDelete<Item> criteriaDelete = cb.createCriteriaDelete(Item.class);
Root<Item> root = criteriaDelete.from(Item.class);
criteriaDelete.where(cb.greaterThan(root.get("itemPrice"), targetPrice));

Transaction transaction = session.beginTransaction();
session.createQuery(criteriaDeleter).executeUpdate();
transaction.commit();








728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !