개요
프로젝트를 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;
}
참조
https://www.baeldung.com/jpa-entity-table-names
https://burningfalls.github.io/java/difference-between-jpql-and-native-sql-in-spring-data-jpa/