본문 바로가기
Java/Spring

springBoot2 jpa Test Errors

by java개발자 2021. 4. 5.

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

bamdule.tistory.com/107

==> 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>();

javaworld.co.kr/85

 

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에 적용을 하면 좋으니,,, 뭔가 공통적으로 해주는 무언가 있지 않을까?