본문 바로가기

Programing/OpenSource

[JPA] Hibernate + MariaDB : count(*)의 매핑이 BigInteger로 되는 이유는?

회사의 수지님이 퇴근 전 물어보아 찾아보게 된 MaraiDB의 JDBC 드라이버.

EntityManager 를 통해 NativeQuery를 수행하는데 반환되는 타입이 Long으로 생각했는데 BigInteger 타입으로 반환되어 ClassCastException 가 발생했다고 한다.

 

간략하게 코드로 보면 아래와 같다.

@Service
public class CountService {

    private final EntityManager entityManager;

    @Autowired
    public CountService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Long countIssue() {
        Query query = entityManager.createNativeQuery("select count(*) from ISSUE");
        return query.getSingleResult();	// ClassCastException 발생
    }

Query는 JPA(Jakarta Persistence API)상 존재하는 인터페이스로 javax.persistence 패키지 하위에 있다.

아래의 시그너처 대로 Object를 반환한다. 경우에 따라 실제 인스턴스는 Long이 될 수도 BigInteger가 될 수도 있다.

package javax.persistence;

public interface Query {
    Object getSingleResult();

 

일단, Query 인터페이스에 대응하는 HIbernate 구현체는 아래와 같이 4가지가 존재한다. (버전에 따라 다를 수도 있다.)

Query라는 이름의 인터페이스가 두 개 보이는데
위의 것이 위에서 본 javax.persistence 패키지의 Query이고, 중간 쯤에 있는 것은 org.hibernate.query 패키지의 Query이다.

 

구현체들의 공통적인 코드는 AbstractProducedQuery가 구현하고 있다.

getSingleResult()의 내부구현은 쿼리의 수행결과를 List로 받아와서 크기가 0이라는 확인을 하고 객체를 돌려주는 식으로 되어 있다.

uniqueElements는 결국 크기가 0이면 null을 반환하고, 그 이외에는 처음 원소를 가져와서 나머지 원소들과 모두 동일한지 확인 후 돌려주는 식으로 구현이 되어 있다.

package org.hibernate.query.internal;

public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {

	private final SharedSessionContractImplementor producer;
    // ..
    
	@Override
	public R getSingleResult() {
		try {
			final List<R> list = list();
			if ( list.size() == 0 ) {
				throw new NoResultException( "No entity found for query" );
			}
			return uniqueElement( list );
		}
		catch ( HibernateException e ) {
			throw getExceptionConverter().convert( e, getLockOptions() );
		}
	}
    
	@Override
	public List<R> list() {
		beforeQuery();
		try {
			return doList();
		}
		// .. catch 들
		finally {
			afterQuery();
		}
	}
    
	protected List<R> doList() {
    // ..
    
	@Override
	public SharedSessionContractImplementor getProducer() {
		return producer;
	}

doList()가 protected임에 주의해야 한다. 다시말해 서브클래싱을 통해 오버라이딩이 가능하다.

네이티브 쿼리의 경우 NativeQueryImple이 사용되는데 아래와 같이 오버라이딩하고 있다.

package org.hibernate.query.internal;

public class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements NativeQueryImplementor<T> {
	@Override
	protected List<T> doList() {
		return getProducer().list(
				generateQuerySpecification(),
				getQueryParameters()
		);
	}

 

디버그를 해보면 List의 객체가 BigInteger임을 알 수 있었다.

ArrayList<BigInteger>가 반환되었다.

결과가 객체로 만들어지는 시점

그렇다면 언제 Object의 타입이 정해질까?

JDBC 수준의 코딩을 해보았으면 쿼리의 실행의 결과는 ResultSet 인터페이스를 받는 것을 알고 있을것이다.

Statement stmt = con.createStatement(
                               ResultSet.TYPE_SCROLL_INSENSITIVE,
                               ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM TABLE2");

하이버네이트 구현체도 내부적으로는 JDBC를 사용한다. 로우레벨에서는 ResultSet을 지지고 볶고 가공해서 값을 빼내는 것을 이 프레임워크는 지능적으로 해낸다.

JDBC 드라이버로 MariaDB를 사용하면 SELECT에 대한 ResultSet 구현체는 SelectResultSet이 사용된다.

DB와 네트워크를 통해 연결이 된다면 결국은 데이터는 패킷의 형태로 전송을 받는다.

이 패킷의 프로토콜에 대한 것은 구현체 마다 다른데 MariaDB의 경우 아래 문서에 잘 기술되어 있다.

또한 MariaDB에서 COUNT 쿼리에 대한 결과는 BIGINT라고 되어 있다.

하이버네이트 로더

하이버네이트는 Loader라는 추상클래스에서 객체 로딩 전략을 구현하고 있다.

Loader는 아래와 같이 계층을 가지고 있다.

QueryLoader의 계층구조

크게 CustomLoader 라인이 있고, BasicLoader 라인이 있는데

만약 createNativeQuery를 통해 네이티브쿼리를 생성하면 CustomLoader 인스턴스가 지정이 된다.

오른쪽의 BasicLoader 라인은 JPQL을 사용할 경우 사용된다.

Query에서 Loader까지의 경로는 네이티브쿼리와 JPQL이 비슷하면서 좀 다른 콜 스택을 가진다.

왼쪽이 native, 오른쪽이 HQL이다.

중간의 박스들을 건너띄고 공통의 Loader의 코드는 아래와 같다.

package org.hibernate.loader;

public abstract class Loader {
	protected List list(
			final SharedSessionContractImplementor session,
			final QueryParameters queryParameters,
			final Set<Serializable> querySpaces,
			final Type[] resultTypes) throws HibernateException {
		final boolean cacheable = factory.getSessionFactoryOptions().isQueryCacheEnabled() &&
				queryParameters.isCacheable();

		if ( cacheable ) {
			return listUsingQueryCache( session, queryParameters, querySpaces, resultTypes );
		}
		else {
			return listIgnoreQueryCache( session, queryParameters );
		}
	}
    // ..

	private Object getRowFromResultSet(
			final ResultSet resultSet,
			final SharedSessionContractImplementor session,
			final QueryParameters queryParameters,
			final LockMode[] lockModesArray,
			final EntityKey optionalObjectKey,
			final List hydratedObjects,
			final EntityKey[] keys,
			boolean returnProxies,
			ResultTransformer forcedResultTransformer) throws SQLException, HibernateException {
		// ..
		return forcedResultTransformer == null
				? getResultColumnOrRow( row, queryParameters.getResultTransformer(), resultSet, session )
				: forcedResultTransformer.transformTuple(
				getResultRow( row, resultSet, session ),
				getResultRowAliases()
		)
				;
	}

	protected Object getResultColumnOrRow(
			Object[] row,
			ResultTransformer transformer,
			ResultSet rs,
			SharedSessionContractImplementor session) throws SQLException, HibernateException {
		return row;
	}

	protected Object[] getResultRow(
			Object[] row,
			ResultSet rs,
			SharedSessionContractImplementor session) throws SQLException, HibernateException {
		return row;
	}

getResultColumnOrRow 호출에서 호출되는 부분이 달라진다.

CustomLoader

네이티브쿼리의 경우 CustomLoader에서 오버라이딩한 getResultColumnOrRow가 호출된다.

package org.hibernate.loader.custom;

public class CustomLoader extends Loader {

	private final ResultRowProcessor rowProcessor;

	@Override
	protected Object getResultColumnOrRow(
			Object[] row,
			ResultTransformer transformer,
			ResultSet rs,
			SharedSessionContractImplementor session) throws SQLException, HibernateException {
		return rowProcessor.buildResultRow( row, rs, transformer != null, session );
	}
    
	@Override
	protected void autoDiscoverTypes(ResultSet rs) {
		try {
			JdbcResultMetadata metadata = new JdbcResultMetadata( getFactory(), rs );
			rowProcessor.prepareForAutoDiscovery( metadata );

			List<String> aliases = new ArrayList<>();
			List<Type> types = new ArrayList<>();
			for ( ResultColumnProcessor resultProcessor : rowProcessor.getColumnProcessors() ) {
				resultProcessor.performDiscovery( metadata, types, aliases );
			}

			validateAliases( aliases );

			resultTypes = ArrayHelper.toTypeArray( types );
			transformerAliases = ArrayHelper.toStringArray( aliases );
		}
		catch (SQLException e) {
			throw new HibernateException( "Exception while trying to autodiscover types.", e );
		}
	}

결국 rowProcessor의 buildResultRow에 의해 객체데이터 처리가 이루어진다.

package org.hibernate.loader.custom;

public class ResultRowProcessor {
	// ...
	private ResultColumnProcessor[] columnProcessors;

	public Object buildResultRow(Object[] data, ResultSet resultSet, boolean hasTransformer, SharedSessionContractImplementor session)
			throws SQLException, HibernateException {
		final Object[] resultRow = buildResultRow( data, resultSet, session );
        // ..

	public Object[] buildResultRow(Object[] data, ResultSet resultSet, SharedSessionContractImplementor session)
			throws SQLException, HibernateException {
		Object[] resultRow;
		if ( !hasScalars ) {
			resultRow = data;
		}
		else {
			resultRow = new Object[ columnProcessors.length ];
			for ( int i = 0; i < columnProcessors.length; i++ ) {
				resultRow[i] = columnProcessors[i].extract( data, resultSet, session );
			}
		}

		return resultRow;
	}

결과 row 처리기는 ResultColumnProcessor 를 통해 extract를 수행한다.

COUNT의 경우 ScalarResultColumnProcessor 가 사용된다.

package org.hibernate.loader.custom;

public class ScalarResultColumnProcessor implements ResultColumnProcessor {
	private int position = -1;
	private String alias; // COUNT(*)
	private Type type; // COUNT(*) 함수의 경우 BigIntegerType 으로 설정이 된다.
    
    // ..
	@Override
	public void performDiscovery(JdbcResultMetadata metadata, List<Type> types, List<String> aliases) throws SQLException {
		if ( alias == null ) {
			alias = metadata.getColumnName( position );
		}
		else if ( position < 0 ) {
			position = metadata.resolveColumnPosition( alias );
		}
		if ( type == null ) {
			type = metadata.getHibernateType( position );
		}
		types.add( type );
		aliases.add( alias );
	}
    
	@Override
	public Object extract(Object[] data, ResultSet resultSet, SharedSessionContractImplementor session)
			throws SQLException, HibernateException {
		return type.nullSafeGet( resultSet, alias, session, null );
	}
}

ScalarResultColumnProcessor는 Loader의 processResultSet 에서 호출이 된다.

위의 CustomLoader에서 autoDiscoverTypes 메서드를 오버라이딩을 한 코드가 있었는데 그 메서드가 호출된다.

package org.hibernate.loader;

public abstract class Loader {
	// ..
	private ResultSet processResultSet(
			ResultSet rs,
			final RowSelection selection,
			final LimitHandler limitHandler,
			final boolean autodiscovertypes,
			final SharedSessionContractImplementor session
	) throws SQLException, HibernateException {
		rs = wrapResultSetIfEnabled( rs, session );

		if ( !limitHandler.supportsLimitOffset() || !LimitHelper.useLimit( limitHandler, selection ) ) {
			advance( rs, selection );
		}

		if ( autodiscovertypes ) {
			autoDiscoverTypes( rs );
		}
		return rs;
	}
    
	protected void autoDiscoverTypes(ResultSet rs) {
		throw new AssertionFailure( "Auto discover types not supported in this loader" );
	}

Loader (processResultSet) → CustomLoader (autoDiscoverTypes) → ScalarResultColumnProcessor (performDiscovery) → JdbcResultMetaData (autoDiscoverTypes)

performDiscovery 에 의해 alias와 type이 설정이 된다.

package org.hibernate.loader.custom;

class JdbcResultMetadata {
	// ...
	public Type getHibernateType(int columnPos) throws SQLException {
		int columnType = resultSetMetaData.getColumnType( columnPos ); // columnType: -5
		int scale = resultSetMetaData.getScale( columnPos ); // scale: 0
		int precision = resultSetMetaData.getPrecision( columnPos ); // precision: 21

		// ... 아래는 매핑 전략중 하나
		else {
			hibernateTypeName = factory.getDialect().getHibernateTypeName(
					columnType,
					length,
					precision,
					scale
			);
		}

		return factory.getTypeResolver().heuristicType(
				hibernateTypeName
		);
	}
}

getHibernateType 메서드의 첫번째 코드가 중요하다.

결국은 java.sql.ResultSetMetaData 의 getColumnType을 이용해서 컬럼타입을 얻는 것이었다.

 

이후 코드는 JDBC의 컬럼타입 코드를 하이버네이트 타입으로 변환을 하는 것이라 생략한다.

코드상 하이버네이트 타입을 먼저 검색하고 지원하는 것이 없을 경우 dialect 기반의 매핑을 사용한다.

package org.hibernate.dialect;

public final class TypeNames {

	private final Map<Integer, String> defaults = new HashMap<Integer, String>();
    
	public String get(int typeCode, long size, int precision, int scale) throws MappingException {
		final Integer integer = Integer.valueOf( typeCode );
		final Map<Long, String> map = weighted.get( integer );
		if ( map != null && map.size() > 0 ) {
			// iterate entries ordered by capacity to find first fit
			for ( Map.Entry<Long, String> entry: map.entrySet() ) {
				if ( size <= entry.getKey() ) {
					return replace( entry.getValue(), size, precision, scale );
				}
			}
		}

		// if we get here one of 2 things happened:
		//		1) There was no weighted registration for that typeCode
		//		2) There was no weighting whose max capacity was big enough to contain size
		return replace( get( typeCode ), size, precision, scale );

TypeNames 에는 defaults라는 Map이 있는데 애플리케이션이 초기화 될 때 같이 값들도 초기화 된다.

JDBC 타입 -5는 하이버네이트타입 big_integer 에 대응한다.

이 작업은 Dialect라는 이름의 추상클래스에서 수행한다.

package org.hibernate.dialect;

public abstract class Dialect implements ConversionContext {

	private final TypeNames typeNames = new TypeNames();
    
	protected void registerColumnType(int code, String name) {
		typeNames.put( code, name );
	}

	protected Dialect() {
		// ..
		// register hibernate types for default use in scalar sqlquery type auto detection
		registerHibernateType( Types.BIGINT, StandardBasicTypes.BIG_INTEGER.getName() );
		registerHibernateType( Types.BINARY, StandardBasicTypes.BINARY.getName() );
		registerHibernateType( Types.BIT, StandardBasicTypes.BOOLEAN.getName() );
		registerHibernateType( Types.BOOLEAN, StandardBasicTypes.BOOLEAN.getName() );
		// ..

Java SQL의 타입중 BIGINT는 아래와 같다.

package java.sql;

public class Types {
	// ..
	public final static int BIGINT = -5;

StandardBasicTypes은 아래와 같다.

package org.hibernate.type;

public final class StandardBasicTypes {
	// ..
	public static final BigIntegerType BIG_INTEGER = BigIntegerType.INSTANCE;

위에서 -5에 매핑되었던 big_interger라는 것은 BigIntegerType의 getName에 대응한다.

package org.hibernate.type;

public class BigIntegerType
		extends AbstractSingleColumnStandardBasicType<BigInteger>
		implements DiscriminatorType<BigInteger> {

	public static final BigIntegerType INSTANCE = new BigIntegerType();

	public BigIntegerType() {
		super( NumericTypeDescriptor.INSTANCE, BigIntegerTypeDescriptor.INSTANCE );
	}

	@Override
	public String getName() {
		return "big_integer";
	}
    // ..

결론을 내보자면 -5라는 JDBC 값은 DBMS가 돌려준 것이고, big_integer로 매핑되는 것은 Dialect의 매핑이 그렇게 되어 있는 것이다.

왜 Long이라고 판단을 했을까?

나중에 질문한 수지님이 이야기 해주었는데, JPQL에서 COUNT 집합 함수가 반환타입이 Long으로 되어 있던 것과 혼동을 했다고 한다.

JPQL에서는 NativeQuery를 만들때와 달리 createQuery 메서드를 사용한다.

또한 테이블이름도 엔티티의 이름을 지정을 해야 에러가 안난다. (QuerySyntaxException 를 담은 IllegalArgumentException가 발생)

@Service
public class CountService {

    private final EntityManager entityManager;

    @Autowired
    public CountService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Long countIssue() {
        Query query = entityManager.createQuery("select count(i) from IssueEntity i");
        return query.getSingleResult();	// ClassCastException 발생하지 않는다.
    }
//
@Entity
public class IssueEntity {
	// ..

동일한 Query 인터페이스의 getSingleResult() 가 호출되고 있기에 위에서 언급한 AbsAbstractProducedQuery의 getSingleResult() 가 호출된다.

package org.hibernate.query.internal;

public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
	@Override
	public R getSingleResult() {
		try {
			final List<R> list = list();
			if ( list.size() == 0 ) {
				throw new NoResultException( "No entity found for query" );
			}
			return uniqueElement( list );
		}
		catch ( HibernateException e ) {
			throw getExceptionConverter().convert( e, getLockOptions() );
		}
	}

다만 다른점이 있다면 타입 R이 BigInteger가 아닌 Long이라는 사실이다.

ArrayList<Long>이 반환되었다.

그럼 어디서 Long으로 바꾸는 것일까?

JPQL에서는 쿼리를 동적으로 분석하여 AST를 만든다. ANTLR라이브러리를 내부적으로 사용한다.

이중에 쿼리 반환 타입의 핵심은 CountNode 이다.

package org.hibernate.hql.internal.ast.tree;

public class CountNode extends AbstractSelectExpression implements SelectExpression {
	@Override
	public Type getDataType() {
		return getSessionFactoryHelper().findFunctionReturnType( getText(), null );
	}
	@Override
	public void setScalarColumnText(int i) throws SemanticException {
		ColumnHelper.generateSingleScalarColumn( this, i );
	}

}

getDataType이라는 타입 반환에서 LongType을 반환을 하게 된다.

참고로 이 타입은 하이버네이트 내부에서 사용되는 타입이고 org.hibernate.type 패키지에 100개에 가깝게 있다.

수 많은 타입들 계층 구조

이 타입은 리스트의 반환 타입에 직접적으로 사용이 된다.

EntityManager의 createQuery 호출되면 쿼리가 컴파일 되어 Query 인스턴스로 생성이 된다. SELECT 형태의 쿼리라면 SelectClause 가 초기화되고 해당 쿼리의 반환 타입도 정해진다.

package org.hibernate.hql.internal.ast.tree;

public class SelectClause extends SelectExpressionList {
	public void initializeExplicitSelectClause(FromClause fromClause) throws SemanticException {
		// ...
		for ( SelectExpression selectExpression : selectExpressions ) {
			if ( AggregatedSelectExpression.class.isInstance( selectExpression ) ) {
				aggregatedSelectExpression = (AggregatedSelectExpression) selectExpression;
				queryReturnTypeList.addAll( aggregatedSelectExpression.getAggregatedSelectionTypeList() );
				scalarSelect = true;
			}
			else {
				// ..
				Type type = selectExpression.getDataType();  // 여기!
				// ..
				queryReturnTypeList.add( type );  // 그리고 여기!
			}
		}

컴파일되는 call trace를 보면 꽤 복잡하다.

동적으로 매번 컴파일을 한다면 비효율적이다. 왜냐하면 파라미터는 바뀌지만 쿼리 자체는 런타임중에 바뀌지 않기 때문이다.

위의 호출중에 QueryPlanCache라고 있는데 쿼리플랜에 대한 캐싱을 수행해서 처음에 1회에 한해 컴파일이 되고 이후에는 getHQLQueryPlan 메서드를 통해 캐시된 인스턴스를 반환한다. (BoundedConcurrentHashMap 에 저장이 된다.)

획득된 이 HQLQueryPlan 인스턴스는 돌고돌아 Session 구현체인 SessionImpl의 list가 호출이 된다.

package org.hibernate.internal;

public class SessionImpl
		extends AbstractSessionImpl
		implements EventSource, SessionImplementor, HibernateEntityManagerImplementor {

	@Override
	public List list(String query, QueryParameters queryParameters) throws HibernateException {
		checkOpenOrWaitingForAutoClose();
		pulseTransactionCoordinator();
		queryParameters.validateParameters();

		HQLQueryPlan plan = queryParameters.getQueryPlan();
		if ( plan == null ) {
			plan = getQueryPlan( query, false );
		}

		autoFlushIfRequired( plan.getQuerySpaces() );

		final List results;
		boolean success = false;

		dontFlushFromFind++;
		try {
			results = plan.performList( queryParameters, this );  // 여기!
			success = true;
		}
		finally {
			dontFlushFromFind--;
			afterOperation( success );
			delayedAfterCompletion();
		}
		return results;
	}

그러면 내부에서 HQLQueryPlan은 performList가 호출된다.

package org.hibernate.engine.query.spi;

public class HQLQueryPlan implements Serializable {

	private final QueryTranslator[] translators;
    // ..
    
	public List performList(
			QueryParameters queryParameters,
			SharedSessionContractImplementor session) throws HibernateException {
		// ..

		if ( translators.length == 1 ) {
			return translators[0].list( session, queryParametersToUse );
		}
		// ..
	}

QueryTranslator 인터페이스의 구현체인 QueryTranslatorImpl의 list가 호출이 된다.

package org.hibernate.hql.internal.ast;

public class QueryTranslatorImpl implements FilterTranslator {

	private QueryLoader queryLoader;
    // ..

	@Override
	public List list(SharedSessionContractImplementor session, QueryParameters queryParameters)
			throws HibernateException {
		// ..
		List results = queryLoader.list( session, queryParametersToUse );

결국은 QueryLoader의 list의 호출이다.

package org.hibernate.loader.hql;

public class QueryLoader extends BasicLoader {

	private Type[] queryReturnTypes; // 위에서 보았지만 queryReturnTypes[0]은 LongType이다.

	public List list(
			SharedSessionContractImplementor session,
			QueryParameters queryParameters) throws HibernateException {
		checkQuery( queryParameters );
		return list( session, queryParameters, queryTranslator.getQuerySpaces(), queryReturnTypes );
	}

결국은 다시 추상클래스 Loader로 돌아오게 된다.

 이번에는 오른쪽이다.

결국은 돌고 돌아 위에서 이미 보았던 Loader의 getRowFromResultSet으로 돌아간다.

네이티브쿼리의 경우 CustomLoader에서 오버라이딩한 getResultColumnOrRow가 호출되었지만,

하이버네이트쿼리의 경우 QueryLoader에서 오버라이딩한 getResultColumnOrRow가 호출된다.

package org.hibernate.loader.hql;

public class QueryLoader extends BasicLoader {
	// ...
    private Type[] queryReturnTypes;
    
	public QueryLoader(
			final QueryTranslatorImpl queryTranslator,
			final SessionFactoryImplementor factory,
			final SelectClause selectClause) {
		super( factory );
		this.queryTranslator = queryTranslator;
		initialize( selectClause );
		postInstantiate();
	}

	private void initialize(SelectClause selectClause) {
		// ..
		queryReturnTypes = selectClause.getQueryReturnTypes();
	// ..

	@Override
	protected Object getResultColumnOrRow(
			Object[] row,
			ResultTransformer transformer,
			ResultSet rs,
			SharedSessionContractImplementor session)
			throws SQLException, HibernateException {

		Object[] resultRow = getResultRow( row, rs, session );
		boolean hasTransform = hasSelectNew() || transformer != null;
		return ( !hasTransform && resultRow.length == 1 ?
				resultRow[0] :
				resultRow
		);
	}

	@Override
	protected Object[] getResultRow(Object[] row, ResultSet rs, SharedSessionContractImplementor session)
			throws SQLException, HibernateException {
		Object[] resultRow;
		if ( hasScalars ) {
			String[][] scalarColumns = scalarColumnNames;
			int queryCols = queryReturnTypes.length;
			resultRow = new Object[queryCols];
			for ( int i = 0; i < queryCols; i++ ) {
				resultRow[i] = queryReturnTypes[i].nullSafeGet( rs, scalarColumns[i], session, null );
			}
		}
		else {
			resultRow = toResultRow( row );
		}
		return resultRow;
	}

CustomLoader의 경우 rowProcessor.buildResultRow를 이용해서 결과를 반환을 했지만

QueryLoader의 경우 getResultRow까지 오버라이딩을 하였다.

 

FastSessionServices의 remapSqlTypeDescriptor 호출시 dialect.remapSqlTypeDescriptor 에서 돌려주는 SqlTypeDescriptor 인터페이스는 BigIntTypeDescriptor이다.

package org.hibernate.type.descriptor.sql;

public class BigIntTypeDescriptor implements SqlTypeDescriptor {
	@Override
	public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
		return new BasicExtractor<X>( javaTypeDescriptor, this ) {
			@Override
			protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
				return javaTypeDescriptor.wrap( rs.getLong( name ), options );
			}
			// ..

BigIntTypeDescriptor가 결정되는 것은 위에서 이미 보았지만 SelectClause가 초기화 될 때이다.