참고
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#preface
JDBC
String url = "jdbc:postgresql://localhost:5432:testtable";
String username = "user";
String password = "pass";
try((
Connection connection = DriverManager.getConnection((url, username, password))
{
System.out.println("Connection create: " + connection);
String sql = "INSERT INTO ACCOUT VALUES(1, 'user', 'pass');";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.execute();
}
}
Domain Model
Account accoutn = new Account('user', 'pass');
accountRepository.save(account);
ORM은 애플리케이션의 클래스와 SQL 데이터베이스의 테이블 사이의 매핑 정보를 기술한 메타데이터를 사용하여, 자바 애플리케이션의 객체를 SQL 데이터베이스의 테이블에 자동으로 영속화해주는 기술
EntityManager : JPA 핵심 클래스
@Transactional : 트랜잭션
엔티티 매핑 어노테이션 (javax.persistence)
@Entity와 @Data는 같이 쓰지 말 것 : 양방향 관계설 정시 무한 루프 가능
toString에서 서로 참조해서 생기는 문제
@Entity(name = "엔티티 이름") | 테이블 매핑, 기본값은 클래스 이름과 동일 |
@Table(name = "테이블 이름") | 기본값은 @Entity이름과 동일 |
@Id | 엔티티의 프라이머리 키 |
@GeneratedValue(strategy = GenerationType.SEQUENCE) | 프라이머리 키 자동생성 방법 매핑, 기본전략은 DB에 따라 다름 TABLE, SEQUENCE, IDENTITY 중 하나 |
@Column(nullable=false, unique=true) | 컬럼 속성 unique nullable length columnDefinition ... |
@Temporal(TemporalType.TIMESTAMP) private Date created = new Date(); |
Date, Calendar ... (TemporalType) |
@Transient | 컬럼 매핑 제외 |
@Lob |
Value타입 매핑
엔티티타입 : 식별자가 있는 독립 존재
Value 타입 : 일반 데이터형, 생명주기가 다른 엔티티에 종속적인 타입
Composit Value 타입의 매핑 : Composit Value 클래스에 @Embeddable 사용
//오버라이딩 가능
@Embedded
@AttributeOverride(name="street", column=@Column(name="home_street"))
private Address address;
Collection Value 타입의 매핑 TBD
관계 매핑
관계는 두 엔티티가 필요 소유자(관계를 정의한 엔티티 즉, 상대의 레퍼런스 변수를 정의한 엔티티)-비소유자
테이블에서는 관계에 방향성이 없지만 엔티티에서 A->B로 접근할 경우, B->A로 접근할 필요도 있는 경우에 따라서, 단방향 관계만 사용할 것인지, 양방향 관계를 사용해야 하는지 결정해야 한다.
단방향 @ManyToOne: 소유자에 FK생성
단방향 @OneToMany: 소유자와 비소유자를 연결하는 조인 테이블 생성
양방향 : @ManyToOne쪽 (FK를 갖는 엔티티)이 소유자가 되고 @OneToMany쪽은 mappedBy로 소유자에서 참조하는 레퍼런스 이름을 기술한다.
Study {@ManuToOne private Account owner;} <- 소유자
Account {@OneToMany(mappedBy=owner) private Set <Study> studies = new HashSet <>();}
관계의 매핑은 반드시 소유자 엔티티에 해야 한다. 이 경우 Study.setOwner();
일반적으로 ConvinientMethod 사용하여 관계 생성
addStudy(Study study){ this.getStudies(). add(study); study.setOwner(this); }
removeStudy(Study study){ this.getStudies(). remove(study); study.setOwner(null);
엔티티 상태와 Cascade : P-C 관계에서 저장과 삭제 등 상태 전이
엔티티의 상태
Transient : JPA가 모르는 상태 (Git의 add 전?)
Persistent : save()를 통해 JPA의 관리대상이 된 상태 (Git의 stage?)
Detached : JPA가 관리하지 않는 상태 (Git의 unstage) - 트랜잭션이 종료되고 리턴돼서 다른 곳에서 사용될 때
Rmoved : JPA가 관리하지만 삭제하기로 한 상태
@oneToMany 또는 @ManyToOne의 옵션 : Parent와 Child관계에 있을 경우에 사용할 수 있음
@oneToMany(cascade = CascadeType.ALL)
Account와 Study는 P-C관계가 아님, Post와 Comment가 전형적인 P-C관계
Fetch (fetch = FetchType.)
연관 관계의 엔티티를 언제 Fetch? Eager: 지금, Lazy: 나중에 필요할때
@OneToMany의 기본값은 Lazy
@ManyToOne의 기본값은 Eager
쿼리: JPQL, Criteria, Native Query가 있지만 QueryDSL쓸거라서 생략
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#criteria
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#sql
Spring-Data-Jpa로 Entity Repository 생성 샘플
public interface PostRepository extends JpaRepository<Post, Long> { //Entity Type, PK Type
}
스프링 데이터 JPA 활용
스프링 데이터 | SQL & NoSQL 저장소 지원 프로젝트의 묶음 |
스프링 데이터 Common | 여러 저장소 지원 프로젝터의 공통 기능 제공 |
스프링 데이터 REST | 저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트 |
스프링 데이터 JPA | 스프링 데이터 Common이 제공하는 기능에 JPA관련 기능 추가 |
@DataJpaTest : 리포지토리 관련 Bean 등록
Common - 리포지토리
Repository | 실질적인 기능 없음 |
CrudRepository | 기본적인 CRUD 기능 제공 |
PagingAndSortingRepository | 소팅, 페이징 findAll(), Page<Post> page = PostRepo.findAll(PageRequest.of(page, size) |
JpaRepository (JPA) |
Common - 인터페이스 정의(쓸일 없음)
사용자 정의 Reposotory Interface 생성
@RepositoryDefinition(domainClass = Entity.class, idClass = idType.class)
public interface entityRepo(){}
@NoReposotiryBean
public interface entityRepo<T, Id extends Serializable> extends Repository<T, Id>{
<E extends T> E save (E entity);
List<T> findAll();
}
public interface comRepo extends entityRepo<Comment, Long>{}
Common - Null 처리
단일 값 리턴시 Optional<post> post = post.repo.findByName(); 등 Optional 사용
Collection은 Null 이 아닌 비어있는 Collection 리턴
스프링 5.0부터는 Null 관련 애노테이션 지원: (package)@NonAllApi, (method)@NonNull, (parameter)@Nullable
Common - 쿼리 만들기
미리 정의한 쿼리 사용 - USE_DELCARED_QUERY : 정의 방법은 저장소 마다 다름
EX) @Query(value = "SELECT c FROM Comment AS c", nativeQuery = true)
or
@NamedQuery
미리 정의한 쿼리가 없으면 만들기 - CREATE_IF_NOT_FOUND (기본값)
메소드 이름 분석을 통해 쿼리 생성 - CREATE
리턴타입 {접두어} {도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)]{정렬조건}(매개변수)
Page<Comment> findByLikeCountGreaterThanAndPost(int likeCount, Post post, Pagable pageable);
리턴타입 | List<Post>, Optional<Post>, Page<Post>, Slice<Post>, Stream<Post>, Post |
접두어 | Find, Get, Query, Count, Delete ... |
도입부 (옵션) | Distinct, First(N), Top(N) |
프로퍼티(필드) 표현식 | Person.Address.ZipCode => find(Person)ByAddress_ZipCode(...) |
조건식 | IgnoreCase, Between, LessThan, GraterThan, Like, Contains, ... |
정렬 조건 | OrderBy{프로퍼티}Asc|Desc |
매개변수 | Pageable, Sort |
// 기본예제
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// distinct
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// ignoring case
List<Person> findByLastnameIgnoreCase(String lastname);
// ignoring case
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 정렬
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
// 페이지
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
// 스트리밍
Stream<User> readAllByFirstnameNotNull(); // try-with-resource 사용할 것.(Stream을 다 쓴다음에 close() 해야 함)
// 비동기 쿼리 : 비추 (테스트코드 작성이 어려움)
@Async Future<User> findByFirstname(String firstname);
@Async CompletableFuture<User> findOneByFirstname(String firstname);
@Async ListenableFuture<User> findOneByLastname(String firstname); // 스프링 TaskExecutor에 전달
Future Non-Blocking Thread call, future.get() : Blocking call
ListenableFuture는 addCallback 으로 콜백 함수를 등록할 수 있으므로 Async는 이걸로 사용
Common - 커스텀 리포지토리
1. 커스텀 리포지토리 인터페이스 정의
2. 인터페이스 구현 클래스 만들기 (접미어 Impl 붙여야함)
3. 엔티티 리포지토리에 커스텀 리포지토리 인터페이스 추가 extends
Common - 기본 리포지토리 커스터마이징
1. JpaRepostory를 상속 받는 인터페이스 정의 @NoRepositoryBean public interface MyRepository<T, ID extends Serializable> extends Jpa...
2. 기본 구현체를 상속 받는 커스템 구현체 만들기 public class SimMyRepository<T, ID extends Serializable> extends SimpleJpaReposotory<T, ID> implements MyRepository<T, ID> {}
생성자 작성필요, EntityManager는 주입이 아니라 인자로 받아온 거를 멤버변수에 넣어서 사용
3. @EnableJpaRepositories에 설정, repositoryBaseClass=SimMyRepository.class
Common - 도메인(Entity) 이벤트
ApplicationContext extends ApplicationEventPublisher : 애플리케이션 컨텍스는 이벤트 퍼블리싱 기능을 갖고 있음
@Autowired
ApplicationContext applicationContext;
applicationContext.publishEvent(new PostPublishedEvent(post));
Event 생성: PostPublichedEvent extends ApplicationEvent
Event 리스너 생성: PostListener implements ApplicationListener<PostPublisshedEvent> : 리스너는 Bean등록 해줘야함
또는 그냥 @EventListner사용 가능(method)
또는 Bean등록시에 public ApplicationListener<PostPublishedEvent> postListner(){ return event->{ }; } 를 등록 가능
Event 를 applicationContext로 이벤트를 publishing 하지 않고 EntityRepository에서 extends AbstractAggregationRoot<E> 를 상속받아서 메서드 하나 만들고 this.registerEvent로 이벤트를 등록해두면
Repository.save()시점에 등록된 모든 이벤트가 퍼블리싱되고 리스너가 call됨
Common - QueryDSL 연동
http://www.querydsl.com/static/querydsl/4.1.3/reference/html_single/#jpa_integration
제공 인터페이스
Optional<T> findOne(Predicate) : Predicate = 조건
List<T>.. findAll(Predicate)
연동 방법
https://hiddentrap.tistory.com/127?category=833353
public interface AccountRepository extends JpaRepository<Account, Long>, QuerydslPredicateExecutor<Account> {
}
@ExtendWith(SpringExtension.class)
@DataJpaTest
class AccountTest {
@Autowired
AccountRepository accountRepository;
@Test
void crud(){
Predicate predicate = QAccount.account
.firstName.containsIgnoreCase("keesun")
.and(QAccount.account.lastName.startsWith("baik"));
Optional<Account> one = accountRepository.findOne(predicate);
assertThat(one).isEmpty();
}
}
Common - 웹 지원 기능
도메인 클래스 컨버터
@GetMapping("/posts/{id}")
public String getAPost(@PathVariable Long id){
Optional<Post> byId = postRepository.findById(id);
Post post = byId.get();
return post.getTitle();
}
@GetMapping("/posts/{id}")
public String getAPost(@PathVariable("id") Post post) {
return post.getTitle();
}
Pageable, Sort 매개변수
파라메터
page : 0부터 시작
size: 기본값 20
sort: 필드,asc|desc
ex: sort=created, desc&sort=title
@GetMapping("/posts")
public PageM<Post> getPosts(Pageable){
return postRepository.findAll(pageable);
}
HATEOAS
HATEOAS 의존성 추가 필요 (starter-hateoas) : 링크 정보 등 부가정보 생성
핸들러 매개변수로 PagedResourcesAssembler 사용
@GetMapping("/posts")
public PagedResources<Resource<Post>> getPosts(Pageable pageable, PagedResourcesAssembler assembler){
return assembler.toResource(posts.findall(pageable));
}
JPA - JpaRepository
JpaReposory 상속할때 @Repository 안붙여도 됨 SimpleJpaRepository 구현체에 이미 붙어있어서 중복임
JPA - Entity 저장
savedEntity=repository.save(entity); 이후 entitiy 사용금지, savedEntity 사용할것
JPA - 쿼리 메소드
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
@Entitiy @NamedQuery(name = "Post.findByTitle", query="SELECT p FROM post AS p WHERE p.title = ?1")
repository 인터페이스에서 List<post> findByTitle(String title);
또는 repsitory 메서드 위에 @Query("SELECT p FROM post AS p WHERE p.title = ?1")
nativeQuery도 사용 가능