나의 에러 일지

JPA - Querydsl-sql 사용 시 Table not found 원인과 해결 방법

Cold Bean 2024. 11. 7. 14:23
728x90

개요

프로젝트를 Mybatis -> JPA, QueryDsl로 마이그레이션하고 있다.

복잡한 쿼리가 있어서 서브쿼리를 프롬절에서 사용해야 했는데, QueryDsl-jpa에서는 from절 서브쿼리를 지원하지 않아서 QueryDsl-sql을 사용해야 했다.

 

이 과정에서 발생했던 에러 처리 과정을 남긴다. 해결 방법은 간단했지만 원인을 알기 위해서 Querydsl, JPA, JPQL, NativeSQL을 조금 깊게 파고들 수 있는 계기가 되었다.

 

문제 발생

쿼리를 테스트하는 과정에서 아래와 같은 에러가 발생했다.

ORGANIZATIONLEADENTITY 테이블을 찾을 수 없다고 한다. 

nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement] with root cause
[2024-11-06 22:04:28.028] [ERROR] [http-nio-8080-exec-6] [o.h.e.j.s.SqlExceptionHelper] Table "ORGANIZATIONLEADENTITY" not found;

 

이상한 점은 엔티티 클래스 이름(ORGANIZATION_LEAD_ENTITY)과 테이블 이름(ORGANIZATION_LEAD)이 다르기 때문에 @Entity name 속성을 사용해서 엔티티 이름을 테이블 이름과 일치시켜 주었었다.

하지만 에러 내용을 확인해 보면 엔티티 클래스 명을 그대로 Table Name으로 사용하고 있는 듯했다. 

@Getter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@ToString(exclude = {"organization", "user"})
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "ORGANIZATION_LEAD")
public class OrganizationLeadEntity extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "organization_id")
    private OrganizationEntity organization;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "lead_id")
    private UsersEntity user;

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDate startDate;

    private LocalDate endDate;
}

 

원인

프로젝트 내에서는 Querydsl-jpa와 Querydsl-sql을 함께 사용하고 있었고 Querydsl-jpa로 작성한 코드에서는 문제없이 동작하고 있었다. 그래서 Querydsl-jpa와 Querydsl-sql의 차이를 알아보았다.

Querydsl

JPA만으로는 복잡한 쿼리, 동적 쿼리를 처리하는 데 한계가 있다.

이를 해결하기 위해 JPQL, Criteria를 사용하기도 하지만 나는 Querydsl을 사용하기로 했다.

Querydsl은 HQL(Hibernate Query Langauage)를 Type-safety하게 생성하고 관리해주는 라이브러리이다.

PQL과 다르게 문자가 아닌 코드로 쿼리를 작성하기 때문에 컴파일 타임에서 오류를 확인할 수 있다는 점도 큰 장점 중 하나다.

// JPQL
String jpql = "SELECT m FROM Member m WHERE m.name = :name AND m.age >= :age";
List<Member> members = entityManager.createQuery(jpql, Member.class)
    .setParameter("name", "개발하는콩")
    .setParameter("age", 30)
    .getResultList();
    
    
// Criteria
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> member = cq.from(Member.class);

cq.select(member)
  .where(cb.and(
      cb.equal(member.get("name"), "개발하는콩"),
      cb.greaterThanOrEqualTo(member.get("age"), 30)
  ));

List<Member> members = entityManager.createQuery(cq).getResultList();


// Querydsl
QMember member = QMember.member;

List<Member> members = queryFactory
    .selectFrom(member)
    .where(member.name.eq("개발하는콩")
        .and(member.age.goe(30)))
    .fetch();

 

Querydsl-jpa vs Querydsl-sql

우리가 흔히 말하는 Querydsl은 Querydsl-jpa를 말한다. 그렇다면 Querydsl-jpa와 Querydsl-sql의 차이는 무엇일까

 

Querydsl-jpa

  • 엔티티를 기반으로 Q클래스 생성
  • JPQL을 생성
  • JPA 엔티티를 기반으로 작동. 객체 지향적인 쿼리
  • 동작: 쿼리 -> Querydsl이 JPQL로 변환 -> JPA구현체가 SQL로 변환 -> DB 전달

Querydsl-sql

  • DB 스키마를 기반으로 Q클래스 생성
  • Native SQL를 생성
  • DB 테이블과 직접 매핑
  • 동작: 쿼리 -> Querydsl이 SQL로 변환 -> DB 전달

위 차이에서 유추해 볼 수 있는 것은 Querydsl-sql은 JPQL을 사용하지 않는다.

 

JPQL vs Native SQL

JPQL(Java Persistence Query Language)

  • 객체 지향적인 쿼리 언어로, 엔티티와 필드를 대상으로 쿼리를 작성한다.
  • JPA가 JPQL을 각 데이터베이스 벤더에 맞게 SQL을 변환하기 때문에 데이터베이스에 독립적이다.
  • 복잡한 쿼리의 경우 성능이 떨어질 수 있다.
  • 엔티티 객체가 변경되면 쿼리도 자동으로 수정된다.
  • From 절에서 서브 쿼리 사용이 제한적이다.

