본문 바로가기
실습/리눅스 서버 + 스프링 부트

JPA

by 이민우 2021. 11. 27.
728x90
반응형

Java Persistence API (JPA)

 

DB의 테이블을 프로그램에서 사용하기 위해 자바에서는 DTO라는 클래스를 DB와 같이 만들어 사용한다.

그렇다면 이 클래스는 DB 테이블과 완전히 같을까? 당연히 그럴 리가 없다.

 

예를 들어보자. 클래스는 부모 클래스를 상속받아 그 변수와 함수를 물려받을 수 있다. 하지만 테이블은? 당연히 상속이라는 개념이 없다.

또한 테이블에는 다른 테이블을 참조하는 외래키가 존재한다. 하지만 객체는? 당연히 외래키를 직접적으로 사용할 수 없기에 해당 객체를 직접 멤버변수르 가져야 하는 불편함이 생긴다.

이는 객체와 DB가 지향하는 목적이 다르기에 어쩔 수 없이 발생하는 표현 방법의 차이이다.

 

이러한 불일치를 패러다임 불일치라 부른다. 그리고 이 패러다임 불일치를 해결해주는 인터페이스가 바로 ORM이다.

 

ORM (Object Relation Mapping, 객체 관계 매핑)이란 클래스와 테이블 사이의 매핑 방법을 사용하여 클래스를 테이블에 영속화해주는 기술이다.

ORM 덕분에 개발자는 그저 앞서 언급한 클래스와 테이블을 매핑하는 방법만 기술하면 되고, 이에 대한 패러다임 불일치를 자동으로 해결해주기에 객체와 데이터베이스를 각자의 용도에 맞게 설계할 수 있게 해주는 장점이 있다.

 

그리고 JPA란 자바에서 제공해주는 ORM 기술의 표준이다.

*스프링에서 제공하는 것인줄 알았는데 아니었다.

 

 

사용법

일단 스프링에서 JPA의 사용을 위해서는 다음의 디펜던시가 추가되어 있어야 한다.

<dependency> 
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId> 
</dependency>

물론 굳이 수작업으로 추가할 필요까지는 없고, 프로젝트 생성 시 JPA를 선택하여 넣어주도록 하자.

 

그리고 application.properties을 통해 JPA 설정을 해준다.

설정하는 항목이야 많겠지만, 기본적으로 이 정도만 설정해주어도 충분하다.

//쿼리문 로그 확인용
spring.jpa.format_sql=true //sql 예쁘게 보기
spring.jpa.show-sql=true //쿼리문이 콘솔창에 보이게 된다.
spring.jpa.properties.hibernate.use_sql_comments=true //주석 표시
logging.level.org.hibernate.type.descriptor.sql=trace //로그에 보이는 ? 값 확인
// 위 로그는 스프링 부트 3부터는 아래 설정으로 대체된다.
// logging.level.org.hibernate.orm.jdbc.bind=trace

//DB 초기화
spring.jpa.hibernate.ddl-auto=update //클래스와 테이블의 매핑정보에 맞춰 테이블을 변경 혹은 추가한다.
//none : 변화없음, update : 변경된 스키마만 적용, validate : 변경이 있을 경우 프로그램 종료
//create : 시작될 때 drop 후 재생성, create-dtop : 시작과 종료때 모두 재생성
//혹은 아래처럼 사용
//spring.jpa.generate-ddl=true //서버 시작 지점에서 클래스에 맞춰 테이블을 생성한다.

참고로 spring.jpa.hibernate.ddl-auto 와 generate-ddl 모두 DB 초기화 전략이다.

그리고 경험상 sql=trace 는 모든 데이터를 조회시켜주기에 데이터가 많은 상황에서 사용하면 속도가 느려지니 참고하자.

 

코드는 아래와 같은 테이블이 존재한다는 가정 하에 작성한다.

ERD 클라우드에서 작성

이제 위의 테이블을 토대로 아래와 같은 도메인 클래스를 작성한다.

package com.example.demo.domain;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;

import lombok.Getter;
import lombok.Setter;

@Entity // 1
@Getter
@Setter
@Table(name="user_info") //2
public class UserInfo {
	@Id //3
	//@GeneratedValue(strategy = GenerationType.IDENTITY) //4
	@Column(name = "user_id", nullable=false, columnDefinition="varchar(10)") //5
	private String userId;
	
