JPA를 사용하여 데이터를 처리하는 시간이다. 2-4에서는 데이터베이스를 활용하기 위해 클래스를 만드는 과정을 거쳤는데 그것도 시간이 꽤 걸렸던 것 같다. 이번 시간은 지난시간에 준비한 것들을 활용하는 시간이라서 길이가 굉장할 것으로 예상한다.
1. 리포지터리
2. 데이터 저장
3. 데이터 조회
4. 데이터 수정
5. 데이터 삭제
6. 답변 데이터 생성 후 저장
7. 답변 조회
8. 답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기
1. 리포지터리
엔티티(Entity)만으로는 데이터베이스에 데이터를 저장하거나 조회를 할 수 없다고 한다.(잉?? 아 그래서 H2~!?)
JPA 리포지터리가 필요한 상황인데 우선 리포지터리는 에티티에 의해 생성된 테이블에 접근하는 메서드들을 사용하기 위한 인터페이스라고 한다. 데이터 테이블은 만들었으나 데이터가 없기 때문에 CRUD(Create, Read, Update, Delete)을 사용하는 것을 리포지터리라고 한다.
QuestionRepository 인터페이스를 생성한다. (Class와 다른 Interface니 주의하자)
설치 위치는 /sbb/src/main/java/com/mysite/sbb/QuestionRepository.java로 하면 된다.
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer>{
}
QuestionRepository 리포지터리로 만들기 위해서 JpaRepository 인터페이스를 상속(extends)했다. JpaRepository를 상속할 때는 제네릭스 타입으로 <Question, Integer> 처럼 리포지터리의 대상이 되는 엔티티의 타입(Question)과 해당 엔티티의 PK의 속성 타입(Integer)을 지정해야 한다. JpaRepository를 생성하기 위한 규칙이라고 한다.
Question 엔티티의 PK(Primary Key = 데이터베이스에서 id와 같은 특징을 가진 속성 기본 키) 속성인 id 타입은 Integer.
Question 엔티티를 Integer로 지정 했으니 Answer 엔티티도 상속하기 위해서 Integer지정해야 하기 때문에 생성 해야 한다.
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer, Integer>{
}
이제부터는 QuestionRepository, AnswerRepository를 활용하여 question, answer 테이블에 데이터를 저장하거나 조회가 가능하게 된다.
2. 데이터 저장
테스트를 하기 위해서 JUnit 기반의 스프링부트의 테스트 프레임워크를 사용하자.
이전에 test환경은 잘 안 쓸것 같다고 했지만 아직도 왜 이곳에서 하는지 의문이긴 하다.
/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java
해당 test 환경에서 새로운 코드를 입력한다.
package com.mysite.sbb;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void contextLoads() {
Question q1 = new Question();
q1.setSubject("sbb가 무엇인가요?");
q1.setContent("sbb에 대해서 알고 싶어요.");
q1.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q1); // 첫번째 질문 저장
Question q2 = new Question();
q2.setSubject("스프링모델 질문입니다.");
q2.setContent("id는 자동 생성되나요?");
q2.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q2); // 두번째 질문 저장
}
}
@SpringBootTest 애너테이션 (기본으로 작성되어 있었음.)은 SbbApplicationTests 클래스가 스프링부트 테스트 클래스임을 알려준다.
@Autowired 애너테이션은 스프링의 DI 기능으로 questionRepository 객체를 스프링이 자동으로 생성해준다.
DI : Dependency Injection은 스프링이 객체를 대신 생성하여 주입하는 역할
@Autowired 객체를 주입하기 위한 스프링의 애너테이션. Setter 또는 생성자를 사용하는 방식도 존재한다. 순환참조(?) 문제와 같은 이유로 해당 애너테이션을 권장하고 있다. 그러나 테스트 코드에서는 객체 주입이 불가능하기 때문에 생성자를 통한 객체 주입방식을 택하게 되었다. |
testjpa 메서드 위의 @Test 애너테이션은 testJpa 메서드가 테스트 메서드임을 타나낸다. 위 클래스를 JUnit으로 실행한다면, @Test 애너테이션이 붙은 메서드가 실행된다.
JUnit : 테스트코드를 작성하고 작성한 테스트코드를 실행하기 위해 사용하는 자바의 테스트 프레임워크
작성이 완료 되었다면 SbbApplicationTests클래스를 실행해보자.
하지만 에러가 발생되었다. 이유는 이미 로컬서버가 구동중이기 때문에 이때는 실행을 하기 위해서는 우선 로컬 서버를 종료 후 진행을 해야한다.
실행 확인을 하였다면 다시 로컬서버를 실행 후 H2콘솔에 접속하도록 한다.
접속 후 Oracle(mysql) 에서 배웠던 가장 기본적인 코드를 사용해 Question 클래스를 불러오도록 한다.
SELECT * FROM QUESTION
id는 Question 엔티티의 기본 키 PK(Primary Key). id는 앞에서 엔티티를 생성할 때 설정했던대로의 데이터를 생성할 때 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있는데, Question 엔티티에서 @GeneratedValue을 설정했기 때문이다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
3. 데이터 조회
findAll : 데이터를 조회할때 사용하는 메서드다.
SbbApplicationTests에서 코드를 수정한다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
총2건의 데이터를 저장(이전의 q1과 q2를 말하는 것 같다.)했기 때문에 데이터의 사이즈는 2가 되어야 한다. 2라는 데이터가 맞는지 확인하기 위해서 JUnit의 asserEquals 메서드를 사용했다.
asserEquals는 asserEquals(기대값, 실제값)과 같이 사용하고 기대값과 실제값이 동일한지를 조사한다. 만약 동일하지 않는다면, 테스트는 실패로 처리된다. 처음 저장한 데이터의 제목은 "sbb가 무엇인가요?"인데 일치하는지 테스트를 해보자.
테스트를 위해서 로컬서버 종료 후 SbbApplicationTests를 다시 실행(Run)한 후 서버를 다시 실행한다.
문제 없이 제대로 떠있음을 확인할 수 있었다.
findByld
Qustion 엔티티의 ID값으로 데이터를 조회하도록 하자.
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
if(oq.isPresent()) {
Question q = oq.get();
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
}
id 값으로 데이터 조회를 위해 findByld 메서드를 사용해야한다. findByld의 리턴 타입은 Question이 아닌 Optional임에 주의를 해야한다. Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로 isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.
(사실 이때까지만 하더라도 왜 저렇게 하나 모르고 있는 상황이다. 영상을 봐도 결과를 도출하는 영상은 찍지 않았기 때문이다.)
findBySubject
Question 엔티티의 subject 값으로 데이터 조회하기.
아쉽게도 findBySubject와 같은 메서드를 기본적으로 제공하지 않으므로 해당 메서드를 사용하려면 QuestionRepository 인터페이스를 변경해야한다.
[위치 : /sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer>{
Question findBySubject(String subject);
}
코드를 입력 했다면 제목으로 테이블 데이터를 조회할 수 있게 된다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
}
테스트 를 실행해보면 문제 없이 진행이 되었다는 것을 알 수 있는데 인터페이스에 findBySubject라는 메서드를 선ㅇㄴ만 하고 구현되지 않았음에도 실행되었다는 것을 알 수 있다.
이러한 이유는 JpaRepository를 상속한 QuestionRepository 객체가 생성될 때 벌어진다.
(솔직히 이해가 가지 않는다.)
findBy + 엔티티의 속성명(ex:findBySubject)과 리포지터리 메서드를 작성하면 해당 속성의 값으로 데이터를 조회할 수 있다.
findBySubject 메서드를 호출할때 어떠한 쿼리가 실행되는지 살펴보자. 실행되는 쿼리를 로그에서 보려면 application.properties 파일을 수정해야 한다. 마지막 두줄을 추가해 주었다.
[위치 : /sbb/src/main/resources/application.properties]
# DATABASE
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:~/local
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
테스트코드를 다시 실행하면 콘솔로그에서 실행된 쿼리를 확인할 수 있다.
실행된 쿼리의 where 조건에 subject가 포함된 것을 확인할 수 있다.
findBySubjectAndContent
제목과 내용을 함께 조회하기. 두 개의 속성을 And 조건으로 조회할때는 리포지터리에 다음과 같은 메서드를 추가해야 한다. 마지막 한 줄 추가.
[위치 : /sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer>{
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
테스트 코드는 다음과 같이 작성한다.
기존의 코드에서 findBySubject를 findBySubjectAndContent로 변경 후 content가 추가 되었으니 함수도 포함한다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶어요.");
assertEquals(1, q.getId());
}
}
Run을 통해 테스트는 통과 되었음을 확인할 수 있다.
콘솔에서도 변경된 where을 확인할 수 있다.
findBySubject와 findBySubjectAndContent 외에도 많은 조합법이 있다.
항목 | 예제 | 설명 |
And | findBySubjectAndContent(String subject, String content) | 여러 컬럼을 and로 검색 |
Or | findBySubjectOrContent(String subject, String content) | 여러 컬럼을 or로 검색 |
Between | findByCreateDateBetween(LocalDateTime fromDate, LocalDateTime toDate) | 컬럼을 between으로 검색 |
LessThan | findByIdLessThan(Integer id) | 작은 항목 검색 |
GreaterThanEqual | findByIdGraterThanEqual(Integer id) | 크거나 같은 항목 검색 |
Like | findBySubjectLike(String subject) | like 검색 |
In | findBySubjectIn(String[] subjects) | 여러 값중에 하나인 항목 검색 |
OrderBy | findBySubjectOrderByCreateDateAsc(String subject) | 검색 결과를 정렬하여 전달 |
응답 결과가 여러건인 경우에는 리포지터리 메서드의 리턴 타입을 Question이 아닌 List<Question>으로 해야한다.(?)
- 공식문서 : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
Spring Data JPA - Reference Documentation
Example 119. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del
docs.spring.io
findBySubjectLike
특정 문자열이 포함되어 있는 데이터를 조회한다.
마지막 줄 추가 후 import 해준다.
[위치 : /sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer>{
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
테스트 코드도 수정을 해줘야 한다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
테스트는 당연히 통과 된 것을 확인 할 수 있는데 "sbb%"에서 % 를 알아보자.
- sbb% : "sbb"로 시작하는 문자열
- %sbb : "sbb"로 끝나는 문자열
- %sbb% : "sbb"를 포함하는 문자열
4. 데이터 수정
질문 데이터를 수정할 것이다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목");
this.questionRepository.save(q);
}
}
asserTrue(값)이 true인지 테스트를 통해 확인할 수 있다.
저장하기 위해서는 this.questionRepository.save(q) 처럼 리포지터리의 save 메서드를 사용한다.
콘솔 로고에서 Question 데이터를 확인할 수 있는데 update 문이 실행됨을 확인할 수 있다.
5. 데이터 삭제
첫 번째 질문을 삭제하기.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
assertEquals(2, this.questionRepository.count());
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
this.questionRepository.delete(q);
assertEquals(1, this.questionRepository.count());
}
}
count() 메서드는 해당 리포지터리의 총 데이터건수를 리턴한다.
delete 메서드를 사용해 데이터를 삭제. 삭제하기 전에는 데이터 건수가 2, 삭제한 후에는 데이터 건수가 1인지를 테스트한다.
6. 답변 데이터 생성 후 저장
답변(Answer) 데이터를 생성하고 저장하기.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
a.setQuestion(q); // 어떤 질문의 답변인지 알기 위해서 Question 객체가 필요하다.
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
AnswerRepository 객체를 @Autowired로 주입.
id가 2인 질문 데이터를 가져온 다음 Answer 엔티티의 question 속성에 방금 가져온 질문 데이터를 대입해(a.setQuestion(q)) 답변 데이터를 생성했다. Answer 엔티티에는 어떤 질문에 해당되는 답변인지 연결할 목적으로 question 속성이 필요하다.
7. 답변 조회
Answer도 Question 엔티티와 마찬가지로 id 속성이 PK(Primary Key=기본 키)이므로 자동으로 생성된다. id값을 이용해 데이터를 조회해보자.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Answer> oa = this.answerRepository.findById(1);
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2, a.getQuestion().getId());
}
}
id 값이 1인 답변을 조회했다. 그 답변의 질문 id가 2인지도 테스트 해보았다.
8. 답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기
Answer 엔티티의 question 속성을 이용하면 "답변에 연결된 질문"을 조회할 수 있다.
a.getQuestion()
답변에 연결되어 있는 질문을 찾는 것은 Answer 엔티티에 question 속성이 정의 되어 있어서 찾기가 쉽다.
반대로는 질문에서 답변을 찾을 수 있을까?
질문 엔티티에 정의한 answerList를 사용하면 쉽게 구할 수 있다.
[위치 : /sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer> answerList = q.getAnswerList();
assertEquals(1, answerList.size());
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
하지만 위의 코드대로 입력을 하게 된다면 오류가 발생된다.
이유는 Question 리포지터리가 findById를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지기 때문이다. 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생되는 것이다. 답변 데이터 리스트는 q 객체를 조회할 때 가져오지 않고 q.getAnswer() 메서드를 호출하는 시점에 가져오기 때문이다.
문제는 테스트 코드에서만 발생된다고 한다. 실제로는 서버에서 JPA 프로그램들을 실행할 때는 DB세션이 종료되지 않기 때문에 위와 같은 오류가 발생되지 않는다고 한다.(!?) 만약, 테스트 코드를 수행할 때 이와 같은 오류가 발생되지 않기 위해서는 @Transactional 애너테이션을 사용하는 것이다. 메서드가 종료될 때까지 DB 세션이 유지된다고 한다.(놀랬었다.)
package com.mysite.sbb;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Transactional
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer> answerList = q.getAnswerList();
assertEquals(1, answerList.size());
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
내가 다시 한번 찾아야할 부분은 findById 에 들어간 함수에 대한 이해가 필요한 것 같다.
'스프링부트' 카테고리의 다른 글
2-4 엔티티(Entity) (0) | 2022.12.16 |
---|---|
2-3 JPA (0) | 2022.12.15 |
2-2 컨트롤러 (0) | 2022.12.15 |
2-1 스프링부트 구조 알아보기 (0) | 2022.12.15 |
1-4 스프링부트도구2 (0) | 2022.12.14 |