Native SQL

  • SQL과 동일한 문법을 사용하고, DB 테이블에 직접 매핑한다. 실제 테이블과 컬럼 이름을 사용한다.
  • 데이터베이스에 종속적이기 때문에 DB가 변경되면 쿼리도 수정해야 한다.
  • DB의 고급 기능을 직접 사용할 수 있다. 
  • 직접 SQL을 작성하기 때문에 성능 최적화가 가능하다.
  • 복잡한 쿼리, 서브쿼리 또는 DB 고급 기능이 필요할 때 사용된다.

 

@Entity

Querydsl-sql은 JPQL을 사용하지 않고 DB 테이블에 직접 매핑한다는 것을 알게되었다.

위 정보로 Entity 클래스에 붙여두었던 @Entity 애너테이션에 대해 생각해보게 된다.

 

/**
 * Specifies that the class is an entity. This annotation is applied to the
 * entity class.
 * 
 * @since 1.0
 */
@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface Entity {

	/**
	 * (Optional) The entity name. Defaults to the unqualified
	 * name of the entity class. This name is used to refer to the
	 * entity in queries. The name must not be a reserved literal
	 * in the Jakarta Persistence query language.
	 */
	String name() default "";
}

JPQL에서는 기본적으로 Entity Class 이름을 사용한다. 하지만 일반적으로 엔티티 클래스 이름과 테이블 이름이 다를 경우 name 속성을 사용해 JPQL쿼리에서 사용할 Entity 이름을 변경할 수 있다.

 

위에서 말했듯, Querydsl-sql은 JPQL을 사용하지 않기 때문에 @Entity의 name 속성이 적용되지 않았고, 재정의되지 않은 Entity Class 이름을 그대로 사용한 것이다. 

 

해결

@Entity는 엔티티의 이름을 정할때 사용된다. 이는 HQL(Hibernate Query Language)에서 엔티티를 식별할 이름을 정한다.
@Table은 Database에 생성될 Table의 이름을 지정할때 사용한다.
@Table이 없고 @Entity만 존재하는 경우, @Entity의 name 속성에 의해, Entity와 Table 이름이 모두 결정된다.

하지만 Querydsl-sql은 Native SQL를 사용하기 때문에 @Entity name 속성이 적용되지 않고 클래스 이름을 테이블 이름으로 인식했기 때문에 DB에서 테이블을 찾을 수 없다는 에러가 발생한 것이다.

 

Querydsl-sql을 사용할 때, 엔티티 클래스 이름과 테이블 이름이 다른 경우 @Table을 사용하여 테이블 명을 일치시켜주어야 한다.

@Table name 속성을 이용해 엔티티와 매핑할 테이블 이름을 지정해주었고 이후 쿼리에서 해당 테이블을 잘 인식했다.

@Getter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@ToString(exclude = {"organization", "user"})
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "ORGANIZATION_LEAD")
@Table(name = "ORGANIZATION_LEAD")
public class OrganizationLeadEntity extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "organization_id")
    private OrganizationEntity organization;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "lead_id")
    private UsersEntity user;

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDate startDate;

    private LocalDate endDate;
}

 

참조

 

catsriding | Querydsl 단일 테이블 여러 번 조인하기

Querydsl을 사용하여 쿼리를 작성할 때, 간혹 하나의 엔티티를 여러 번 조인해야 하는 상황이 발생합니다. 데이터베이스 관점으로 보면 단일 테이블을 여러 번 조인하는 작업과 동일합니다. SQL 쿼

www.catsriding.com

https://www.baeldung.com/jpa-entity-table-names

 

[JPA] 엔티티 매핑 - @Entity, @Table

@Entity JPA에서 엔티티란 쉽게 생각하면, DB 테이블에 대응하는 하나의 클래스라고 생각할 수 있습니다. @Entity가 붙은 클래스는 JPA가 관리해주며, JPA를 사용해서 DB 테이블과 매핑할 클래스는 @Entity

ttl-blog.tistory.com

 

Querydsl - 레퍼런스 문서

본 절에서는 SQL 모듈의 쿼라 타입 생성과 쿼리 기능을 설명한다. com.mysema.query.sql.Configuration 클래스를 이용해서 설정하며, Configuration 클래스는 생성자 인자로 Querydsl SQL Dialect를 취한다. 예를 들어

querydsl.com

https://burningfalls.github.io/java/difference-between-jpql-and-native-sql-in-spring-data-jpa/

 

[Spring] Spring Data JPA에서 JPQL과 Native SQL의 차이는?

Spring Data JPA에서 JPQL과 Native SQL의 차이는?

burningfalls.github.io

 

728x90