본문 바로가기

Programing/OpenSource

[Hibernate] JPA 스키마 검증은 어떻게 수행될까?

실제 DB와 엔티티가 일치하는지 애플리케이션이 동작할 때 검증을 하도록 설정이 되어 있다.

만약 일치하지 않는 경우가 발생하면 예외를 던지며 애플리케이션을 멈추어 조기에 문제가 있음을 알 수 있게 한다.

 

DB에 무엇인가 쿼리를 날려서 일치여부를 확인할 것인데 쿼리도 보이지 않아서 파게되었다.

일부러 예외를 발생시키고 예외의 stacktrace를 찾다보니

SpringHibernateJpaPersistenceProvider 에서 createContainerEntityManagerFactory 에서  시작할 수 있었다.

package org.springframework.orm.jpa.vendor;

class SpringHibernateJpaPersistenceProvider extends HibernatePersistenceProvider {

	@Override
	@SuppressWarnings("rawtypes")
	public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
		final List<String> mergedClassesAndPackages = new ArrayList<>(info.getManagedClassNames());
		if (info instanceof SmartPersistenceUnitInfo) {
			mergedClassesAndPackages.addAll(((SmartPersistenceUnitInfo) info).getManagedPackages());
		}
		return new EntityManagerFactoryBuilderImpl(
				new PersistenceUnitInfoDescriptor(info) {
					@Override
					public List<String> getManagedClassNames() {
						return mergedClassesAndPackages;
					}
				}, properties).build();
	}

}

mergedClassesAndPackages 안에는 @Entity로 정의한 클래스의 full name이 들어간다.

package org.hibernate.jpa.boot.internal;