	@Column(name = "name", nullable=true, columnDefinition="varchar(50)")
	private String name;
	
	@Column(name = "age", nullable=false, columnDefinition="int")
	private int age;
	
	@Column(name = "gender", nullable=false, columnDefinition="varchar(1)")
	private String gender;
	
	@Column(name="is_used", nullable=false, columnDefinition="boolean")
	@ColumnDefault("false") //6
	private boolean isUsed;
	
	@Temporal(TemporalType.TIMESTAMP) //7
	@Column(name="reg_dtm", nullable=false, updatable=false)
	@CreationTimestamp //8
    //@UpdateTimestamp //9
	private Date regDtm;
}
  1. Entity : JPA가 관리하는 클래스
  2. Table : 엔티티와 매핑할 테이블 지정
  3. Id : 기본키 설정
  4. GeneratedValue : 기본키 자동 할당. IDENTITY (DB가 알아서 생성, AUTO_INCREMENT), SEQUENCE(시퀀스 사용), TABLE(키 생성 사용), AUTO(자동 지정) 으로 구성됨.
  5. Column : 멤버변수를 테이블의 컬럼에 매핑. unique 등도 사용 가능
  6. ColumnDefault : 디폴트 값 지정. 반드시 스트링으로 입력할 것 주의.
  7. Temporal : 날짜 타입 매핑에 사용. DATE, TIME, TIMESTAMP 사용 가능
  8. CreationTimestamp : Insert 발생 시 현재 시간으로 값 생성
  9. UpdateTimestamp : Update 발생 시 현재 시간으로 값 생성

 

참고로 위의 경우에는 id가 하나인데,

모든 테이블의 id가 하나일 리는 없다.

 

그런데 리포지토리를 사용하기 위해서는 클래스의 Primary Key의 타입을 지정해줘야 하는데,

이러한 경우에는 다음과 같이 별도의 pk 클래스를 만들어주어야 한다.

package com.example.demo.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name="user_info_multi_id")
@IdClass(UserInfoMultiIdPk.class) //Id 클래스 지정
public class UserInfoMultiId {
	@Id
	@Column(name = "user_id", nullable=false, columnDefinition="varchar(10)")
	private String userId;
    
	@Id
	@Column(name = "name", nullable=true, columnDefinition="varchar(50)")
	private String name;
	
	@Column(name = "age", nullable=false, columnDefinition="int")
	private int age;
}
package com.example.demo.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ToString
public class UserInfoMultiIdPk implements Serializable { //Serializable은 필수이다.
	@Id
	@Column(name = "user_id", nullable=false, columnDefinition="varchar(10)")
	private String userId;
    
	@Id
	@Column(name = "name", nullable=true, columnDefinition="varchar(50)")
	private String name;
}

리포지토리

package com.example.demo.repository;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.domain.UserInfo;

@Repository
public interface UserInfoRepository extends CrudRepository<UserInfo, String>{

}

 

일단 리포지토리는 선언만 해놓고 사용하자.

기본적으로 리포지토리가 선언되면 아래의 함수를 사용할 수 있다.

  • S save(S entity)
  • Iterable<S> saveAll(Iterable<S> entities)
  • Optional<T> findById(ID id)
  • boolean existsById(ID id)
  • Iterable<T> findAll()
  • Iterable<T> findAllById(Iterable<ID> ids)
  • long count()
  • void deleteById(ID id)
  • void delete(T entity)
  • void deleteAllById(Iterable<? extends ID> ids)
  • void deleteAll(Iterable<? extends T> entities)
  • void deleteAll()

각 메소드의 용도는 이미 이름에서 직관적으로 설명해주고 있으니 설명하지 않겠다.

추가적으로 메소드는 아래와 같이 만들어 사용할 수 있다.

 

 

UPDATE

 

자세히 보면 update문은 별도로 존재하지 않는데, save 자체가 update이기도 하고, 애초에 코드 내에서 데이터베이스에서 빼온 객체의 값을 변경하면 자동으로 변경되기에 반드시 필요하지는 않다.

 

하지만 만약 쿼리가 필요하다면 아래와 같이 작성할 수 있다.

//userId를 기반으로 userName 변경
@Transactional
@Modifying
@Query("UPDATE UserInfo u SET u.userName = :userName WHERE u.userId = :userId")
public void updateUserName(String userName, String userId);

