2주차 데이터베이스 다루기

데이터베이스 생성 (H2)

  • Server Mode
    • 외부에서 DB 엔진 구동
    • 애플리케이션을 종료시 데이터가 사라지지 않음.
    • 배포용도
  • In-memory Mode
    • 엔진을 설치하지 않고 애플리케이션 내부의 엔진을 사용
    • 데이터를 애플리케이션의 메모리에 저장.
    • 애플리케이션 종료시 데이터가 사라짐.
    • 테스트 용도
  • Embedded Mode
    • 엔진을 설치하지 않고 애플리케이션 내부의 엔진을 사용.
    • 데이터를 애플리케이션 외부에 저장.
    • 애플리케이션 종료시 데이터가 사라지지 않음.
    • 개발 용도

인메모리 모드 설정 방법

# application.yml
spring:  
    datasource:    
        driver-class-name: org.h2.Driver
        url: jdbc:h2:mem:{DB 이름}
        username: sa
        password:

임베디드 모드 설정 방법

# application.yml
spring:
  datasource:
      driver-class-name: org.h2.Driver
      url: jdbc:h2:{DB가 저장될 경로}    
      username: sa    
      password:

alt text

h2 콘솔은 spring boot를 통해 여는 것이긴 하지만 application.yml 설정과 상관 없이 콘솔 입력 값에 따라 동작한다.

데이터베이스에서 데이터 다루기(SQL)

  • DDL: 데이터 정의 언어
    • 테이블 생성 (CREATE TABLE)
    • 테이블 수정 (ALTER TABLE)
    • 테이블 삭제 (DROP TABLE)
  • DML: 데이터 조작 언어
    • SELECT문 (SELECT, WHERE, ORDER BY, GROUP BY, JOIN ...)
    • 데이터 추가 (INSERT INTO)
    • 데이터 업데이트 (UPDATE)
    • 데이터 삭제 (DELETE FROM)
  • DCL: 데이터 제어 언어

트랜잭션

  • 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위
  • 원자성 (All of Noting)
    • 트랜잭션이 데이터베이스에 모두 반영되던가, 아니면 전혀 반영되지 않아야 한다.
    • 트랜잭션은 논리적인 작업 단위.
  • 일관성 (Keeps Data Correct)
    • 작업 처리 결과가 항상 일관성이 있어야 한다.
    • 트랜잭션 중에 데이터베이스가 변경되어도 이전 데이터베이스를 바탕으로 작업한다.
  • 독립성 (Independent)
    • 둘 이상의 트랜잭션이 동시에 실행되고 있을 경우 다른 트랜잭션의 연산에 끼어들 수 없다.

데이터베이스 연결 (Driver)

  • 드라이버의 역할: 애플리케이션의 요청을 데이터베이스가 이해할 수 있는 언어로 변환.
  • 드라이버의 종류: 데이터베이스 시스템마다 호환되는 드라이버가 있음.

드라이버의 동작 방식

  • 연결 초기화
    • 요청 수신: 애플리케이션은 데이터베이스 작업을 시작하기 위해 드라이버에 연결을 요청.
    • 연결 설정: 드라이버는 데이터베이스 서버에 로그인하고 필요한 설정을 수행하여 연결을 완료.(네트워크 정보, 인증 자격 증명 등 사용)
  • SQL 전송 및 실행
    • SQL 명령 변환: 애플리케이션에서 발송된 SQL 명령을 데이터베이스가 이해할 수 있는 형태로 변환.
    • 명령 처리: 변환된 명령은 데이터베이스 서버로 전송되어 실행.
  • 결과 처리
    • 결과 수신: 데이터베이스에서 작업의 결과가 도착하면 애플리케이션에서 해석할 수 있는 형태로 변환.
    • 결과 전달: 결과를 애플리케이션에 전달.
  • 연결 종료
    • 드라이버는 데이터베이스 서버와의 연결을 종료.

alt text

JDBC Driver Manager는 런타임 시점에 Connection(데이터베이스 서버와 연결 함), Statement(쿼리를 요청하게 함), ResultSet(쿼리 결과를 받아올 수 있게 함) 객체를 생성한다.

데이터베이스 데이터를 외부에서 다루기 (JDBC)

