실제 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)
'Programing > OpenSource' 카테고리의 다른 글
[slf4j] MDC에 put만 계속한다면 (0) | 2020.06.15 |
---|---|
[JPA] Hibernate + MariaDB : count(*)의 매핑이 BigInteger로 되는 이유는? (0) | 2020.06.12 |
[라이선스] Wunderlist 3.19.41 (0) | 2020.03.27 |
[Apache Lucene] Lucene의 의미는? (0) | 2020.02.18 |
[tomcat] HttpServletRequest.getHeader (0) | 2020.02.11 |