그리고 CUD 작업 같이 DB의 상태를 변화시키는 작업은 반드시 위의 @Transactional, @Modifying을 선언해주어야 한다.

 

 

SELECT

 

기본적인 리포지토리는 아이디를 기반으로 검색을 허용하고 있다.

하지만 아이디가 아닌 다른 칼럼을 기반으로 검색을 하고 싶다면 아래와 같이 사용할 수 있다.

//이름으로 검색
//동명이인이 있을 수 있으므로 리스트 반환
@Query
public List<UserInfo> findByUserName(String userName);

//이름과 나이로 검색
//마찬가지로 같은 사람이 있을 수 있으므로 리스트 반환
@Query
public List<UserInfo> findByUserNameAndAge(String userName, int age);

보다시피 검색은 findBy<COLUMN>AND<COLUMN>AND... 와 같은 방식이다.

 

만약 BETWEEN이 필요하다면 아래와 같이 사용이 가능하다.

//해당 날짜 안에 가입한 사람 검색
@Query
public List<UserInfo> findByRegDtmBetween(Date startDate, Date endDate);

//해당 날짜 안에 가입한 사람 중 이름으로 검색
@Query
public List<UserInfo> findByUserNameAndRegDtmBetween(String userName, Date startDate, Date endDate);

 

불리안 타입으로 검색은 아래와 같이 수행한다.

//비활성화 상태의 사용자 검색
@Query
public List<UserInfo> findByIsUsedIsFalse();

//비활성화 상태의 사용자 중 이름으로 검색
@Query
public List<UserInfo> findByUserNameAndUsedIsFalse(String userName);

참고로 NULL의 경우라도 위와 같은 방식으로 아래와 같이 수행한다.

//이름이 null인 사용자 검색
@Query
public List<UserInfo> findByUserNameIsNull();

//이름이 null이 아닌 사용자 검색
@Query
public List<UserInfo> findByUserNameIsNotNull();

 

만약 LIKE 검색을 수행해야 한다면 아래와 같이 사용한다.

 

 

이 외에도 Repository에는 다양한 문법들이 존재하고, 그 내역은 아래와 같다.

키워드 예시 JPQL
Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

 


NativeQuery

 

종종 조인, 다양한 where 처리 등으로 인해 작업이 복잡해질 때가 있을 수 있다.

이럴 때는 NativeQuery를 사용할 수 있는데,

기본적으로 EntityManagercreateNativeQuery 를 사용하여 getResultList 로 반환할 수도 있고,

그냥 간단하게 아래와 같이 스트링으로 입력할 수도 있다.

@Query(value="SELECT * FROM user_dtl;", nativeQuery = true)
public List<Map<String, Object>> getAllUserDtl();

@Query(value="SELECT * FROM user_dtl WHERE user_id=?1", nativeQuery = true)
public List<Map<String, Object>> getAllUserDtlById(String id);

@Query(value="SELECT * FROM user_dtl WHERE user_id=:id", nativeQuery = true)
public List<Map<String, Object>> getAllUserDtlById(@Param("id") String id);

이 경우 결과값은 Key-Value 의 리스트, 즉 맵의 리스트가 나온다.

그렇기에 각 키를 매핑되는 클래스의 멤버변수에 맞게 입력하는 작업이 필요한데,

물론 그냥 수작업으로 Setter를 사용하여 매핑할 수도 있겠지만 @JsonProperty를 사용할 수도 있다.

@JsonProperty("user_id") //매핑될 키
@Id
@Column(name = "user_id", nullable=false, columnDefinition="varchar(10)")
private String userId;

위의 도메인 클래스의 각 멤버변수 위에 JsonProperty를 추가하고,

아래와 같이 ObjectMapper을 사용해 자동으로 매핑해줄 수 있다.

@Autowired ObjectMapper mapper;
@Autowired UserInfoRepository userRepo;

...

List<UserDtl> userList = new ArrayList<>();
List<Map<String, Object>> queryResult = userRepo.getAllUserDtl();

for(Map<String, Object> queryRes : queryResult) 
	userList.add(mapper.convertValue(queryRes, UserDtl.class));

일부 테이블만 가져오기

 

때로는 모든 테이블의 모든 칼럼까지는 필요하지 않고, 일부 칼럼만 필요한 경우가 있다.