spring-boot-starter-jdbc

  • JDBC API 지원: JDBC API를 통해 SQL 데이터베이스에 접근하고 작업을 수행할 수 있음.
  • DataSource 구성: 데이터 소스 연결을 위한 기본적인 설정을 자동으로 구성.
  • JdbcTemplate: Spring의 핵심 클래스 중 하나. SQL 쿼리 실행, 결과 세트 처리, 예외 처리 등을 단순화.
  • 장점
    • 간소화된 데이터베이스 연결: DataSource, JdbcTemplate 사용을 통해 JDBC 코드 간소화.
    • 자동 구성: 대부분의 구성을 자동으로 처리.
    • 효율적인 예외 처리: DataAccessException을 통해 일관될 예외 체계로 변환.

JDBC 실습

// JdbcApplication.java

package com.thesun4sky.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JdbcApplication {

    public static void main(String[] args) throws SQLException {
        // 어플리케이션 실행 컨텍스트 생성
        SpringApplication.run(JdbcApplication.class, args);

        // 데이터베이스 연결정보
        String url = "jdbc:h2:mem:test";     // spring.datasource.url
        String username = "sa";                // spring.datasource.username

        // connection 얻어오기
        try (Connection connection = DriverManager.getConnection(url, username, null)) {
            try {
                // 테이블 생성 (statement 생성)
                String creatSql = "CREATE TABLE USERS (id SERIAL, username varchar(255))";
                try (PreparedStatement statement = connection.prepareStatement(creatSql)) {
                    statement.execute();
                }

                // 데이터 추가 (statement 생성)
                String insertSql = "INSERT INTO USERS (username) VALUES ('teasun kim')";
                try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
                    statement.execute();
                }

                // 데이터 조회 (statement 생성 후 rs = resultSet 수신 & next() 조회)
                String selectSql = "SELECT * FROM USERS";
                try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
                    var rs = statement.executeQuery();
                    while (rs.next()) {
                        System.out.printf("%d, %s", rs.getInt("id"), rs.getString("username"));
                    }
                }
            } catch (SQLException e) {
                if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
                    System.out.println("USERS 테이블이 이미 존재합니다.");
                } else {
                    throw new RuntimeException();
                }
            }
        }
    }
}

PreparedStatement란?

  • Statement를 상속하고 있는 Interface로 내부적으로 Statement의 4단계 중 첫 번째(parse) 과정의 결과를 캐싱하고 나머지 3 단계만 거쳐 SQL 문이 실행될 수 있게 한다. (성능 향상)

alt textalt text

String을 만들 때 + 연산자를 사용하는 것과 %s 등으로 포맷팅 하는 것의 차이와 비슷.

JDBC Template

JDBC로 직접 SQL을 작성했을 때는 자원 관리를 따로 해줘야 하고, 중복 코드도 자주 발생한다는 문제가 있음. 이를 해결하기 위해 Persistence Framework가 등장.

  • Persistence Framework
    • SQL Mapper: JDBC Template, MyBatis
    • ORM: JPA, Hibernate
  • SQL Mapper(QueryMapper)
    • SQL <-> Object
    • SQL 문과 객체의 필드를 매핑하여 데이터를 객체화
  • RowMapper
    • 쿼리 수행 결과와 객체 필드 매핑
    • 응답필드 매핑코드 재사용.
// UserRowMapper.java
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;


public class UserRowMapper implements RowMapper<User> {

    // JDBCTemplate 에서 row 응답을 mapRow() 메서드에 rs 파라미터로 넘겨주어 객체에 매핑하기 쉽도록 도와준다.
  @Override
  public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    var user = new User();
    user.setId(rs.getInt("ID"));
    user.setName(rs.getString("NAME"));
    return user;
  }
}

// 레지스토리
// 사용자 ID로 User 조회 (Read)
public User findUserById(Long id) {
    return jdbcTemplate.queryForObject(
        "SELECT * FROM users WHERE id = ?",
        new UserRowMapper(),  // 이자리에 매퍼를 생성자로 넣어주면 됨
        id
    );
}

3주차 RawJPA 기본

쿼리 파일 만들기 (QueryMapper)

MyBatis

  • 코드의 설정(Connection) 부분을 줄여 반복적인 JDBC 프로그래밍 단순화.
  • SQL 쿼리를 XML 파일에 작성하여 코드와 SQL 분리.
  • 다른 방식에 비해 객체 보다는 쿼리에 집중 가능.

