반응형
들어가며
ORM?
- Object-Relational Mapping (객체와 관계형 데이터베이스 매핑, 객체와 DB의 테이블이 매핑을 이루는 것)
- 객체가 테이블이 되도록 매핑 시켜주는 프레임워크이다.
- 프로그램의 복잡도를 줄이고 자바 객체와 쿼리를 분리할 수 있으며 트랜잭션 처리나 기타 데이터베이스 관련 작업들을 좀 더 편리하게 처리할 수 있는 방법
JPA?
- Java Persistence API (자바 ORM 기술에 대한 API 표준 명세)
- 한마디로 ORM을 사용하기 위한 인터페이스를 모아둔 것 이라고 볼 수 있다.
- 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다.
- MyBatis는 ORM이 아니고 SQL Mapper임
Hibernate?
- JPA를 사용하기 위해서 JPA를 구현한 ORM 프레임워크중 하나.
- JPA 인터페이스의 실제 구현부를 담당함
JPA의 장점
- 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 있게 도와줌
- 객체 지향적으로 데이터를 관리할 수 있기 때문에 전체 프로그램 구조를 일관되게 유지할 수 있음
- SQL을 직접 작성하지 않고 객체를 기준으로 동작하기 때문에 유지보수가 쉬움 (ex. 컬럼 수정시 해당 모델 객체 필드만 수정해주면 끝)
- 쿼리 문법 오류를 런타임시점이 아닌 컴파일시점에 미리 알 수 있음
- DBMS에 대한 코드 종속성이 줄어듬
JPA의 단점
- 학습 비용이 높음
- 통계와 같은 복잡한 쿼리 사용시 불리함 (실시간 처리용 쿼리에 최적화)
- 잘못 사용할 경우 실제 SQL문을 직접 작성하는 것보다 성능이 떨어질 수 있음
Spring JPA 기본
설명
- spring boot에 jpa를 연동하는 기본 예제
- DBMS는 MySQL을 사용
테이블 생성
CREATE TABLE IF NOT EXISTS student
(
studentSeq BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(200),
age INTEGER,
PRIMARY KEY (studentSeq)
);
Dependency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
properties 설정
# MySQL
spring.datasource.url=jdbc:mysql://mysql.server.com:3306/test?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&useSSL=false
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# SQL문에 바인딩된 파라미터값을 로그로 출력하기 위한 설정.
logging.level.org.hibernate.type.descriptor.sql=trace
# 테이블 자동 생성하지 않도록 설정.
spring.jpa.hibernate.ddl-auto=none
# 변수명을 그대로 칼럼명으로 지정 가능하도록 설정 (ex. 카멜케이스)
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# 하이버네이트가 실행하는 모든 SQL문을 콘솔로 출력해 준다.
spring.jpa.properties.hibernate.show_sql=true
# 콘솔에 출력되는 JPA 실행 쿼리를 가독성있게 표현한다.
spring.jpa.properties.hibernate.format_sql=true
# 디버깅이 용이하도록 SQL문 이외에 추가적인 정보를 출력해 준다.
spring.jpa.properties.hibernate.use_sql_comments=false
# 데이터베이스 방언 설정
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
Entity 클래스 생성
- @Id
- 기본키(PK) 지정
- @GeneratedValue
- 기본키 생성 전략 설정
- IDENTITY는 기본키 생성을 DB에 위임하는 방식
// lombok의 @Data나 @ToString 사용하면 toString() 메소드 관련 StackOverFlow 오류 발생할 수 있음
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long studentSeq;
private String name;
private Integer age;
}
Repository 클래스 생성
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
List<Student> findByName(String name);
}
CRUD 테스트
@Slf4j
@SpringBootTest
public class JpaTest {
@Autowired
private StudentRepository studentRepository;
@Test
public void insert() {
studentRepository.save(new Student(null, "john", 25));
studentRepository.save(new Student(null, "tom", 30));
}
@Test
public void update() {
studentRepository.save(new Student(1L, "john2", 26));
}
@Test
public void delete() {
//studentRepository.delete(new Student(1L, null, null));
studentRepository.deleteById(1L);
}
@Test
public void select() {
List<Student> students1 = studentRepository.findAll();
List<Student> students2 = studentRepository.findByName("john");
Optional<Student> student = studentRepository.findById(1L);
log.info("findAll : {}", students1);
log.info("findByName : {}", students2);
log.info("findById : {}", student);
}
}
join
설명
- 객체 조인을 표현하기 위해 멤버로 해당 Entity 객체를 필드로 두고 어노테이션을 달아 명시함
- 어노테이션 종류
- @OneToMany
- @ManyToOne
- @OneToOne
테스트 테이블
create table category
(
categorySeq integer primary key auto_increment,
categoryName varchar(100)
);
create table book
(
bookSeq integer primary key auto_increment,
bookName varchar(500),
bookCategorySeq integer
);
create table bookDetail
(
bookDetailSeq integer primary key auto_increment,
bookPrice integer,
bookSeq integer
);
@OneToMany
- @OneToMany
- 양방향 조인일 경우 @JoinColumn 없이 mappedBy 속성만으로 사용 가능
- mappedBy에 들어갈 내용은 Owner Entity의 이름
- default fetch type : FetchType.LAZY
- @JoinColumn
- referencedColumnName : Owner Entity에 조인 필드
- name : 상대 Entity의 조인 필드
@Getter
@Setter
@Entity(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer categorySeq;
private String categoryName;
// @OneToMany(mappedBy = "category")
@OneToMany
@JoinColumn(referencedColumnName = "categorySeq", name = "bookCategorySeq")
private List<Book> books;
}
@ManyToOne
- default fetch type : FetchType.EAGER
@Getter
@Setter
@Entity(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bookSeq;
private String bookName;
@ManyToOne
@JoinColumn(referencedColumnName = "categorySeq", name = "bookCategorySeq")
private Category category;
}
@OneToOne
- default fetch type : FetchType.EAGER
@Getter
@Setter
@Entity(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bookSeq;
private String bookName;
@OneToOne
@JoinColumn(referencedColumnName = "bookSeq", name = "bookSeq")
private BookDetail bookDetail;
}
@Getter
@Setter
@Entity(name = "bookDetail")
public class BookDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bookDetailSeq;
private Integer bookPrice;
}
@OneToOne 조회 쿼리 살펴보기
- Book과 BookDetail은 1:1 관계이기 때문에 inner join으로 한 번에 조회해오면 될 것 같은데 아래 코드를 수행해보면 book 조회, bookDetail 조회 각각 1번씩 총 2번 조회해오고있음
truncate book;
truncate bookDetail;
insert into book (bookName, bookCategorySeq) values ('book-1-1', 1);
insert into bookDetail (bookPrice, bookSeq) values (2000, 1);
// LAZY fetch되는 데이터 접근시 @Transactional 없으면 오류 발생
@Transactional(readOnly = true)
@Test
public void select1() {
Book book = bookRepository.getOne(1);
System.out.println(book.getBookName());
System.out.println(book.getBookDetail().getBookPrice());
}
Hibernate:
select
book0_.bookSeq as bookseq1_0_0_,
book0_.bookName as bookname2_0_0_
from
book book0_
where
book0_.bookSeq=?
Hibernate:
select
bookdetail0_.bookDetailSeq as bookdeta1_1_0_,
bookdetail0_.bookPrice as bookpric2_1_0_
from
bookDetail bookdetail0_
where
bookdetail0_.bookDetailSeq=?
book-1-1
2000
@OneToOne 쿼리 한 번으로 조회하기
- JPQL을 활용한 join fetch로 한 번에 조회
@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
@Query("select b from book b join fetch b.bookDetail where b.bookSeq = :bookSeq")
Book getOneJoinFetch(Integer bookSeq);
}
@Transactional(readOnly = true)
@Test
public void select1() {
Book book = bookRepository.getOneJoinFetch(1);
System.out.println(book.getBookName());
System.out.println(book.getBookDetail().getBookPrice());
}
Hibernate:
select
book0_.bookSeq as bookseq1_0_0_,
bookdetail1_.bookDetailSeq as bookdeta1_1_1_,
book0_.bookName as bookname2_0_0_,
bookdetail1_.bookPrice as bookpric2_1_1_
from
book book0_
inner join
bookDetail bookdetail1_
on book0_.bookSeq=bookdetail1_.bookDetailSeq
where
book0_.bookSeq=?
book-1-1
2000
JPA의 N+1 문제
설명
- 위 예제에서 모든 Category를 조회해온 후 각 카테고리의 Book 목록을 사용한다고 가정
- @OneToMany의 기본 fetch type은 LAZY이기 때문에 Category 목록을 조회해올 때 Book 목록을 함께 조회하지 않고, Book 목록 사용 코드가 발생하면 그 때 조회한다
- 이처럼 모든 Category를 조회하기 위해 한 번, 각 Book 목록 접근시에도 N번의 조회 쿼리가 발생하는 이슈를 말한다
테스트 데이터 추가
truncate category;
truncate book;
insert into category (categoryName) values ('category-1');
insert into category (categoryName) values ('category-2');
insert into book (bookName, bookCategorySeq) values ('book-1-1', 1);
insert into book (bookName, bookCategorySeq) values ('book-1-2', 1);
insert into book (bookName, bookCategorySeq) values ('book-1-3', 1);
insert into book (bookName, bookCategorySeq) values ('book-2-1', 2);
insert into book (bookName, bookCategorySeq) values ('book-2-2', 2);
insert into book (bookName, bookCategorySeq) values ('book-2-3', 2);
Category별 Book 목록 출력
@Transactional(readOnly = true)
@Test
public void nPlusOneIssue() {
List<Category> categories = categoryRepository.findAll();
categories.forEach(category -> {
System.out.println(
category.getBooks()
.stream()
.map(Book::getBookName)
.collect(Collectors.joining(", "))
);
});
}
실행 결과
- Category 전체 조회하는 쿼리 1번, 각 Category의 Book 목록을 조회하기 위해 Category 개수(2개)만큼 추가 조회 발생
Hibernate: select category0_.categorySeq as category1_1_, category0_.categoryName as category2_1_ from category category0_
Hibernate: select books0_.bookCategorySeq as bookcate3_0_0_, books0_.bookSeq as bookseq1_0_0_, books0_.bookSeq as bookseq1_0_1_, books0_.bookName as bookname2_0_1_, books0_.bookCategorySeq as bookcate3_0_1_ from book books0_ where books0_.bookCategorySeq=?
book-1-1, book-1-2, book-1-3
Hibernate: select books0_.bookCategorySeq as bookcate3_0_0_, books0_.bookSeq as bookseq1_0_0_, books0_.bookSeq as bookseq1_0_1_, books0_.bookName as bookname2_0_1_, books0_.bookCategorySeq as bookcate3_0_1_ from book books0_ where books0_.bookCategorySeq=?
book-2-1, book-2-2, book-2-3
solution 1 - join fetch
- JPQL을 활용한 join fetch를 통해 Category, Book 데이터를 한 번에 조회한다
@Repository
public interface CategoryRepository extends JpaRepository<Category, Integer> {
@Query("select distinct(c) from category c join fetch c.books")
List<Category> findAllJoinFetch();
}
@Test
public void nPlusOneIssue() {
List<Category> categories = categoryRepository.findAllJoinFetch();
categories.forEach(category -> {
System.out.println(
category.getBooks()
.stream()
.map(Book::getBookName)
.collect(Collectors.joining(", "))
);
});
}
Hibernate:
select
distinct category0_.categorySeq as category1_1_0_,
books1_.bookSeq as bookseq1_0_1_,
category0_.categoryName as category2_1_0_,
books1_.bookName as bookname2_0_1_,
books1_.bookCategorySeq as bookcate3_0_1_,
books1_.bookCategorySeq as bookcate3_0_0__,
books1_.bookSeq as bookseq1_0_0__
from
category category0_
inner join
book books1_
on category0_.categorySeq=books1_.bookCategorySeq
book-1-1, book-1-2, book-1-3
book-2-1, book-2-2, book-2-3
solution 2 - @EntityGraph
- join fetch와 원리는 같음
- join fetch는 inner join, @EntityGraph는 left outer join을 활용함
@Repository
public interface CategoryRepository extends JpaRepository<Category, Integer> {
@EntityGraph(attributePaths = {"books"})
@Query("select distinct(c) from category c")
List<Category> findAllJoinFetch();
}
Hibernate:
select
distinct category0_.categorySeq as category1_1_0_,
books1_.bookSeq as bookseq1_0_1_,
category0_.categoryName as category2_1_0_,
books1_.bookName as bookname2_0_1_,
books1_.bookCategorySeq as bookcate3_0_1_,
books1_.bookCategorySeq as bookcate3_0_0__,
books1_.bookSeq as bookseq1_0_0__
from
category category0_
left outer join
book books1_
on category0_.categorySeq=books1_.bookCategorySeq
book-1-1, book-1-2, book-1-3
book-2-1, book-2-2, book-2-3
QueryDSL 설정하기
QueryDSL?
- JpaRepository나 JPQL은 복잡하고 동적인 쿼리 작성이 어렵다
- QueryDSL은 쿼리를 자바 코드로 만들 수 있게 해주는 방법을 제공한다
- 요청할 쿼리를 자바 코드로 작성하기 때문에 문법 오류를 컴파일 시점에 파악할 수 있다
- 동적 쿼리 작성이 쉽다
pom.xml 설정
Java일 경우<build>
...
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
...
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
Kotlin일 경우
<build>
...
<plugins>
...
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>kapt</id>
<phase>generate-sources</phase>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>target/generated-sources/kapt/test</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
JpaConfig 설정
@Configuration
public class JpaConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
Entity 생성
@Getter
@Setter
@Entity(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer categorySeq;
private String categoryName;
}
메이븐 컴파일 실행
mvn compile
Q클래스 생성 확인
메이븐 컴파일이 완료되면 지정된 경로에 Entity별 Q클래스가 생성된다
Simple CRUD Repisitory 클래스 작성
@RequiredArgsConstructor
@Repository
public class CategoryRepository {
private final EntityManager entityManager;
private final JPAQueryFactory queryFactory;
private final QCategory category = QCategory.category;
public void insert(Category params) {
entityManager.persist(params);
}
public void update(Category params) {
queryFactory
.update(category)
.where(category.categorySeq.eq(params.getCategorySeq()))
.set(category.categoryName, params.getCategoryName())
.execute();
}
public void delete(Integer categorySeq) {
queryFactory
.delete(category)
.where(category.categorySeq.eq(categorySeq))
.execute();
}
public List<Category> findAll(String categoryName) {
return queryFactory
.selectFrom(category)
.where(
categoryName == null ? null : category.categoryName.eq(categoryName)
)
.fetch();
}
}
CRUD 테스트
@SpringBootTest
public class QueryDslCrudTest {
@Autowired
private CategoryRepository categoryRepository;
@Transactional
@Rollback(false)
@Test
public void insert() {
categoryRepository.insert(new Category(null, "category-insert"));
}
@Transactional
@Rollback(false)
@Test
public void update() {
categoryRepository.update(new Category(1, "category-update"));
}
@Transactional
@Rollback(false)
@Test
public void delete() {
categoryRepository.delete(1);
}
@Transactional(readOnly = true)
@Test
public void select() {
List<Category> categories1 = categoryRepository.findAll(null);
List<Category> categories2 = categoryRepository.findAll("category-1");
System.out.println(categories1.stream().map(Category::getCategoryName).collect(Collectors.joining(", ")));
System.out.println(categories2.stream().map(Category::getCategoryName).collect(Collectors.joining(", ")));
}
}
QueryDSL 심화
join
QCategory category = QCategory.category;
QBook book = QBook.book;
List<Category> categories = queryFactory
.selectFrom(category)
.innerJoin(category.books, book)
// .leftJoin(category.books, book) // left outer join
.fetchJoin()
.distinct()
.fetch();
dynamic query
QCategory category = QCategory.category;
Integer categorySeq = null;
String categoryName = "category";
List<Category> categories = queryFactory
.selectFrom(category)
.where(
categorySeq == null ? null : category.categorySeq.eq(categorySeq),
category.categoryName.isNotEmpty().or(category.categoryName.like(categoryName))
)
.fetch();
group by / having
QCategory category = QCategory.category;
List<Tuple> tuples = queryFactory
.select(category.categoryName, category.categoryName.count())
.from(category)
.groupBy(category.categoryName)
.having(category.categoryName.count().gt(0))
.fetch();
order by
QCategory category = QCategory.category;
List<Category> categories = queryFactory
.selectFrom(category)
.orderBy(category.categoryName.desc(), category.categorySeq.asc())
.fetch();
paging
QCategory category = QCategory.category;
QueryResults<Category> result = queryFactory
.selectFrom(category)
.offset(0)
.limit(10)
.fetchResults();
List<Category> categories = result.getResults();
long total = result.getTotal();
long offset = result.getOffset();
long limit = result.getLimit();
subquery
QProduct product = QProduct.product;
QHistory history = QHistory.history;
List<Product> products = queryFactory
.selectFrom(product)
.where(
product.productSeq.in(
JPAExpressions
.select(history.productSeq)
.from(history)
.groupBy(history.productSeq)
.having(history.amount.sum().goe(5000))
)
)
.fetch();
다른 모델로 매핑하여 조회
@Getter
@Setter
public class OtherCategory {
private Integer categorySeq;
private String otherCategoryName;
}
QCategory category = QCategory.category;
List<OtherCategory> categories = queryFactory
.select(Projections.bean(OtherCategory.class,
category.categorySeq,
category.categoryName.as("otherCategoryName")
))
.from(category)
.fetch();
참고
반응형
'Development > Spring' 카테고리의 다른 글
[Spring] Spring Cloud Config (0) | 2020.12.27 |
---|---|
[Spring] SQL Mapper (with MyBatis) (0) | 2020.12.27 |
[Spring] WebSocket (0) | 2020.12.27 |
[Spring] Dependency Injection (0) | 2020.12.27 |
[Spring] Distributed Lock (with MySQL, Redis) (4) | 2020.12.27 |