물론 테이블을 통째로 가져와 필요한 칼럼만 사용하는 방법도 있겠지만, 그러면 당연히 속도가 느려질 수 있으므로,

아래와 같이 필요한 칼럼만 가져올 수 있다.

package com.example.demo.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;

@Entity
@Getter
@Table(name="user_info")
public class UserInfoVO {
	//아이디와 이름만 가져오는 VO
	@Id
	@Column(name = "user_id", nullable=false, columnDefinition="varchar(10)")
	private String userId;
	
	@Column(name = "name", nullable=true, columnDefinition="varchar(50)")
	private String name;\
}

 

기존 테이블의 몇 가지 칼럼만을 가진 VO 도메인 클래스를 만들고, 그 클래스를 사용하는 리포지토리 클래스를 별도로 만들어 사용하면 VO에서 지정한 데이터만을 가져오는 방법을 사용할 수 있다.

 


 

Specification

 

검색 기능을 구현한다고 생각해보자.

 

아이디, 이름 중 특정 단어가 들어가고 2021년 11월 26일과 2021년 11월 30일 사이에 가입한 사용자 중 남자인 사용자를 조회할 것이다.

 

이러한 기능은 문법들을 조합하면 충분히 구현할 수는 있겠지만, 함수의 길이가 너무 길어진다.

 

게다가 아이디, 이름 중 특정 단어가 들어가는 조건은 OR로 처리해야 하는데, 다른 조건은 AND로 처리되어 순서를 어떻게 해야할 지 감도 잘 잡히지 않고,

 

추가로 만약 검색 값이 "이"와 같은 특정 단어가 아닌 NULL 등이 오는 경우에 대한 예외 처리도 해주어야 한다.

 

이러한 불편함을 해결하기 위해 Specification이 존재한다.

 

사용법은 아래와 같다.

 

먼저 Specification을 선언해준다.

package com.example.demo.repository;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;

import com.example.demo.domain.UserInfo;

public class UserInfoSpecification {
	
	public static Specification<UserInfo> searchUserInfo(String searchText, Date startDate, Date endDate) {
		return (Specification<UserInfo>) ((root, query, builder) -> {
			//root은 UserInfo이고,
			//query는 CriteriaQuery,
			//builder은 CriteriaBuilder이다.
			
			//검색 조건을 저장할 리스트
			List<Predicate> predicate = new ArrayList<>();
			
			//남자인 조건
			//root (UserInfo)의 gender 멤버변수가 M인 것을 찾는 쿼리이다.
			predicate.add(builder.equal(root.get("gender"), "M"));
			
			//가입일 조건
			//startDate 과 endDate의 사이 쿼리
			if(startDate != null && endDate != null)
				predicate.add(builder.between(root.get("instDtm"), startDate, endDate));
			
			//검색 단어 조건
			if(searchText != null) {
				//각 predicate를 선언한 후
				Predicate idPredicate = builder.like(root.get("userId"), "%"+searchText+"%");
				Predicate namePredicate = builder.like(root.get("name"), "%"+searchText+"%");
				
				//or로 묶어 리스트에 추가한다.
				//지금은 두 개로 했지만, 몇 개가 들어가도 상관없다.
				Predicate combilePredicate = builder.or(idPredicate, namePredicate);
				predicate.add(combilePredicate);
			}
			
			//저장된 모든 쿼리의 항목들을 AND로 묶어 반환한다.
			return builder.and(predicate.toArray(new Predicate[0]));
		});
	}
}

 

그 후에는 사전에 선언한 Repository에 JpaSpecificationExecuter를 추가 상속하여 Specification을 사용할 것임을 명시하자.

public interface UserInfoRepository extends CrudRepository<UserInfo, String>, JpaSpecificationExecutor<UserInfo>{

 

그 후에 findAll을 선언한 후 앞서 제작한 함수를 입력하면 조건에 맞는 리스트가 출력된다.

List<UserInfo> userInfoList = userRepo.findAll(UserSpecification.searchUserInfo("이", startDate, endDate));

 

728x90
반응형

'실습 > 리눅스 서버 + 스프링 부트' 카테고리의 다른 글

ntp를 이용한 타임서버 구축  (0) 2022.01.22
Scheduler  (0) 2022.01.15
Spring Security  (0) 2021.11.07
HAProxy를 활용한 IFrame 활성화  (0) 2021.09.26
[스프링부트] Thymeleaf  (0) 2021.09.26