단점

  • 결국 SQL을 직접 작성해야 한다는 한계가 있음.
  • DB타입 및 테이블에 종속적임.

쿼리 파일

<!-- UserMapper.xml -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.thesun4sky.querymapper.mapper.UserMapper">
    <select id="selectUserById" resultType="User">
        select id, name from users where id = #{id}
    </select>
</mapper>

Dao 클래스 정의

// UserDao.java
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Component;

import com.thesun4sky.querymapper.domain.User;

@Component
public class UserDao {

  // SqlSession 멤버 변수로 사용하며 쿼리파일 수행 요청
  private final SqlSession sqlSession;

  public UserDao(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }

  public User selectUserById(long id) {
    return this.sqlSession.selectOne("selectUserById", id);
  }

}

select id(selectUserById)를 sqlSession.selectOne의 첫 파라미터로 넣는다. 다음 파라미터부터는 쿼리의 변수들을 받는다.

Mapper Interface 정의

// UserMapper.java
@Mapper
public interface UserMapper {

  User selectUserById(@Param("id") Long id);

}

select id(selectUserById)와 메서드 이름을 맞춘다. 파라미터로는 쿼리의 변수들을 받는다.

쿼리 코드 만들기 (JpaRepository)

ORM은 테이블을 하나의 객체와 대응시킨다.

테이블과 객체를 매핑할 때 문제점

  • 상속의 문제
    • 문제점: 객체에는 상속관계를 맺을 수 있으나 RDB는 모두 독립적으로 존재함.
    • 해결법: 매핑정보에 상속정보를 넣어줌. (@OneToMany, @ManyToOne)
  • 관계 문제
    • 문제점: 참조를 통해 관계를 가지며 방향을 가지나 RDB는 FK를 설정해 Join해야 참조 가능.
    • 해결법: 매핑정보에 방향정보를 넣어줌. (@JoinColumn, @MappedBy)
  • 탐색 문제
    • 문제점: 객체는 참조를 통해 다른 객체로 순차적 탐색 가능하나 RDB는 참조하는 만큼 추가 쿼리나 Join이 발샘함.
    • 해결법: 매핑/조회 정보로 참조탐색 시점을 관리함. (@FetchType, fetchJoin())
  • 밀도 문제
    • 문제점: 객체는 멤버 객체크기가 각양각색이나 RDB는 기본 데이터 타입만 존재함.
    • 해결법: 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리. (@embedded)
  • 식별성 문제
    • 문제점: 객체는 hashCode 또는 equals() 메소드를 통해 식하나 RDB는 PK로만 식별함.
    • 해결법: PK를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 함. (@Id, @GeneratedValue)

영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연

(영속성에 대한 정리는 많이 했었으므로 자세한 내용 생략!)

영속성이란 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.

alt text

영속성 4가지 상태 (비영속 -> 영속 -> 준영속 / 삭제)

alt text

단 키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 중복키 생성을 방지하기 위해 단일 쿼리로 수행하며 쓰기 지연이 발생하지 않는다.

JpaRepository

JpaRepository는 @NotRepositoryBean 된 인터페이스들을 상속 받고있음. Bean으로 등록하지 않을 것이라는 뜻으로 상속받으면 생성돼서 사용.

테이블 객체 이해하기

Raw JPA 타입 매핑 기능

  • @Entity
    • 객체 관점에서의 테이블
  • @Table
    • RDB의 테이블 이름. 기본값은 Entity 이름
  • @Id
    • 엔티티의 주키를 매핑할 때 사용
    • 자바의 모든 primitive 타입과 그 래퍼 클래스 사용 가능.
    • Date, BigDecimal, BigInteger도 사용 가능.
  • @GeneratedValue
    • TABLE, SEQUENCE, IDENTITY 중 하나
  • @Column
    • uniqu, nullable, length, columnDefinition... DB의 설정 넣기 위해 사용
  • @Temparal
    • 시간, 날짜 관련
  • @Transient
    • 칼럼으로 매핑하고 싶지 않은 변수에 사용

