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:
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 명령을 데이터베이스가 이해할 수 있는 형태로 변환.
- 명령 처리: 변환된 명령은 데이터베이스 서버로 전송되어 실행.
- 결과 처리
- 결과 수신: 데이터베이스에서 작업의 결과가 도착하면 애플리케이션에서 해석할 수 있는 형태로 변환.
- 결과 전달: 결과를 애플리케이션에 전달.
- 연결 종료
- 드라이버는 데이터베이스 서버와의 연결을 종료.
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 문이 실행될 수 있게 한다. (성능 향상)
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차 캐시)를 활용한 쓰기지연
(영속성에 대한 정리는 많이 했었으므로 자세한 내용 생략!)
영속성이란 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
영속성 4가지 상태 (비영속 -> 영속 -> 준영속 / 삭제)
단 키 생성전략이 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를 사용하자.
테이블 객체로 자동 쿼리 생성하기
구조와 작동 원리
@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인 칼럼 제외.
불필요한 칼럼이 들어가는 것을 막아 최적화.
'Spring' 카테고리의 다른 글
Websocket을 알아보자. Spring, React에서 사용해보며 (1) (0) | 2024.09.30 |
---|---|
QueryDSL의 null처리 (0) | 2024.09.23 |
[Spring] 🙉Fixture Monkey🙉 (1) | 2024.06.15 |
[Spring] DB 테스트 환경 분리 (0) | 2024.06.13 |
[Spring] AOP란 무엇일까? (+Spring AOP) (0) | 2024.06.12 |