Hibernate NativeQuery HHH-14778 issue (with Postgres)
Postgres 환경에서 Hibernate NativeQuery를 사용할 때 생기는 버그를 디버깅해본 나름의 결과(?)를 공유해봅니다.
버그 재현
우선, 다음과 같은 테이블이 존재한다고 가정해보자.
create table message (
id int8 generated by default as identity,
body varchar(255),
count int8,
primary key (id)
)
그리고 다음과 같은 hibernate 네이티브 쿼리를 실행하면 테스트는 실패합니다. 이 버그는 Postgres에서만 발생하는데요. 보다 자세한 내용을 아래서 설명하도록 하겠습니다.
class ApplicationTests {
@Autowired
private EntityManager entityManager;
@Test
void nativeQuery() {
assertThatThrownBy(() -> {
final Long id = 1L;
final Query query =
entityManager.createNativeQuery("UPDATE message SET count = :count WHERE id = :id")
.setParameter("count", null)
.setParameter("id", id);
query.executeUpdate();
})
.hasCauseInstanceOf(SQLGrammarException.class)
.hasRootCauseMessage(
"ERROR: column \"count\" is of type bigint but expression is of type bytea\n" +
" Hint: You will need to rewrite or cast the expression.\n" +
" Position: 28");
}
}
위 버그는 하이버네이트 이슈로 몇 달전에 리포팅되어 있으니 참고바랍니다. (참고: https://hibernate.atlassian.net/browse/HHH-14778)
위 에러는 query.executeUpdate()
시점에 발생합니다. 어디가 어떻게 문제일까요? 🤔
분석
ERROR: column "count" is of type bigint but expression is of type bytea
메시지를 보면 bytea
으로 bigint
를 표현할 수 없다(?)고 합니다. 결국 하이버네이트에서 count 파라미터를 bytea
으로 인식하여 쿼리를 실행한다고 볼 수 있습니다. (PrepareStatement
를 만들게 되는 것이지요.)
QueryParameterBindingsImpl
그렇게 하이버네이트를 분석해보면서 다음과 같은 코드를 찾았습니다.
쿼리를 분석하고 적절한 타입을 바인딩하기 위해 추론하고 적절한 바인더를 만들기 위해 bindType
을 정의하는데요. 결국 적절한 bindType
을 찾지 못할 경우, SerializableType.INSTANCE
로 초기화하게 됩니다. SerializableType.INSTANCE
은 아래와 같은 타입을 갖게됩니다.
- java type:
Serializable
- sql type:
Types.VARBINARY
(bytea)
VarbinaryTypeDescriptor
(BasicBinder
)
실제 쿼리에 파라미터들을 바인딩하기 위해 PrepareStatement
에 다음과 같이 셋팅을 합니다. 인자로 넘어가는 sqlDescriptor
가 VarbinaryTypeDescriptor
인 것을 알 수 있습니다.
PgPrepareStatement
PgPrepareStatement는 전달받은 sqlType
으로 케이스별로 파라미터에 반영합니다. 이러한 과정때문에 테스트가 깨진 것인데요.
public void setNull(int parameterIndex, int sqlType) throws SQLException {
int oid;
switch (sqlType) {
case Types.SQLXML:
oid = Oid.XML;
break;
case Types.INTEGER:
oid = Oid.INT4;
break;
// ...
그렇다면, mysql에서는 어떻게 처리하길래 문제가 되지 않을까요?
@Override
public void setNull(int parameterIndex, int sqlType) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
((PreparedQuery<?>) this.query).getQueryBindings().setNull(getCoreParameterIndex(parameterIndex)); // MySQL ignores sqlType
}
}
mysql의 ClientPreparedStatement를 살펴보면 다음과 같이 sqlType
을 무시하는 것을 알 수 있었습니다. 🤔
참고로
ServerPreparedStatement
도ClientPreparedStatement
를 상속받았기 때문에 동일한 동작을 합니다.
분석 정리
결국, 하이버네이트에서 각 드라이버의 호환성 문제로 인한 버그라고 볼 수 있을 것 같습니다. (개인적으로는 postgres가 더 정교한 것 같습니다만...) 그런데 한편으로는 HQL같은 엔티티로 쿼리를 작성하면 타입 추론이 가능하지만, 네이티브 쿼리는 어떻게 가능할까 라는 생각이 들기도 합니다.
이렇더라도 우리는 버그를 퇴치(?)를 해야하니 해결방법에 대해 알아보도록 하겠습니다.