Raw JPA 필드 매핑 기능

  • 기본 타입
    • @Column: DB의 설정 넣기 위해 사용. Class에 @Entity가 붙어있으면 자동으로 필드에 @Column이 붙음.
    • @Enumerated: Enum 매핑용도로 쓰임. @Enumerated(EnumType.STRING) 으로 사용 권장. Default의 경우 순서에 영향을 받는 문제 있음.
  • Composite Value 타입
    • @Embeddable: 복합 값 객체로 사용할 클래스 지정
    • @Embedded: 복합 값 객체 적용할 필드 지정
    • @AttributeOverrides: 복합 값 객체 여러개 지정
    • @AttributeOverride: 복합 값 객체 필드명 선언
    • @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Setter @Getter public class Member{ @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id;
    • @Embeddable public class Address { private String city; private String street; }
    private String name;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "home_city")),
            @AttributeOverride(name = "street", column = @Column(name = "home_street")),
    })
    private Address homeAddress;


    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "company_city")),
            @AttributeOverride(name = "street", column = @Column(name = "company_street")),
    })
    private Address companyAddress;

}
```
  • Collection Value 타입
    • 현업에선 잘 사용하지 않음. 일대다 연관관계로 풀어 씀.
    • @ElementCollection

테이블 객체 만들기

템플릿

/**
 * 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
 */


/**
 * 생성자 - 약속된 형태로만 생성가능하도록 합니다.
 */


/**
 * 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
 */


/**
 * 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
 */


/**
 * 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
 */

샘플

// User.java
// lombok
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString

// jpa
@Entity
@Table(name = "users")
public class User {

  /**
   * 컬럼 - 연관관계 컬럼을 제외한 컬럼을 정의합니다.
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  private String username;

  private String password;

  /**
   * 생성자 - 약속된 형태로만 생성가능하도록 합니다.
   */
  @Builder
  public User(String username, String password) {
    this.username = username;
    this.password = password;
  }

  /**
   * 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
   */
  @OneToMany
  @Exclude
  private Set<UserChannel> userChannel;

  /**
   * 연관관계 편의 메소드 - 반대쪽에는 연관관계 편의 메소드가 없도록 주의합니다.
   */

  /**
   * 서비스 메소드 - 외부에서 엔티티를 수정할 메소드를 정의합니다. (단일 책임을 가지도록 주의합니다.)
   */
  public void updateUserName(String username) {
    this.username = username;
  }

  public void updatePassword(String password) {
    this.password = password;
  }
}

테이블 객체끼리 관계만들기

  • @OneToOne
  • @OneToMany
    • 단방향으로 쓰이면 문제가 발생할 수 있음.
    • 속성: mappedBy, fetch(기본 LAZY), cascade, targetEntity...
  • @ManyToOne
    • optional(default true): false로 설정하면 연관된 엔티티가 반드시 있어야 함.
    • fetch: 기본 EGEAR이나 LAZY 추천.
    • cascade: 영속성 전이
    • targetEntity: 연관된 엔티티의 타입 정보 설정
  • @ManyToMany
    • 실무에서는 사용하지 않음. 중간 테이블 생성.

4주차 SpringData JPA 기본

테이블 객체 다루는법

Cascade (영속성 전이)

  • 부모 엔티티(OneToMany, OneToOne이 있는 곳)에서 사용
  • 양쪽 엔티티의 라이프사이클이 비슷할 경우
  • 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 함
  • 옵션 종류
    • ALL: 전체 상태 전이
    • PERSIST: 저장 상태 전이
    • REMOVE: 삭제 상태 전이
    • MERGE: 업데이트 상태 전이
    • REFRESH: 갱신 상태 전이
    • DETACH: 비영속성 상태 전이

orhanRemoval (고아 객체 제거)

  • 부모 엔티티에서 사용
  • 부모 객체에서 리스트 요소를 삭제했을 때 자식 객체는 매핑정보가 없어져 고아 객체가 되므로 삭제.

보통 둘 다 같이 사용.

Fetch (조회시점)

  • Entity에 FetchType으로 설정
  • 기본적으로 LAZY를 설정하고 필요할 때만 fetch Join을 수행하는 것이 좋다.
  • 항상 같이 쓰일 경우에는 EAGER를 사용하자.

테이블 객체로 자동 쿼리 생성하기

구조와 작동 원리

alt text

@SpringBootApplication을 통해 자동으로 붙여지는 @EnableJpaRepositories의 JpaRepositoriesRegistrar를 통해 등록된다. JrpRepositoriesRegistrar는 ImportBeanDefinitionRegistrar의 구현체로 프로그래밍을 통해 빈을 주입해준다.

JpaRepository 쿼리 사용 방법

리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)(And|Or){프로퍼티 표현식}(조건식) (매개변수...)

   
접두어 Find, Get, Query, Count, ...
도입부 Distinct, First(N), Top(N)
프로퍼티 표현식 Person.Address.ZipCode => find(Person)ByAddress_ZipCode(...)
조건식 IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, ...
정렬 조건 OrderBy{프로퍼티}Asc
리턴 타입 E, Optional, List, Page, Slice, Stream
매개변수 Pageable, Sort

예시 코드

// 기본
List<User> findByNameAndPassword(String name, String password);

// distinct (중복제거)
List<User> findDistinctUserByNameOrPassword(String name, String password);
List<User> findUserDistinctByNameOrPassword(String name, String password);

// ignoring case (대소문자 무시)
List<User> findByNameIgnoreCase(String name);
List<User> findByNameAndPasswordAllIgnoreCase(String name, String password);

// 정렬
List<Person> findByNameOrderByNameAsc(String name);
List<Person> findByNameOrderByNameDesc(String name);

// 페이징
Page<User> findByName(String name, Pageable pageable);  // Page 는 카운트쿼리 수행됨
Slice<User> findByName(String name, Pageable pageable); // Slice 는 카운트쿼리 수행안됨
List<User> findByName(String name, Sort sort);
List<User> findByName(String name, Pageable pageable);

// 스트림 (stream 다쓴후 자원 해제 해줘야하므로 try with resource 사용추천)
Stream<User> readAllByNameNotNull();

JpaRepository 효율적으로 사용하는 방법

default 메서드를 사용하여 변형한 jpa 쿼리 메서드를 사용할 수 있다.

Optional 제거하고 반환

public interface UserRepository extends JpaRepository<User, Long> {
// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리
default User findUserById(Long id) {
        return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));
    }
}

메서드명 간소화

public interface UserRepository extends JpaRepository<User, Long> {
// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리
default User findUserById(Long id) {
        return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));
    }
}

비즈니스 로직 통합(예시를 위한 코드로 현업에서는 서비스 레이어에서 처리)

public interface UserRepository extends JpaRepository<User, Long> {

// 사용자 ID로 사용자를 찾고, 존재할 경우 연락처 정보를 업데이트하는 메소드
default void updateUserContact(Long userId, String newContact) {
        findById(userId).ifPresent(user -> {
            user.setContact(newContact);
            save(user);
        });
    }
}

테이블 객체로 페이지 조회하기

페이징 요청: Pageable

Pageable 만드는 법

PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들

Pageable 메서드

pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환

페이징 응답

  • Page: 게시판 형태의 페이징에서도 사용됨. 전체 요소 갯수도 함께 조회.
  • `{ "content": [ {"id": 1, "username": "User 0", "address": "Korea", "age": 0}, ... {"id": 5, "username": "User 4", "address": "Korea", "age": 4} ], "pageable": { "sort": { "sorted": false, // 정렬 상태 "unsorted": true, "empty": true }, "pageSize": 5, // 페이지 크기 "pageNumber": 0, // 페이지 번호 (0번 부터 시작) "offset": 0, // 해당 페이지의 첫번째 요소의 전체 순번 (다음 페이지에서는 5) "paged": true, "unpaged": false }, "totalPages": 20, // 페이지로 제공되는 총 페이지 수 "totalElements": 100, // 모든 페이지에 존재하는 총 원소 수 "last": false, // 마지막 페이지 여부 "number": 0, "sort": { "sorted": false, // 정렬 사용 여부 "unsorted": true, "empty": true }, "size": 5, // Contents 사이즈 "numberOfElements": 5, // Contents 의 원소 수 "first": true, // 첫페이지 여부 "empty": false // 공백 여부 }`
  • Slice: 더보기 형태의 페이징에서 사용됨. 전체 요소 갯수 대신 offset 필드로 조회 가능하나 성능이 안 좋아서 현업에서 안 씀.
`{ "content": [ { "id": 13, "username": "User 12", "address": "Korea", "age": 12 }, ... { "id": 16, "username": "User 15", "address": "Korea", "age": 15 } ], "pageable": { "sort": { "sorted": false, "unsorted": true, "empty": true }, "pageNumber": 3, "pageSize": 4, "offset": 12, "paged": true, "unpaged": false }, "number": 3, "numberOfElements": 4, "first": false, "last": false, "size": 4, "sort": { "sorted": false, "unsorted": true, "empty": true }, "empty": false }`
  • List: 전체 목록보기 형태의 페이징에서 사용.

정렬

Sort 클래스 사용.

Sort sort1 = Sort.by("name").descending();     // 내림차순
Sort sort2 = Sort.by("password").ascending();  // 오름차순
Sort sortAll = sort1.and(sort2);      // 2개이상 다중정렬도 가능하다
Pageable pageable = PageRequest.of(0, 10, sortAll);  // pageable 생성시 추가

컬럼이 아닌 값도 Alias를 기준으로 정렬 가능.

// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u.user_name, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));

SQL 함수도 사용 가능: JpaSort 사용

// 아래와 같이 일반적인 쿼리에서
@Query("SELECT u FROM user u WHERE u.username = ?1") // 이건 없어도됨
List<User> findByUsername(String username, Sort sort);
// 이렇게 쿼리함수 LENGTH() 조건을 걸어서 password 문자길이 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", JpaSort.unsafe("LENGTH(password)"));

테이블 객체로 수동 쿼리 생성하기

Query

@Query의 인자값으로 쿼리 작성 가능.

  • 테이블명이 아니라 Entity 명으로 작성한다.
  • ?변수순번, :변수명 두 가지 사용 방법이 있다.
public interface UserRepository extends JpaRepository<User, Long> { @Query("SELECT u, u.password AS customField FROM User u WHERE u.username = ?1") List<User> findByUsernameWithCustomField(String username, Sort sort);`

@Query("SELECT u FROM User u WHERE u.username = :username")  
List findByUsername(String username, Sort sort);  
}

5주차 SpringDataJPA 심화

QueryDSL

Entity 의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스)로 재구성해주는 기술.
JPAQueryFactory는 Q클래스를 통해 객체 또는 함수로 쿼리를 작성하고 실행하게 해주는 기술로 Q클래스를 활용할 수 있는 기능들을 제공함.


@PersistenceContext  
EntityManager em;

public List selectUserByUsernameAndPassword(String username, String password){  
JPAQueryFactory jqf = new JPAQueryFactory(em);  
QUser user = QUser.user;

List<Person> userList = jpf
                            .selectFrom(user)
                            .where(person.username.eq(username)
                                .and(person.password.eq(password))
                            .fetch();

return userList;

}

config


// configuration 패키지안에 추가

@Configuration  
public class JPAConfiguration {

@PersistenceContext  
private EntityManager entityManager;

@Bean  
public JPAQueryFactory jpaQueryFactory() {  
return new JPAQueryFactory(entityManager);  
}  
}

Auditing

엔티티를 언제 생성/마지막 수정 했는지 자동으로 기록

  • 메인 애플리케이션 위에 @EnableJpaAuditing 추가
  • 엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가

@CreatedDate  
private Date created;

@LastModifiedDate  
private Date updated;

@CreatedBy  
@ManyToOne  
private Account createdBy;

@LastModifiedBy  
@ManyToOne  
private Account updatedBy;

방명록 기능에서 작성/수정한 사람을 찾기 위해 AuditorAware 구현체를 만들 수 있음.

  • SecurityContextHolder에서 UserDetailsImpl을 사용해 user 객체 가져오기.

@Service  
public class UserAuditorAware implements AuditorAware {  
@Override  
public Optional getCurrentAuditor() {  
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
    return Optional.empty();
}

return Optional.of(((UserDetailsImpl) authentication.getPrincipal()).getUser());

}

}

```

Dynamic Insert/Update

@DynamicInsert

  • Insert 문에 null인 칼럼 제외.

@DynamicUpdate

  • update 문에 null인 칼럼 제외.

불필요한 칼럼이 들어가는 것을 막아 최적화.

+ Recent posts