public class EntityManagerFactoryBuilderImpl implements EntityManagerFactoryBuilder {
	// ...
	@Override
	public EntityManagerFactory build() {
		final SessionFactoryBuilder sfBuilder = metadata().getSessionFactoryBuilder();
		populateSfBuilder( sfBuilder, standardServiceRegistry );

		try {
			return sfBuilder.build();
		}
		catch (Exception e) {
			throw persistenceException( "Unable to build Hibernate SessionFactory", e );
		}
	}

 

sfBulder.build()가 호출되면 구현체인 SessionFactoryBuilderImpl 의 메서드가 호출된다.

package org.hibernate.boot.internal;

public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplementor {
	// ..
	@Override
	public SessionFactory build() {
		metadata.validate();
		final StandardServiceRegistry serviceRegistry = metadata.getMetadataBuildingOptions().getServiceRegistry();
		BytecodeProvider bytecodeProvider = serviceRegistry.getService( BytecodeProvider.class );
		addSessionFactoryObservers( new SessionFactoryObserverForBytecodeEnhancer( bytecodeProvider ) );
		return new SessionFactoryImpl( metadata, buildSessionFactoryOptions() );
	}

처음에는 metadata.validate()에서 검증이되는 줄 알았는데 문제 없이 진행되었다.

 

결국 하이버네이트 영역까지 들어간다.

package org.hibernate.internal;

public class SessionFactoryImpl implements SessionFactoryImplementor {
	// ..
	public SessionFactoryImpl(final MetadataImplementor metadata, SessionFactoryOptions options) {
	    // ..
			SchemaManagementToolCoordinator.process(
					metadata,
					serviceRegistry,
					properties,
					action -> SessionFactoryImpl.this.delayedDropAction = action
			);

SchemaManagementToolCoordinator 클래스의 process 정적 메서드에서 hbm2ddl.auto. 등이 수행된다.

아래와 같이 ddl-auto에 create-drop 을 하면 애플리케이션이 수행될 때 엔티티를 참고해서 테이블을 만들고, 종료시 drop을 한다.

spring:
  jpa:
    hibernate:
      ddl-auto: create-drop

이 유형은 org.hibernate.tool.schema.Action 에 정의되어 있다.

이중 VALIDATE 가 검증을 수행하게 하는 명령이다.

public class SchemaManagementToolCoordinator {
	private static final Logger log = Logger.getLogger( SchemaManagementToolCoordinator.class );

	public static void process(
			final Metadata metadata,
			final ServiceRegistry serviceRegistry,
			final Map configurationValues,
			DelayedDropRegistry delayedDropRegistry) {
		// ..
		performScriptAction( actions.getScriptAction(), metadata, tool, serviceRegistry, executionOptions );
		performDatabaseAction( actions.getDatabaseAction(), metadata, tool, serviceRegistry, executionOptions );
		// ..
	}

	@SuppressWarnings("unchecked")
	private static void performDatabaseAction(
			final Action action,
			Metadata metadata,
			SchemaManagementTool tool,
			ServiceRegistry serviceRegistry,
			final ExecutionOptions executionOptions) {

		// IMPL NOTE : JPA binds source and target info..

		switch ( action ) {
			case CREATE_ONLY: {
			// ...
			case VALIDATE: {
				tool.getSchemaValidator( executionOptions.getConfigurationValues() ).doValidation(
						metadata,
						executionOptions
				);
				break;
			}
		}
	}

SchemaValidator 인터페이스의 doValidation() 가 호출되면

AbstractSchemaValidator 추상클래스의 해당 메서드가 호출된다.

public abstract class AbstractSchemaValidator implements SchemaValidator {
	// ..

	@Override
	public void doValidation(Metadata metadata, ExecutionOptions options) {
		final JdbcContext jdbcContext = tool.resolveJdbcContext( options.getConfigurationValues() );

		final DdlTransactionIsolator isolator = tool.getDdlTransactionIsolator( jdbcContext );

		final DatabaseInformation databaseInformation = Helper.buildDatabaseInformation(
				tool.getServiceRegistry(),
				isolator,
				metadata.getDatabase().getDefaultNamespace().getName()
		);

		try {
			performValidation( metadata, databaseInformation, options, jdbcContext.getDialect() );
		}
		finally {
			try {
				databaseInformation.cleanup();
			}
			catch (Exception e) {
				log.debug( "Problem releasing DatabaseInformation : " + e.getMessage() );
			}

			isolator.release();
		}
	}

	public void performValidation(
			Metadata metadata,
			DatabaseInformation databaseInformation,
			ExecutionOptions options,
			Dialect dialect) {
		for ( Namespace namespace : metadata.getDatabase().getNamespaces() ) {
			if ( schemaFilter.includeNamespace( namespace ) ) {
				validateTables( metadata, databaseInformation, options, dialect, namespace );
			}
		}

		for ( Namespace namespace : metadata.getDatabase().getNamespaces() ) {
			if ( schemaFilter.includeNamespace( namespace ) ) {
				for ( Sequence sequence : namespace.getSequences() ) {
					if ( schemaFilter.includeSequence( sequence ) ) {
						final SequenceInformation sequenceInformation = databaseInformation.getSequenceInformation(
								sequence.getName()
						);
						validateSequence( sequence, sequenceInformation );
					}
				}
			}
		}
	}

performValidation 에서는 테이블 검증, 컬럼 검증, 시퀀스 검증 등의 순서로 확인을 한다.

 

AbstractSchemaValidator 추상클래스의 구현체는 IndividuallySchemaValidatorImpl와 GroupedSchemaValidatorImpl가 있다.

package org.hibernate.tool.schema.internal;

public class GroupedSchemaValidatorImpl extends AbstractSchemaValidator {
	// ..
	@Override
	protected void validateTables(
			Metadata metadata,
			DatabaseInformation databaseInformation,
			ExecutionOptions options,
			Dialect dialect, Namespace namespace) {

		final NameSpaceTablesInformation tables = databaseInformation.getTablesInformation( namespace );
		for ( Table table : namespace.getTables() ) {
			if ( schemaFilter.includeTable( table ) && table.isPhysicalTable() ) {
				validateTable(
						table,
						tables.getTableInformation( table ),
						metadata,
						options,
						dialect
				);
			}
		}
	}
}

 

--- JDBC 레벨..

테이블 정보를 가져오는 쿼리(Vendor에 따라 다를 수 있다)

public class MariaDbDatabaseMetaData implements DatabaseMetaData {
	// ..
  public ResultSet getTables(
      String catalog, String schemaPattern, String tableNamePattern, String[] types)
      throws SQLException {

    StringBuilder sql =
        new StringBuilder(
            "SELECT TABLE_SCHEMA TABLE_CAT, NULL  TABLE_SCHEM,  TABLE_NAME,"
                + " IF(TABLE_TYPE='BASE TABLE', 'TABLE', TABLE_TYPE) as TABLE_TYPE,"
                + " TABLE_COMMENT REMARKS, NULL TYPE_CAT, NULL TYPE_SCHEM, NULL TYPE_NAME, NULL SELF_REFERENCING_COL_NAME, "
                + " NULL REF_GENERATION"
                + " FROM INFORMATION_SCHEMA.TABLES "
                + " WHERE "
                + catalogCond("TABLE_SCHEMA", catalog)
                + " AND "
                + patternCond("TABLE_NAME", tableNamePattern));

    if (types != null && types.length > 0) {
      sql.append(" AND TABLE_TYPE IN (");
      for (int i = 0; i < types.length; i++) {
        if (types[i] == null) {
          continue;
        }
        String type = escapeQuote(mapTableTypes(types[i]));
        if (i == types.length - 1) {
          sql.append(type).append(")");
        } else {
          sql.append(type).append(",");
        }
      }
    }

    sql.append(" ORDER BY TABLE_TYPE, TABLE_SCHEMA, TABLE_NAME");

    return executeQuery(sql.toString());
  }
  
  public ResultSet getColumns(
      String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
      throws SQLException
  // ..
  
  private ResultSet executeQuery(String sql) throws SQLException {
    Statement stmt =
        new MariaDbStatement(connection, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
    SelectResultSet rs = (SelectResultSet) stmt.executeQuery(sql);
    rs.setStatement(null); // bypass Hibernate statement tracking (CONJ-49)
    rs.setForceTableAlias();
    return rs;
  }

org.mariadb.jdbc.MariaDbDatabaseMetaData#executeQuery 가 두 번 호출되는데 한번은 테이블 정보, 다음은 컬럼 정보을 얻을 때 사용된다. (여기에 break point를 걸어보면 쿼리를 확인할 수 있다.)

 

--- 다시 Hibernate

테이블 정보는 TableInformation 인터페이스에 있다.

 

테이블 명이 있는지 확인하고, 각 컬럼이 제대로 이름과 타입이 맞는지 확인한다.

public class TableInformationImpl implements TableInformation {
	private final InformationExtractor extractor;
	private final IdentifierHelper identifierHelper;

	private final QualifiedTableName tableName;
	private final boolean physicalTable;
	private final String comment;

	private PrimaryKeyInformation primaryKey;
	private Map<Identifier, ForeignKeyInformation> foreignKeys;
	private Map<Identifier, IndexInformation> indexes;
	private Map<Identifier, ColumnInformation> columns = new HashMap<>(  );

	// ..
	@Override
	public ColumnInformation getColumn(Identifier columnIdentifier) {
		return columns.get( new Identifier(
				identifierHelper.toMetaDataObjectName( columnIdentifier ),
				false
		) );
	}

Call Stacks

GroupedSchemaValidatorImpl#validateTables <- 여기가 아래에서 파란색 부분이다.

DatabaseInformation#getTablesInformation(Namespace namespace)를 수행하여 테이블 정보를 획득하고

 내부 구현은 JDBC 드라이버에 따라 동작이 나뉘어진다.

ExtractionContext#getJdbcDatabaseMetaData() -> DatabaseMetaData#getColumns(String catalog, String schemaPattern,
                         String tableNamePattern, String columnNamePattern)