중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
✍️
이런 걸
배워요!
ORM에 대한 이해
JPA 프로그래밍
Bean 생성 방법
스프링 JPA가 어렵게 느껴졌다면?
개념과 원리, 실제까지 확실하게 학습해 보세요.
제대로 배우는
백기선의 스프링 데이터 JPA
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
강의 :
https://www.inflearn.com/course/스프링-데이터-jpa#reviews
강의자료 :
https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit
소스 코드
https://github.com/braverokmc79/springdatajpa
강좌 소개
Application -> 스프링 데이터 JPA (-> JPA -> JDBC) -> Database
강사 소개
백기선
마이크로소프트(2+) <- 아마존(1) <- 네이버(4.5) <- SLT(2.5) ...
강좌
스프링 프레임워크 입문 (Udemy)
백기선의 스프링 부트 (인프런)
특징
스프링 프레임워크 중독자
JPA 하이버네이트 애호가
유튜브 / 백기선
[1부: 핵심 개념 이해]
3.관계형 데이터베이스와 자바
강의 :
본격적인 스프링 데이터 JPA 활용법을 학습하기에 앞서, ORM과 JPA에 대한 이론적인 배경을 학습합니다.
관계형 데이터베이스와 자바
JDBC
(관계형) 데이터베이스와 자바의 연결 고리
JDBC
DataSource / DriverManager
Connection
PreparedStatement
SQL
DDL
DML
무엇이 문제인가?
SQL을 실행하는 비용이 비싸다.
SQL이 데이터베이스 마다 다르다.
스키마를 바꿨더니 코드가 너무 많이 바뀌네...
반복적인 코드가 너무 많아.
당장은 필요가 없는데 언제 쓸 줄 모르니까 미리 다 읽어와야 하나...
의존성 추가
org.postgresql postgresql 42.6.0
PostgreSQL 설치 및 서버 실행 (docker)
docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=keesun -e POSTGRES_DB=springdata --name postgres_boot -d postgres docker exec -i -t postgres_boot bash su - postgres psql springdata 데이터베이스 조회 \list 테이블 조회 \dt 쿼리 SELECT * FROM account;
★ 윈도우에서 도터 컨테이너 접속후 psql 접속시 다음과 같이 유저명과 DB 명을 입력해야한다.
psql --username keesun --dbname springdata
package me.whiteship; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Application { public static void main(String[] args) throws Exception { String url ="jdbc:postgresql://localhost:5432/springdata"; String username="keesun"; String password="pass"; try(Connection connection= DriverManager.getConnection(url, username, password)){ System.out.println("connection = " + connection); // String sql ="CREATE TABLE ACCOUNT(id int , username varchar(255), password varchar(255)); "; String sql ="INSERT INTO ACCOUNT VALUES(1 , 'test', 'pass')"; try(PreparedStatement statement = connection.prepareStatement(sql)){ statement.execute(); } } } }
4.ORM: Object-Relation Mapping
강의 :
JDBC 사용
try(Connection connection = DriverManager.getConnection(url, username, password)) { System.out.println("Connection created: " + connection); String sql = "INSERT INTO ACCOUNT VALUES(1, 'keesun', 'pass');"; try(PreparedStatement statement = connection.prepareStatement(sql)) { statement.execute(); } }
도메인 모델 사용
Account account = new Account(“keesun”, “pass”); accountRepository.save(account);
JDBC 대신 도메인 모델 사용하려는 이유?
객체 지향 프로그래밍의 장점을 활용하기 좋으니까.
각종 디자인 패턴
코드 재사용
비즈니스 로직 구현 및 테스트 편함.
ORM은 애플리케이션의 클래스와 SQL 데이터베이스의 테이블 사이의 맵핑 정보를 기술한 메타데이터를 사용하여,
자바 애플리케이션의 객체를 SQL 데이터베이스의 테이블에 자동으로 (또 깨끗하게) 영속화 해주는 기술입니다.
장점 : 생산성 , 유지보수성 ,성능 ,밴더 독립성
단점 : 학습비용
5.ORM 패러다임 불일치
강의 :
6.ORM 패러다임 불일치
강의 :
참조 :
https://velog.io/@jwpark06/SpringBoot에-JDBC로-Postgresql-연동하기
데이터베이스 실행
PostgreSQL 도커 컨테이너 재사용
docker start postgres_boot
스프링 부트
스프링 부트 v2.*
스프링 프레임워크 v5.*
스프링 부트 스타터 JPA
JPA 프로그래밍에 필요한 의존성 추가
JPA v2.*
Hibernate v5.*
자동 설정: HibernateJpaAutoConfiguration
컨테이너가 관리하는 EntityManager (프록시) 빈 설정
PlatformTransactionManager 빈 설정
JDBC 설정
jdbc:postgresql://localhost:5432/springdata
keesun
pass
application.properties
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.hibernate.ddl-auto=create
================================================= ================================================= =================================================
================================================= ================================================= =================================================
================================================= ================================================= =================================================
스프링부트 3.0.5 설정 파일 : jPA + dev 툴 + postsql + lombok
설정 파일 다운로드
=> 롬복 적용
콘솔 색깔 설정
application.properties
spring.output.ansi.enabled=always
Account
package com.jpa.spring; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.Data; import org.springframework.boot.autoconfigure.domain.EntityScan; @Entity @Data public class Account { @Id @GeneratedValue private Long id; private String username; private String password; }
JpaRunner
package com.jpa.spring; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.hibernate.Session; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component @Transactional public class JpaRunner implements ApplicationRunner { @PersistenceContext EntityManager entityManager; @Override public void run(ApplicationArguments args) throws Exception { Account account=new Account(); account.setUsername("HongGilDong"); account.setPassword("jpa"); //entityManager.persist(account); Session session =entityManager.unwrap(Session.class); session.save(account); } }
spring.datasource.platform=postgres 를 추가하면 spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 제거해도 된다.
쿼리 파라미터 로그 남기기 설정하기
참조 : https://macaronics.net/index.php/m01/spring/view/2055
로그에 다음을 추가하기: SQL 실행 파라미터를 로그로 남긴다.
주의! 스프링 부트 3.x를 사용한다면 영상 내용과 다르기 때문에 다음 내용을 참고하자.
1)스프링 부트 2.x, hibernate5
org.hibernate.type: trace
외부 라이브러리 사용
https://github.com/gavlyukovskiy/spring-boot-data-source-decorator
스프링 부트를 사용하면 이 라이브러리만 추가하면 된다.
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
> 참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는
편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.
2) 쿼리 파라미터 로그 남기기 - 스프링 부트 3.0
스프링 부트 3.x, hibernate6
org.hibernate.orm.jdbc.bind: trace
p6spy-spring-boot-starter 라이브러리는 현재 스프링 부트 3.0을 정상 지원하지 않는다.
스프링 부트 3.0에서 사용하려면 다음과 같은 추가 설정이 필요하다
1. org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 추가
src/resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration
폴더명: src/resources/META-INF/spring
파일명: org.springframework.boot.autoconfigure.AutoConfiguration.imports
2. spy.properties 파일 추가
src/resources/spy.properties
appender=com.p6spy.engine.spy.appender.Slf4JLogger
spy.properties 에 입력하지 말것
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
이렇게 2개의 파일을 추가하면 정상 동작한다
application.properties 추가 설정
spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
메인븐 라이브러리
<dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.9.0</version> </dependency>
7.JPA 프로그래밍: 엔티티 맵핑
강의 :
@Entity
“엔티티”는 객체 세상에서 부르는 이름.
보통 클래스와 같은 이름을 사용하기 때문에 값을 변경하지 않음.
엔티티의 이름은 JQL에서 쓰임.
@Table
“릴레이션" 세상에서 부르는 이름.
@Entity의 이름이 기본값.
테이블의 이름은 SQL에서 쓰임.
@Id
엔티티의 주키를 맵핑할 때 사용.
자바의 모든 primitive 타입과 그 랩퍼 타입을 사용할 수 있음
Date랑 BigDecimal, BigInteger도 사용 가능.
복합키를 만드는 맵핑하는 방법도 있지만 그건 논외로..
@GeneratedValue
주키의 생성 방법을 맵핑하는 애노테이션
생성 전략과 생성기를 설정할 수 있다.
기본 전략은 AUTO: 사용하는 DB에 따라 적절한 전략 선택
TABLE, SEQUENCE, IDENTITY 중 하나.
@Column
unique
nullable
length
columnDefinition
...
@Temporal
현재 JPA 2.1까지는 Date와 Calendar만 지원.
@Transient
컬럼으로 맵핑하고 싶지 않은 멤버 변수에 사용.
application.properties spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
Account
package com.jpa.spring; import jakarta.persistence.*; import jdk.jfr.Timestamp; import lombok.Data; import java.time.LocalDateTime; import java.util.Date; @Entity @Data public class Account { @Id @GeneratedValue private Long id; @Column(nullable = false, unique = true) private String username; private String password; @Temporal(TemporalType.TIMESTAMP) private Date created=new Date(); @Transient private String yes; private String no; }
8.JPA 프로그래밍: Value 타입 맵핑 (값 타입 매핑)
강의 :
엔티티 타입과 Value 타입 구분 식별자가 있어야 하는가. 독립적으로 존재해야 하는가.
Value 타입 종류 기본 타입 (String, Date, Boolean, ...)
Composite Value 타입
Collection Value 타입
기본 타입의 콜렉션
컴포짓 타입의 콜렉션
Composite Value 타입 맵핑
@Embeddable
@Embedded
@AttributeOverrides
@AttributeOverride
Address
package com.jpa.spring; import jakarta.persistence.Embeddable; @Embeddable public class Address { private String street; private String city; private String state; private String zipCode; }
Account
@Embedded @AttributeOverrides({ @AttributeOverride(name="street", column = @Column(name="home_street") ) }) private Address address;
9.JPA 프로그래밍 4. 관계 맵핑
강의 :
관계에는 항상 두 엔티티가 존재 합니다.
둘 중 하나는 그 관계의 주인(owning)이고 다른 쪽은 종속된(non-owning) 쪽입니다.
해당 관계의 반대쪽 레퍼런스를 가지고 있는 쪽이 주인.
단방향에서의 관계의 주인은 명확합니다. 관계를 정의한 쪽이 그 관계의 주인입니다.
단방향 @ManyToOne
기본값은 FK 생성
단방향 @OneToMany
기본값은 조인 테이블 생성
양방향
FK 가지고 있는 쪽이 오너 따라서 기본값은 @ManyToOne 가지고 있는 쪽이 주인.
주인이 아닌쪽(@OneToMany쪽)에서 mappedBy 사용해서 관계를 맺고 있는 필드를 설정해야 합니다.
양방향
@ManyToOne (이쪽이 주인)
@OneToMany(mappedBy)
주인한테 관계를 설정해야 DB에 반영이 됩니다.
Account
package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import java.util.Date; import java.util.HashSet; import java.util.Set; @Entity @Data public class Account { @Id @GeneratedValue private Long id; @Column(nullable = false, unique = true) private String username; private String password; @Temporal(TemporalType.TIMESTAMP) private Date created=new Date(); @Transient private String yes; private String no; @Embedded @AttributeOverrides({ @AttributeOverride(name="street", column = @Column(name="home_street") ) }) private Address address; //CascadeType.ALL 는 order // persist(ownerA); // persist(ownerB); // persist(ownerB); // persist(ownerB); // persist(study); // =>persist를 각각 해줘야 하는데 CascadeType.ALL 적용하면 persist(study); 한번에 적용된다. @OneToMany(mappedBy ="owner", cascade = CascadeType.ALL) private Set<Study> studies=new HashSet<>(); public void addStudy(Study study){ this.getStudies().add(study); study.setOwner(this); } public void removeStudy(Study study){ this.getStudies().remove(study); study.setOwner(null); } }
Study
package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; @Entity @Data public class Study { @Id @GeneratedValue private Long id; private String name; @ManyToOne(fetch = FetchType.LAZY) private Account owner; }
10. JPA 프로그래밍: Cascade
강의 :
영속성전이(CASCADE)란 ?
부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미합니다.
각각의 operations에 대한 설명은 다음과 같습니다.
- CascadeType.ALL: 모든 Cascade를 적용
- CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
- CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
- CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
- CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X
- CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침
엔티티의 상태 변화를 전파 시키는 옵션.
잠깐? 엔티티의 상태가 뭐지?
Transient: JPA가 모르는 상태
Persistent: JPA가 관리중인 상태 (1차 캐시, Dirty Checking, Write Behind, ...)
Detached: JPA가 더이상 관리하지 않는 상태.
Removed: JPA가 관리하긴 하지만 삭제하기로 한 상태.
Post
package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import java.util.ArrayList; import java.util.List; @Entity @Data public class Post { @Id @GeneratedValue @Column(name="post_id") private Long id; private String title; //CascadeType.ALL 는 comment // persist(commentA); // persist(commentB); // persist(commentB); // persist(commentB); // persist(post); // =>persist를 각각 해줘야 하는데 CascadeType.ALL 적용하면 persist(study); 한번에 적용된다. @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) private List<Comment> comments =new ArrayList<>(); /** Set 으로 할경우 hashCode 에 대한 StackOverflowError 나와서 id 에 대한 hashCode 를 설정해 줘야 한다. // StackOverflowError hashCode **/ public void addComment(Comment comment){ this.getComments().add(comment); comment.setPost(this); } }
Comment
package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; @Entity @Data public class Comment { @Id @GeneratedValue @Column(name="comment_id") private Long id; private String comment; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="post_id") private Post post; }
PostDTO
package com.jpa.spring; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.ToString; @Data @AllArgsConstructor @RequiredArgsConstructor @ToString public class PostDTO { private Long id; private String title; private Long commentId; private String comment; }
CascadeType.ALL: 모든 Cascade를 설정으로 연속해서 데이터 등록및 삭제 처리가 가능하다.
만약에 설정을 하지 않는다면, 다음과 같은 코드를 추가해 줘야 한다.
entityManager.persist(comment1);
entityManager.persist(comment2);
entityManager.persist(comment3)
JpaRunner
package com.jpa.spring; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Component @Transactional public class JpaRunner implements ApplicationRunner { @PersistenceContext EntityManager entityManager; // public void run2(ApplicationArguments args) throws Exception { // Account account=new Account(); // account.setUsername("HongGilDong"); // account.setPassword("jpa"); // // //entityManager.persist(account); // Study study =new Study(); // study.setName("Spring Data JPA"); // // //보조적인 설정 //// account.getStudies().add(study); //// //항상 관계의 주인에 넣어줘야 한다. //// study.setOwner(account); // // //====> // account.addStudy(study); // // Session session =entityManager.unwrap(Session.class); // session.save(account); // session.save(study); // // Account kessun = session.load(Account.class, account.getId()); // System.out.println(" ====================================" ); // System.out.println("1.kessun = " + kessun.getUsername()); // // //더티 체킹 // kessun.setUsername("whiteship"); // kessun.setUsername("whiteship"); // System.out.println(" ====================================" ); // System.out.println("2.kessun = " + kessun.getUsername()); // } @Override public void run(ApplicationArguments args) throws Exception{ Post post=new Post(); post.setTitle("Spring Data JPA 언제 보나..."); Comment comment =new Comment(); comment.setComment("빨리 보고 싶어요."); post.addComment(comment); Comment comment1 =new Comment(); comment1.setComment("곧 보여드릴께요"); post.addComment(comment1); // Session session =entityManager.unwrap(Session.class); // session.save(post); entityManager.persist(post); /** cascade = CascadeType.ALL 설정 되어 있기 때문에 다음 코드를 넣지 않아도 * comment data 가 들어간다. * * **/ // entityManager.persist(comment); // entityManager.persist(comment1); Post entityManager.flush(); entityManager.close(); List<PostDTO> resultList = entityManager.createQuery("select new com.jpa.spring.PostDTO( p.id, p.title, c.id, c.comment ) from Post p join p.comments c on p.id=1", PostDTO.class).getResultList(); for(PostDTO p :resultList){ System.out.println("resultList = " + p); } /** 출력 p.getComments() = PostDTO(id=1, title=Spring Data JPA 언제 보나..., commentId=1, comment=빨리 보고 싶어요.) p.getComments() = PostDTO(id=1, title=Spring Data JPA 언제 보나..., commentId=2, comment=곧 보여드릴께요) * */ //삭제 Post singleResult = entityManager.createQuery("select p from Post p where p.id = 1l ", Post.class).getSingleResult(); System.out.println("singleResult = " + singleResult.getTitle()); entityManager.remove(singleResult); entityManager.flush(); entityManager.clear(); List<Comment> commentList = entityManager.createQuery("select c from Comment c where c.post.id = 1l ", Comment.class).getResultList(); System.out.println("CommentList = " + commentList.size()); } }
11. Fetch
강의 :
연관 관계의 엔티티를 어떻게 가져올 것이냐... 지금 (Eager)? 나중에(Lazy)?
@OneToMany의 기본값은 Lazy
@ManyToOne의 기본값은 Eager
참조 : =>
자바 ORM 표준 JPA 프로그래밍 - 10. 객체지향 쿼리 언어2 - 중급 문법 ★ 페치 조인(fetch join)
12. JPA 프로그래밍: Query
강의 :
@Entity @Data @ToString(of = {"id", "title"}) public class Post { ~
JpaRunner
@Override public void run(ApplicationArguments args) throws Exception{ Post post = new Post(); post.setTitle("Spring Data JPA 언제 보나..."); Comment comment = new Comment(); comment.setComment("빨리 보고 싶어요."); post.addComment(comment); Comment comment1 = new Comment(); comment1.setComment("곧 보여드릴께요"); post.addComment(comment1); entityManager.persist(post); List<Post> resultList = entityManager.createQuery("select p from Post p join p.comments ", Post.class).getResultList(); System.out.println("========================================================="); resultList.forEach(System.out::println); System.out.println("========================================================="); }
JPQL (HQL)
Java Persistence Query Language / Hibernate Query Language
데이터베이스 테이블이 아닌, 엔티티 객체 모델 기반으로 쿼리 작성.
JPA 또는 하이버네이트가 해당 쿼리를 SQL로 변환해서 실행함.
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql
Criteria
타입 세이프 쿼리
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#criteria
CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<Post> criteria = builder.createQuery(Post.class); Root<Post> root = criteria.from(Post.class); criteria.select(root); List<Post> posts = entityManager.createQuery(criteria).getResultList();
Native Query
SQL 쿼리 실행하기
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#sql
List<Post> posts = entityManager .createNativeQuery("SELECT * FROM Post", Post.class) .getResultList();
13. 스프링 데이터 JPA 소개 및 원리
강의 :
JpaRepository<Entity, Id> 인터페이스
매직 인터페이스
@Repository가 없어도 빈으로 등록해 줌.
@EnableJpaRepositories
매직의 시작은 여기서 부터
매직은 어떻게 이뤄지나?
시작은 @Import(JpaRepositoriesRegistrar.class)
핵심은 ImportBeanDefinitionRegistrar 인터페이스
package com.jpa.spring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component @Transactional public class 스프링데이터JPA소개및원리13 implements ApplicationRunner { @Autowired PostRepository postRepository; @Override public void run(ApplicationArguments args) throws Exception { System.out.println("================================================="); postRepository.findAll().forEach(System.out::println); System.out.println("================================================="); } }
14. 핵심 개념 마무리
강의 :
@Autowired PostRepository postRepository; @Override public void run(ApplicationArguments args) throws Exception { Post post =new Post(); post.setTitle("spring"); Comment comment =new Comment(); comment.setComment("hello"); postRepository.save(post); }
데이터베이스와 자바
패러다임 불일치
ORM이란?
JPA 사용법 (엔티티, 벨류 타입, 관계 맵핑)
JPA 특징 (엔티티 상태 변화, Cascade, Fetch, 1차 캐시, ...)
주의할 점
반드시 발생하는 SQL을 확인할 것.
팁: “?”에 들어있는 값 출력하기
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
로그 설정
=>
https://macaronics.net/index.php/m01/spring/view/2088
댓글 ( 4)
댓글 남기기