JDBC
자바 언어로 데이터베이스 프로그래밍을 하기위한 라이브러리
JPA
Java ORM(Object Relational Mapping) 표준 기술
- ORM : 객체와 데이터베이스의 관계를 맵핑 하는 방법
등장배경
Spring Data JPA의 등장배경은 JPA ORM을 사용하기 위해서 다음과 같은 라이브러리를 사용하고 있다가
- Hibernate
- EclipseLink
- DataNucleus
- OpenJPA
- TopLink
아래는 위와 같은 라이브러리 코드의 일부인데 모든 라이브러리들이 아래와 같은
public class User {
private String name;
private int age;
private String email;
}
EnttiyManager em = emf.createEntityManager();
var user = new User("홍길동", 20, "hong@gmail.com");
em.persist(user);
em.getTransaction().commit();
em.close();
반복적인 작업이 일어난다는 것을 알아차리고 이를 "그냥 처음부터 Spring에서 관리해줄게!!!" 하면서 등장한 것이 바로 Spring Data JPA
이다.
- 도식도 👇
How To Use
설정
Spring initializer에서 MySQL(사용시), JPA, Web, Lombok를 선택하고 프로젝트를 생성한다.
처음 프로젝트를 생성하면 resources 폴더에 application.properties
파일이 생성이 되는데 이 파일을 application.yaml
로 변경하고 아래 내용을 작성한다.
spring:
jpa:
# 실행되는 코드 log 찍어줌
show-sql: true
properties:
format_sql: true
# 사용하는 MySQL 버전에 따라서 달라짐
# MySQL8 << 이런식으로 class에서 검색해서 최상단에 import한 값 + class name으로 지정
dialect: org.hibernate.dialect.MySQL8Dialect
hibernate:
# validate - 연결할려는 DB와 Entity가 있는지 확인하고 없으면 Error
# create - 연결하려는 DB나 Entity가 없으면 생성
# update - 데이터 베이스와 내 Entity를 비교 후 다른 점이 있다면 업데이트
# drop - Spring 종료시 삭제
ddl-auto: validate
datasource:
# 연결하고자 하는 DB url
# 3306/[] << DB 이름이 들어감 ex) data_store, user ...
url: jdbc:mysql://localhost:3306/[DB이름]?userSSL=[ssl설정]&useUnicode=true&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: "비밀번호"
Entity 자동 생성
Entity가 자동으로 생성되게 할려면(Database의 테이블이 없는 경우) spring.jpa.hibernate.ddl-auto
를 create
로 설정해주면 된다.
위에서 사용한 설정은 validate
로 설정되어 있어서 Database에 테이블이 없으면 Error를 뱉어준다.
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: create
Config 파일 생성
@EntityScan
은 basePackages 속성에 정의된 패키지에서 JPA Entity class를 찾아서 Spring Data JPA가 사용할 수 있도록 등록한다.
@EnableJpaRepositories
Spring Data JPA Repository 인터페이스를 구현한 클래스를 찾아서 Spring Data JPA가 사용할 수 있도록 설정한다. basePackages에 정의된 패키지에서 Repository를 찾아서 등록한다.
@Configuration
@EntityScan(basePackages = {"org.delivery.db"})
@EnableJpaRepositories(basePackages = {"org.delivery.db"})
public class JpaConfig {
}
Entity 생성
Entity를 ORM과 연결시에 주의할점은 Entity Annotation의 name에 테이블 이름을 지정해줘야 한다는 점과 @ID
를 반드시 지정해줘야 한다는 것이다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = "user") // 테이블 이름
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserEntity {
@Id
// 해당 데이터는 MySQL에 의해서 Auto Increment가 된다는 것을 의미한다.
// strategy의 종류는 DB에 따라서 달라진다.
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String email;
}
@JoinColumn
만약에 Entity의 변수명과 DB의 Column명이 다르다면 @JoinColumn
Annotation의 name 옵션을 사용해서 매핑해줄 수 있다.
@JoinColumn
Annotation은 DB와 매핑시에 기본적으로 변수명_id
와 같은 식으로 매핑되게 된다. 예를 들어서 변수명이 storeEntity라면 'store_entity_id'와 같은 형식으로 매핑되게 된다.
@ManyToOne
@JoinColumn(nullable = false, name = "store")
/// 여기를 store로 바꿔서 매핑해줄 수도 있음
private StoreEntity storeEntity;
Auditing
EntityListeners를 활용해서 LocalDateTime
을 자동으로 생성하는 방법은 다음과 같다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 100)
private String email;
@Column(length = 30)
private String name;
@Column(name = "created_at")
// 객체가 생성될 때 자동으로 생성
@CreatedDate
private LocalDateTime createdAt;
@Column(name = "updated_at")
// 데이터가 업데이트 될 때 자동으로 생성
@LastModifiedDate
private LocalDateTime updatedAt;
}
이 때 EntityListeners를 사용하기 위해서는 Spring Boot Main 부분에 @EnableJpaAuditing
Annotation을 붙여줘야 한다.
@EnableJpaAuditing
@SpringBootApplication
public class SpringDataRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataRedisApplication.class, args);
}
}
CreatedBy, LastModifiedBy
Entity에 @CreatedBy
, @LastModifiedBy
를 사용하면 해당 필드에 대해 자동으로 값이 들어가게 된다.
@CreatedBy
@Column(updatable = false, name = "created_by")
lateinit var createdBy: String
그리고 저 필드를 업데이트 할 때 작용할 Bean으로 AuditorAware 클래스를 상속받아서 구현해주면 된다.
@Component("auditAwareImpl")
class AuditAwareImpl: AuditorAware<String> { // 만약 필드가 Long이면 Long 타입으로 들어가야 함
override fun getCurrentAuditor(): Optional<String> {
val username = "SYSTEM"
return Optional.of(username)
}
}
마지막으로, Main Class위에 붙어있는 @EnableJpaAuditing
Annotation을 사용해서 AuditorAware를 사용할 수 있도록 해준다.
@EnableJpaAuditing(auditorAwareRef = "auditAwareImpl")
@SpringBootApplication
class AccountsApplication
Repository 생성
Memory DB 프로젝트 (opens in a new tab)에서 생성한 Repository의 구조를 보면 Repository 인터페이스를 DataRepository가 implements하고 DataRepository를 SimpleDataRepository가 implements하고 각 Repository에 대한 구현체가 있었다.
그러나 Spring Data에서는 JpaRepository\<[Entity], [ID Type]\>
인터페이스를 상속받으면 된다.
JpaRepository를 뜯어보면 이미 내부적으로 JpaRepository -> PagingAndSortingRepository -> CrudRepository 순으로 구현이 되어있다.
public interface UserRepository extends JpaRepository<UserEntity, Long> /* Entity / Id Type */{
}
Controller
Find All
@RequestMapping("/api/user")
public class UserApiController {
private final UserRepository userRepository;
@GetMapping("/find-all")
public List<UserEntity> findAll() {
return userRepository.findAll();
}
...
}
Create
@GetMapping("/add/{name}")
public void autoSave(
@PathVariable String name
) {
/*
INSERT INTO user.user (name) VALUES (value)
위의 Query와 동일한 코드이다.
*/
var user = UserEntity.builder().name(name).build();
userRepository.save(user);
}
JPA Query Method
JPA로 기존에 생성되어 있는 쿼리 외에 작성하는 방법은 크게 3가지가 있다.
JPA Query Method
형식은 쿼리메서드By[변수-필드]조건문
와 같고 조건문은 공식문서 (opens in a new tab)에서 확인할 수 있다.
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// select * from user where score > [??]
// SELECT * FROM book_store.`user` WHERE score > 95;
// 쿼리메서드By[변수]조건문 : JPA에서는 메서드의 명에 따라서 Query를 자동으로 생성해준다.
public List<UserEntity> findAllByScoreGreaterThan(Integer sc);
}
Native Query
@Query에 nativeQuery를 true로 지정하고 Value 옵션을 사용해서 직접 Query를 작성할 수 있다. 이 때, Parameter 앞에 @Param Annotation을 사용해서 인자를 지정해주는 방식과 아니면 그냥 들어온 순서대로 ?1, ?2 ... 이런식으로 매칭하는 방식이 있다.
@Query(
// form user u : user는 Entity를 뜻하고 u는 user의 별칭
// u.score : user entity 안에 있는 score 필드(변수)
// ?1, ?2 : 들어오는 첫 번째 인자, 두 번째 인자
// named parameter 매칭 방식 : @Param으로 등록해준 이름으로 :[이름]을 사용해서 매칭한다.
value = "select * from user u where u.score <= :high and u.score >= :low",
nativeQuery = true
)
public List<UserEntity> scoreBetween(
@Param(value = "high") Integer high,
@Param(value = "low") Integer low);
JPQL Query
사실 Native Query와 크게 다르지는 않은데 별칭을 필수로 사용한다는 부분에서 차이가 있는 것(* 사용 불가)같다.
@Query(
// form user u : user는 Entity를 뜻하고 u는 user의 별칭
// u.score : user entity 안에 있는 score 필드(변수)
// ?1, ?2 : 들어오는 첫 번째 인자, 두 번째 인자
"select u from user u where u.score <= ?1 and u.score >= ?2"
)
public List<UserEntity> scoreBetween(
Integer high,
Integer low);
참고
Memory DB를 Jpa Repository로 변경 후 MySQL 연동 + JPA Query 작성 #1 (opens in a new tab)
JPA 연관
Board와 Post를 Board(1) : Post(N)로 설정하고자 한다고 할 때 Board 쪽에는 OneToMany
Annotation을 사용해줄 수 있다.
이 때 지정될 PostEntity의 변수명을 입력해주면 된다.
public class BoardEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String boardName;
private String status;
@OneToMany(
mappedBy = "board"
)
private List<PostEntity> postList = List.of();
}
PostEntity 측에선 ManyToOne
Annotation을 통해서 어떤 변수와 연관있는지를 지정해준다.
그러면 이 부분에 대해서 자동으로 _id를 붙여주게 되는데 실제 데이터 베이스의 해당 부분이 board_id 이므로 변수명을 board라고 해준 것이다.
public class PostEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 이 Annotation을 붙여주면 해당 변수를 Column으로 인식을 한다.
// 자동으로 뒤에다가 _id를 붙여주게 된다.
@ManyToOne
private BoardEntity board;
private String userName;
private String password;
private String email;
private String status;
private String title;
// Database의 sql 유형이 TEXT인 경우
// 아래의 Annotation을 통해서 데이터 타입을 맞춰줄 수 있음
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime postedAt;
// Entity Annotation이 붙어있으면 기본적으로 이곳에 선언된 변수들은 DB의 Column으로 인식한다.
// Transient Annotation을 붙여주면 DB의 Column으로 인식하지 않는다.(Container 등록시에 무시함)
@Transient
private List<ReplyEntity> replyList = List.of();
@Transient
private Integer replyCount;
}
무한 참조 오류와 DTO
위의 코드에서 오류가 발생할 수 있다. 그 이유는 Json Mapping과 toString 때문이다.
Json Mapper는 getter를 참조해서 Json Mapping을 해주게 되는데 PostEntity의 아래 부분과
private BoardEntity board;
BoardEntity의 아래 부분이 계속 참조를 반복하면서
private List<PostEntity> postList = List.of();
PostEntity를 참조하라고? 알았어. > PostEntity로 이동 > BoardEntity를 참조하라고? 알았어. > BoardEntity로 이동 > PostEntity를 참조하라고? 알았어.
를 무한히 반복하면서 발생하는 오류이다. toString도 역시 위와 비슷한 이유로 무한 참조 오류가 발생한다.
이를 끊어내기 위해서는 한쪽에서 참조의 순회를 끊어줘야 하는데 더 이상 참조할 필요가 없는 곳에서 @JsonIgnore
과 @ToString.Exclude
Annotation을 사용해준다.
나는 이 순회를 PostEntity에서 끊어주기로 했다.
왜냐하면 JsonIgnore로 인해서 BoardID가 전달되지 않게 되더라도 Reponse를 받는 입장에서는 BoardID를 전달받을 이유가 없기 때문이다.
public class PostEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 이 Annotation을 붙여주면 해당 변수를 Column으로 인식을 한다.
// 자동으로 뒤에다가 _id를 붙여주게 된다.
@ManyToOne
// Object Mapper가 BoardEntity에서 참조를 하고 BoardEntity에서 또 이곳을 참조하고 ... 하는 식으로
// 상호 참조가 반복되어서 무한 루프가 돌게되는데 이를 방지하기 위해서 @JsonIgnore를 붙여준다.
@JsonIgnore
// toString끼리 상호참조로 인한 무한 루프를 방지하기 위해서 사용한다.
@ToString.Exclude
private BoardEntity board;
private String userName;
private String password;
private String email;
private String status;
private String title;
// Database의 sql 유형이 TEXT인 경우
// 아래의 Annotation을 통해서 데이터 타입을 맞춰줄 수 있음
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime postedAt;
// Entity Annotation이 붙어있으면 기본적으로 이곳에 선언된 변수들은 DB의 Column으로 인식한다.
// Transient Annotation을 붙여주면 DB의 Column으로 인식하지 않는다.(Container 등록시에 무시함)
@Transient
private List<ReplyEntity> replyList = List.of();
@Transient
private Integer replyCount;
}
DTO
위와 같은 오류와 클라이언트 측에 보내줄 필요가 없는 부분을 관리하기 위해서 Response에 사용할 DTO(Data Transfer Object)를 설계하게 된다.
고로 지금까지의 UseCase를 따지자면 RequestModel > ModelEntity > RequestDto
순으로 동작하게 되며 이는 Api에서 Request 호출 > Entity 참조 > Reponse(DTO) 전송과 같은 역할을 하게 된다.
ManyToOne
내가 다수이고 상대방이 하나인 경우에 사용하는 옵션
@ManyToOne
@JoinColumn(nullable = false, name = "store_id")
/// 여기를 store로 바꿔줘도 됨
private StoreEntity storeEntity;
Error
Jpa 변수명과 Entity의 연관관계
Jpa Query는 Entity의 변수명을 기준으로 쿼리를 작성하는데 변수명을 userOrderMenuEntity
로 작성하고
public class UserOrderMenuEntity extends BaseEntity {
@ManyToOne
@JoinColumn(nullable = false, name = "user_order_id")
private UserOrderEntity userOrderEntity; // UserOrderID 1 : n UserOrderMenuEntity
...
}
쿼리에다가 UserOrderId를 써놔서 발생한 에러
interface UserOrderMenuRepository: JpaRepository<UserOrderMenuEntity, Long> {
// select * from user_order_menu where user_order_id = ? status = ?
fun findAllByUserOrderIdAndStatus(userOrderId: Long?, status: UserOrderMenuStatus?): List<UserOrderMenuEntity>
}
해결방법은 두 가지이다.
- 변수명을 UserOrder로 변경한다.
public class UserOrderMenuEntity extends BaseEntity {
@ManyToOne
@JoinColumn(nullable = false, name = "user_order_id")
private UserOrderEntity userOrder; // UserOrderID 1 : n UserOrderMenuEntity
...
}
- 쿼리에 들어가는 변수명을 UserOrderEntityId로 변경한다.
interface UserOrderMenuRepository: JpaRepository<UserOrderMenuEntity, Long> {
// select * from user_order_menu where user_order_id = ? status = ?
fun findAllByUserOrderEntityIdAndStatus(userOrderId: Long?, status: UserOrderMenuStatus?): List<UserOrderMenuEntity>
}
그런데 2번 방법을 택할경우 뭔가 쿼리가 이상해지므로 1번 방법을 선택하기로 한다.
에러 메시지
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userOrderBusiness' defined in file [/Users/rookedsysc/Documents/delivery-app/delivery-java-project/api/build/classes/java/main/org/delivery/api/domain/userorder/business/UserOrderBusiness.class]: Unsatisfied dependency expressed through constructor parameter 5; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userOrderMenuService' defined in file [/Users/rookedsysc/Documents/delivery-app/delivery-java-project/api/build/classes/java/main/org/delivery/api/domain/userordermenu/service/UserOrderMenuService.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userOrderMenuRepository' defined in org.delivery.db.userordermenu.UserOrderMenuRepository defined in @EnableJpaRepositories declared on JpaConfig: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List org.delivery.db.userordermenu.UserOrderMenuRepository.findAllByUserOrderIdAndStatus(java.lang.Long,org.delivery.db.userordermenu.enums.UserOrderMenuStatus); Reason: Failed to create query for method public abstract java.util.List org.delivery.db.userordermenu.UserOrderMenuRepository.findAllByUserOrderIdAndStatus(java.lang.Long,org.delivery.db.userordermenu.enums.UserOrderMenuStatus)! No property 'userOrderId' found for type 'UserOrderMenuEntity'; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List org.delivery.db.userordermenu.UserOrderMenuRepository.findAllByUserOrderIdAndStatus(java.lang.Long,org.delivery.db.userordermenu.enums.UserOrderMenuStatus)! No property 'userOrderId' found for type 'UserOrderMenuEntity'
Reference
참조할 Commit (opens in a new tab) - 변경내역 중에서 Entity.java 파일 참조