jpa 시작해보기
> 아주 기본만 익히고, 일단 실습. 필요한 것은 그때그때 찾아본다.
==> JPA 1회독 하고, 다시 이 글을 보니... 참... 맨땅에 삽질했구나싶다. 결국 기본부터 다시 보게 된다...
개발환경
java 1.8
springboot 2.1.9
gradle 4.10.2
1. 패키지명을 java.orm.jpa로 해서 Application.java를 실행하면, 다음 에러 발생
누가 패키지명을 java로 시작하나-_-'
---------------------------
Java Virtual Machine Launcher
---------------------------
Error: A JNI error has occurred, please check your installation and try again
---------------------------
확인
---------------------------
2. 클래스명을 Order로 하면 다음 에러 발생
2021-04-05 21:13:49.754 WARN 14880 --- [ restartedMain] o.h.t.s.i.ExceptionHandlerLoggedImpl : GenerationTarget encountered exception accepting command : Error executing DDL "create table order (order_id bigint not null, name varchar(255), member_id bigint, primary key (order_id)) engine=InnoDB" via JDBC Statement
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table order (order_id bigint not null, name varchar(255), member_id bigint, primary key (order_id)) engine=InnoDB" via JDBC Statement
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final]
해결: Orders로 변경
또는 Order class에 @Table(name = "ORDERS") 추가
3. FK가 있는 경우 에러
2021-04-05 21:17:38.390 WARN 16476 --- [ restartedMain] o.h.t.s.i.ExceptionHandlerLoggedImpl : GenerationTarget encountered exception accepting command : Error executing DDL "alter table orders drop foreign key FKpktxwhj3x9m4gth5ff6bkqgeb" via JDBC Statement
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table orders drop foreign key FKpktxwhj3x9m4gth5ff6bkqgeb" via JDBC Statement
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final]
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ORDERS" not found; SQL statement:
alter table orders drop foreign key FKpktxwhj3x9m4gth5ff6bkqgeb [42102-199]
해결: application.properties 추가
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
==> 이제 보니 테이블을 못찾은 것 같은데??
===> 서버 시작할 때, drop을 하려고 하는데, 없으니 당연히 에러... ddl-auto=update가 맞다.
4. Test Case에서 Member 도메인에 걸어놓은 Lombok의 @ToString 어노테이션 에러
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.example.domain.member.Member.orders, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:600)
at java.lang.String.valueOf(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at org.example.domain.member.Member.toString(Member.java:22)
at java.lang.String.valueOf(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at java.util.AbstractCollection.toString(Unknown Source)
at java.lang.String.valueOf(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at org.example.domain.member.MemberRepositoryTest.멤버저장_불러오기(MemberRepositoryTest.java:42)
==> lombok @ToString대신 직접 만들기.
==> 이제 보니, fetch가 문제인가? could not initialize proxy - no Session 문제
===> 영속성은 끊어졌는데, 프록시객체를 조회하려고 해서 그런가?
5. Test Case에서 다음에러 발생
org.springframework.orm.jpa.JpaSystemException: No default constructor for entity: : org.example.domain.Address; nested exception is org.hibernate.InstantiationException: No default constructor for entity: : org.example.domain.Address
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:352)
Caused by: org.hibernate.InstantiationException: No default constructor for entity: : org.example.domain.Address
at org.hibernate.tuple.PojoInstantiator.instantiate(PojoInstantiator.java:85)
Address class에 @AllArgsConstructor 처리를 했는데,
해결: @NoArgsConstructor 도 필요하다.
==> 엔티티는 기본생성자 필수
6. 객체 비교?
Member member2 = memberService.findById(saveId);
assertEquals(member.getId(), member2.getId());
assertEquals(member.getName(), member2.getName());
// 대신, 객체 비교를 할려면, hashCode, equals 재정의?
assertEquals(member, member2);
7. 왜 JpaRepository에는 find 함수가 없을까?
EntityManager에는 find함수가 있다.ㅠㅠ
em.find(Item.class, id);
8. 서버 실행하면 되는데, 왜 TestCase에서는 안될까?
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.example.domain.order.Order.orderItems, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148)
at org.hibernate.collection.internal.PersistentBag.get(PersistentBag.java:540)
at org.example.OrderServiceTest.상품주문(OrderServiceTest.java:50)
Test class에 @Transactional 추가 필요
==> 음,, 이제 보니 이것도 could not initialize proxy - no Session 오류..
9. JPA의 동시성 문제는 어떻게 처리하나?
A상품, B상품을 동시에 조회해서 재고를 차감하면, 둘다 반영해야 한다.
==> "트랜잭션 락" 참고~
10. Entity 순환참조 이슈...
단순히 DTO만 쓴다고 해결되는 문제는 아니다.
Jackson 1.6+ 버전에서는
부모클래스에 @JsonManagedReference와 참조클래스에@JsonBackReference
Jackson 2.0+ 에서는
@JsonIdentityInfo
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
클래스에 추가하면,
무한루프에 걸리는 부분(products)이 id만 남고, 나머지는 나타나지 않아서, 순환참조를 피해갈 수 있다.
{
"category": [
{
"id": 1,
"name": "category12",
"products": [
{
"id": 2,
"name": "name0.21342207004622693",
"category": {
"id": 1,
"name": "category12",
"products": [
2
]
}
}
]
},
{
"id": 3,
"name": "category12",
"products": [
]
}
]
}
{
"product": [
{
"id": 2,
"name": "name0.21342207004622693",
"category": {
"id": 1,
"name": "category12",
"products": [
2
]
}
},
{
"id": 4,
"name": "name0.11252326618453645",
"category": null
}
]
}
@JsonManagedReference와 @JsonBackReference를 사용하면, 한쪽만 제대로 볼 수 있고, 나머지 한쪽은 포기해야 한다.
@JsonIdentityInfo를 사용하면, 양쪽모두 볼 수 있다.
==> 이상한 현상 발견. category1개를 이용해서 product 여러개를 동시에 생성할 경우, 아래와 같이 json 구조가 나온다. 음....
{
"product": [
{
"id": 3,
"name": "name8.075188198917967",
"category": {
"id": 2,
"name": "name1",
"products": [
3,
{
"id": 4,
"name": "name4.431918383718946",
"category": {
"id": 2,
"name": "name1",
"products": [
3,
4,
{
"id": 5,
"name": "name7.751463573838379",
"category": {
"id": 2,
"name": "name1",
"products": [
3,
4,
5,
==> 개선
@JsonManagedReference와 @JsonBackReference를 사용하되 Dto를 이용해서 json 출력하기.
Dto에서 아래와 같이 하더라도,
category 및 category 에 속한 product까지는 출력되지만, 그 product에 속한 category는 출력되지 않는다. @JsonBackReference 때문에...
@Data
public class ProductDto {
private final Long id;
private final String name;
private Category category;
11. AOP 에러 처리
//@Aspect
//@Component
@RestControllerAdvice
public class ControllerAspect {
// 400이면서 에러 메시지를 json으로 넘기기
@ExceptionHandler(ValidationException.class)
public ResponseEntity handleCustomException(ValidationException ex) {
return new ResponseEntity<>(ResponseDto.of(ex.getMessage()), null, HttpStatus.BAD_REQUEST);
}
// 200이면서 에러 메시지를 json으로 넘기기
// @ExceptionHandler(ValidationException.class)
// public ResponseDto handleCustomException(ValidationException ex) {
// return ResponseDto.of(ex.getMessage());
// }
@ExceptionHandler(CustomException.class)
public ResponseDto handleCustomException(CustomException ex) {
return ResponseDto.of(ex.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseDto handleCustomException(IllegalArgumentException ex) {
return ResponseDto.of(ex.getMessage());
}
}
12. custom validator
==> enum도 검증해보자.
velog.io/@hellozin/Annotation%EC%9C%BC%EB%A1%9C-Enum-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0
==> Exception발생시 aop에서 받아야 한다.
그런데, 너무 다양한 에러가 발생한다.
단순히 e.getMessage()로 했을때, 패키지 정보까지 다 줘버리니...-_-;
BindException
@GetMapping("/test11")
public ResponseEntity test1(@Valid GetRequest request/*, BindingResult bindingResult*/) {
MethodArgumentNotValidException
@PostMapping("/test3")
public String test2(@Valid @RequestBody InputRequest request) {
ConstraintViolationException
리스트각 요소를 형변환 하다가...
@PostMapping("/test5")
public String test5(@Valid @RequestBody List<PostRequest> request) {
ValidationException
Exception (NumberFormatException)
@GetMapping("/test2/{id}")
public String test2(@PathVariable("id") @Min(5) int id) {
==> 컨트롤러에서 파라메터 변환을 할 수 있다.
@Component
public class StringToLocalDateTimeConverter implements Converter<String, MyLocalDateTime> {
13.querydsl gradle 세팅 실패
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileQuerydsl'.
> Annotation processor 'com.querydsl.apt.jpa.JPAAnnotationProcessor' not found
다음을 추가했다.
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
querydsl.extendsFrom compileClasspath
}
14.
jpa
could not initialize proxy - no Session
==> LAZY로 안되는 문제
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
15
@Query 의 select 절에 new DTO()
DTO 생성자 함수에서 sort하려고ㅠㅠ stream을 사용하면 다음 에러 발생..
그냥, Collections 써야겠다..
@Query("select new com.kakaocommerce.order.api.dto.OrderResultDto(o) from Order o "
this.items = order.getOrderItems().stream().sorted().collect(Collectors.toList()); // Tuple 변환 에러발생
{
"data": null,
"error": "org.hibernate.QueryException: could not instantiate class [com.kakaocommerce.order.api.dto.OrderResultDto] from tuple; nested exception is java.lang.IllegalArgumentException: org.hibernate.QueryException: could not instantiate class [com.kakaocommerce.order.api.dto.OrderResultDto] from tuple"
}
16. h2에서 rollback이 안된다.
@Transactional로 하고, RuntimeException을 날렸는데,,,, 롤백이 안된다ㅠㅠ jpa에서...
==> 아.. insert rollback은 되는데, 이미 증가된 sequence가 롤백이 안되는 거였다.
17. 딜레마
parent
children: fk는 not null
=======> cascade 영속성전이 이용
children의 fk는 not null이므로, parent를 먼저 save해서 id를 받아오면, children을 save
그런데, parent를 save하고, children save에서 에러발생하면(length 초과), insert 롤백은 할 수 있지만 parent의 auto_increment는 롤백이 안된다.ㅠ
최대한 insert시에 에러가 발생하는 것이 아니라
그 전에 class 생성후 setter에서 데이터 체크가 되면 좋을텐데...
==> @Column, @Size, @Length의 차이를 이해하고, setter하고 나서 validation 을 돌리면 된다.
@Size(max = 50)
private String name;
// 검증
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.usingContext().getValidator();
Set<ConstraintViolation<OrderItem>> constrains = validator.validate(orderItem);
for (ConstraintViolation<OrderItem> constrain : constrains) {
throw new IllegalArgumentException("[" + constrain.getPropertyPath() + "][" + constrain.getMessage() + "]");
}
참고: phoenixnap.com/kb/spring-boot-validation-for-rest-services
=> 이걸 좀더 편하게 하면 좋을 텐데....
=> 어차피 모든 Model에 적용을 하면 좋으니,,, 뭔가 공통적으로 해주는 무언가 있지 않을까?
'Java > Spring' 카테고리의 다른 글
[Spring data JPA] 조회: Entity + 연관 Entity(LAZY) + Pageable (0) | 2021.04.24 |
---|---|
[Spring data JPA] native query + XML + DTO (0) | 2021.04.24 |
JPA 씹어먹기 (0) | 2021.04.19 |
[spring을 spring 답게] net.sf.log4jdbc.sql.jdbcapi.DriverSpy (0) | 2020.03.28 |
Memory usage is low, parachute is non existent, your system may start failing (0) | 2017.10.25 |