Backend/Spring
Spring JPA의 @Table 어노테이션에 대해서 알아보자 - name을 어떻게 매핑하는가?
Seyun(Marco)
2023. 11. 29. 23:26
728x90
Spring JPA의 @Table 어노테이션에 대해서 알아보자 - name을 어떻게 매핑하는가?
서론
- Entity와 Table을 매핑하기 위해 사용하는 어노테이션은 @Table 어노테이션 입니다.
- 실제 @Table 어노테이션이 어떤 역할을 해주는것인지, 또한 각 속성값을 어떻게 동작시키게 되는지에 대해서 알아볼 예정입니다.
- 코드를 살펴보다가, 대체 @Table 의 name은 어떻게 매핑이 되는건지?에 대한 궁금증이 생겨서 그걸 중점적으로 이야기를 나눠보도록 하겠습니다.
어노테이션 코드 살펴보기
package jakarta.persistence;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specifies the primary table for the annotated entity. Additional
* tables may be specified using {@link SecondaryTable} or {@link
* SecondaryTables} annotation.
*
* <p> If no <code>Table</code> annotation is specified for an entity
* class, the default values apply.
*
* <pre>
* Example:
*
* @Entity
* @Table(name="CUST", schema="RECORDS")
* public class Customer { ... }
* </pre>
*
* @since 1.0
*/
@Target(TYPE)
@Retention(RUNTIME)
public @interface Table {
/**
* (Optional) The name of the table.
* <p> Defaults to the entity name.
*/
String name() default "";
/** (Optional) The catalog of the table.
* <p> Defaults to the default catalog.
*/
String catalog() default "";
/** (Optional) The schema of the table.
* <p> Defaults to the default schema for user.
*/
String schema() default "";
/**
* (Optional) Unique constraints that are to be placed on
* the table. These are only used if table generation is in
* effect. These constraints apply in addition to any constraints
* specified by the <code>Column</code> and <code>JoinColumn</code>
* annotations and constraints entailed by primary key mappings.
* <p> Defaults to no additional constraints.
*/
UniqueConstraint[] uniqueConstraints() default {};
/**
* (Optional) Indexes for the table. These are only used if
* table generation is in effect. Note that it is not necessary
* to specify an index for a primary key, as the primary key
* index will be created automatically.
*
* @since 2.1
*/
Index[] indexes() default {};
}
- 위에 주석에서 Specifies the primary table for the annotated entity. Additional tables may be specified using SecondaryTable or SecondaryTables annotation. 해당 부분을 번역해보면 아래와 같습니다.
- 어노테이션이 달린 엔티티의 기본 테이블을 지정합니다. 보조 테이블 또는 보조 테이블 어노테이션을 사용하여 추가 테이블을 지정할 수 있습니다.
- 그렇다면 실제 속성들을 살펴보면 아래와 같습니다.
- name: 테이블의 이름이며, 기본값은 Entity의 이름입니다.
- catalog: 테이블의 catalog의 이름이며, default catalog를 디폴트로 사용합니다.
- DB의 catalog란?
- DB의 메타데이터로 데이터에 대한 데이터를 포함한 정보를 구조화하여 저장한 것을 의미합니다.
- 테이블, 인덱스, 뷰, 도메인 등 데이터베이스의 구조와 관련된 객체의 이름, 데이터 유형 및 사이즈, 연관되어 있는지 정보를 제공하게 됩니다.
- DB의 catalog란?
- schema: 테이블의 스키마이며, 사용자의 스키마를 사용하빈다.
- uniqueConstraints: 테이블에 설정할 고유 제약 조건입니다. 테이블 생성이 유효한 경우에만 사용됩니다. 이러한 제약 조건은 Column 및 JoinColumn 주석에 지정된 모든 제약 조건과 기본 키 매핑에 수반되는 제약 조건에 추가로 적용됩니다. 기본값은 추가 제약 조건이 없는 것입니다.
- indexes : 테이블의 인덱스입니다. 테이블 생성이 유효한 경우에만 사용됩니다. 기본 키 인덱스는 자동으로 생성되므로 기본 키에 대한 인덱스를 지정할 필요가 없습니다.
- cf) catalog와 schema의 차이
- Postgres 기준으로 클러스터 > 카탈로그 > 스키마 > 테이블 > 열 및 행 이러한 계층 구조가 있습니다.
- 아래에서 그림을 토대로 알수 있습니다.
그렇다면 name은 어떻게 매핑하는가?
- 원래 주제로 넘어오면, name을 어떻게 매핑하는가?에 대한 코드를 살펴봐야 할거 같습니다.
- 코드를 살펴본 결과 하이버네이트에 EntityBinder#handleClassTable 에 대해서 봐야 할거 같습니다.
private void handleClassTable(InheritanceState inheritanceState, PersistentClass superEntity) {
final String schema;
final String table;
final String catalog;
final UniqueConstraint[] uniqueConstraints;
boolean hasTableAnnotation = annotatedClass.isAnnotationPresent( jakarta.persistence.Table.class );
if ( hasTableAnnotation ) {
final jakarta.persistence.Table tableAnnotation = annotatedClass.getAnnotation( jakarta.persistence.Table.class );
table = tableAnnotation.name();
schema = tableAnnotation.schema();
catalog = tableAnnotation.catalog();
uniqueConstraints = tableAnnotation.uniqueConstraints();
}
else {
//might be no @Table annotation on the annotated class
schema = "";
table = "";
catalog = "";
uniqueConstraints = new UniqueConstraint[0];
}
final InFlightMetadataCollector collector = context.getMetadataCollector();
if ( inheritanceState.hasTable() ) {
createTable( inheritanceState, superEntity, schema, table, catalog, uniqueConstraints, collector );
}
else {
if ( hasTableAnnotation ) {
//TODO: why is this not an error?!
LOG.invalidTableAnnotation( annotatedClass.getName() );
}
if ( inheritanceState.getType() == InheritanceType.SINGLE_TABLE ) {
// we at least need to properly set up the EntityTableXref
bindTableForDiscriminatedSubclass( collector.getEntityTableXref( superEntity.getEntityName() ) );
}
}
}
- @Table 의 어노테이션이 붙어 있는지를 체크하고, 붙어 있지 않다면 default값을 사용하며, 있다면 해당 값들을 사용하게 됩니다.
- 이후에 InFlightMetadataCollector를 가져와 collector로 저장합니다. 그리고 만약 inheritanceState에 table이 있다면 createTable 메서드를 호출합니다.
- InFlightMetadataCollector란 프로그램이 실행되는 중에 데이터베이스 초기화 및 구성 단계에서, 일반적으로 엔티티, 테이블, 열, 링크 등과 같은 다양한 요소에 대한 정보를 수집합니다. 이렇게 수집된 메타데이터는 Hibernate가 다양한 작업을 수행하는 데 필요한 Context 정보를 제공해 일관된 매핑을 유지하고, 엔티티와 DB 테이블 간의 신뢰할 수 있는 매핑을 활용하여 개발자가 애플리케이션에서 DB 작업을 수행하도록 돕습니다
- 위의 메서드에선 Class와 Table에 대한 매핑을 진행한다고 생각해주시면 됩니다.
- 하나 더 메서드를 보면 좋은데 spring-JDBC에 GenericTableMetaDataProvider#locateTableAndProcessMetaData 입니다.
private void locateTableAndProcessMetaData(DatabaseMetaData databaseMetaData,
@Nullable String catalogName, @Nullable String schemaName, @Nullable String tableName) {
Map<String, TableMetaData> tableMeta = new HashMap<>();
ResultSet tables = null;
try {
tables = databaseMetaData.getTables(
catalogNameToUse(catalogName), schemaNameToUse(schemaName), tableNameToUse(tableName), null);
while (tables != null && tables.next()) {
TableMetaData tmd = new TableMetaData(tables.getString("TABLE_CAT"), tables.getString("TABLE_SCHEM"),
tables.getString("TABLE_NAME"));
if (tmd.schemaName() == null) {
tableMeta.put(this.userName != null ? this.userName.toUpperCase() : "", tmd);
}
else {
tableMeta.put(tmd.schemaName().toUpperCase(), tmd);
}
}
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Error while accessing table meta-data results: " + ex.getMessage());
}
}
finally {
JdbcUtils.closeResultSet(tables);
}
if (tableMeta.isEmpty()) {
if (logger.isInfoEnabled()) {
logger.info("Unable to locate table meta-data for '" + tableName + "': column names must be provided");
}
}
else {
processTableColumns(databaseMetaData, findTableMetaData(schemaName, tableName, tableMeta));
}
}
- 여기서 실제 Database의 meta Data를 가져오게 되고, 이때 Tabels의 데이터들을 가져오게 됩니다.
- 마지막으로 아래 메서드에서 validation을 통해 이름이 매칭되는지 체크하게 됩니다.
- AbstractSchemaValidator#validateTables/validateTable
protected abstract void validateTables(
Metadata metadata,
DatabaseInformation databaseInformation,
ExecutionOptions options,
ContributableMatcher contributableInclusionFilter,
Dialect dialect, Namespace namespace);
protected void validateTable(
Table table,
TableInformation tableInformation,
Metadata metadata,
ExecutionOptions options,
Dialect dialect) {
if ( tableInformation == null ) {
throw new SchemaManagementException(
String.format(
"Schema-validation: missing table [%s]",
table.getQualifiedTableName().toString()
)
);
}
for ( Column column : table.getColumns() ) {
final ColumnInformation existingColumn = tableInformation.getColumn( Identifier.toIdentifier( column.getQuotedName() ) );
if ( existingColumn == null ) {
throw new SchemaManagementException(
String.format(
"Schema-validation: missing column [%s] in table [%s]",
column.getName(),
table.getQualifiedTableName()
)
);
}
validateColumnType( table, column, existingColumn, metadata, options, dialect );
}
}
- 실제 Column까지도 다른게 있는지 체크하는 로직이 작성되어 있습니다.
결론
- 단순히 @Table 이라는 어노테이션은 테이블과 엔티티의 이름만 매핑시켜주는 역할만 하는줄 알았지만, 실질적으로 이름이 뿐만 아니라 여러가지 정보를 지정하고, 매핑시켜주는 역할을 하게 됩니다.
- 내부적으로 validate의 로직을 보았을때, 이러한 에러가 발생햇을때 어떤 부분이 문제인지도 알수 있다는 장점이 있는거 같습니다.
- DB의 기능을 잘 알아야, JPA도 잘 쓸수 있는거 같습니다.
728x90
728x90