Java tutorial
/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.mapping; import java.io.Serializable; import java.lang.annotation.Annotation; import java.sql.Types; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.Objects; import javax.persistence.AttributeConverter; import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor; import org.hibernate.boot.model.convert.spi.ConverterDescriptor; import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.Mapping; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.IdentityGenerator; import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.metamodel.model.convert.spi.JpaAttributeConverter; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.BinaryType; import org.hibernate.type.RowVersionType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.JdbcTypeNameMapper; import org.hibernate.type.descriptor.converter.AttributeConverterSqlTypeDescriptorAdapter; import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter; import org.hibernate.type.descriptor.java.BasicJavaDescriptor; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext; import org.hibernate.type.descriptor.sql.JdbcTypeJavaClassMappings; import org.hibernate.type.descriptor.sql.LobTypeMappings; import org.hibernate.type.descriptor.sql.NationalizedTypeMappings; import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.usertype.DynamicParameterizedType; /** * Any value that maps to columns. * @author Gavin King */ public class SimpleValue implements KeyValue { private static final CoreMessageLogger log = CoreLogging.messageLogger(SimpleValue.class); public static final String DEFAULT_ID_GEN_STRATEGY = "assigned"; private final MetadataImplementor metadata; private final List<Selectable> columns = new ArrayList<>(); private final List<Boolean> insertability = new ArrayList<>(); private final List<Boolean> updatability = new ArrayList<>(); private String typeName; private Properties typeParameters; private boolean isVersion; private boolean isNationalized; private boolean isLob; private Properties identifierGeneratorProperties; private String identifierGeneratorStrategy = DEFAULT_ID_GEN_STRATEGY; private String nullValue; private Table table; private String foreignKeyName; private String foreignKeyDefinition; private boolean alternateUniqueKey; private boolean cascadeDeleteEnabled; private ConverterDescriptor attributeConverterDescriptor; private Type type; /** * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext)} instead. */ @Deprecated public SimpleValue(MetadataImplementor metadata) { this.metadata = metadata; } /** * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext, Table)} instead. */ @Deprecated public SimpleValue(MetadataImplementor metadata, Table table) { this(metadata); this.table = table; } /** * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext, Table)} instead. */ @Deprecated public SimpleValue(MetadataBuildingContext buildingContext) { this(buildingContext.getMetadataCollector()); } public SimpleValue(MetadataBuildingContext buildingContext, Table table) { this.metadata = buildingContext.getMetadataCollector(); this.table = table; } public MetadataImplementor getMetadata() { return metadata; } @Override public ServiceRegistry getServiceRegistry() { return getMetadata().getMetadataBuildingOptions().getServiceRegistry(); } @Override public boolean isCascadeDeleteEnabled() { return cascadeDeleteEnabled; } public void setCascadeDeleteEnabled(boolean cascadeDeleteEnabled) { this.cascadeDeleteEnabled = cascadeDeleteEnabled; } public void addColumn(Column column) { addColumn(column, true, true); } public void addColumn(Column column, boolean isInsertable, boolean isUpdatable) { int index = columns.indexOf(column); if (index == -1) { columns.add(column); insertability.add(isInsertable); updatability.add(isUpdatable); } else { if (insertability.get(index) != isInsertable) { throw new IllegalStateException( "Same column is added more than once with different values for isInsertable"); } if (updatability.get(index) != isUpdatable) { throw new IllegalStateException( "Same column is added more than once with different values for isUpdatable"); } } column.setValue(this); column.setTypeIndex(columns.size() - 1); } public void addFormula(Formula formula) { columns.add(formula); insertability.add(false); updatability.add(false); } @Override public boolean hasFormula() { Iterator iter = getColumnIterator(); while (iter.hasNext()) { Object o = iter.next(); if (o instanceof Formula) { return true; } } return false; } @Override public int getColumnSpan() { return columns.size(); } @Override public Iterator<Selectable> getColumnIterator() { return columns.iterator(); } public List getConstraintColumns() { return columns; } public String getTypeName() { return typeName; } public void setTypeName(String typeName) { if (typeName != null && typeName.startsWith(AttributeConverterTypeAdapter.NAME_PREFIX)) { final String converterClassName = typeName .substring(AttributeConverterTypeAdapter.NAME_PREFIX.length()); final ClassLoaderService cls = getMetadata().getMetadataBuildingOptions().getServiceRegistry() .getService(ClassLoaderService.class); try { final Class<? extends AttributeConverter> converterClass = cls.classForName(converterClassName); this.attributeConverterDescriptor = new ClassBasedConverterDescriptor(converterClass, false, ((InFlightMetadataCollector) getMetadata()).getClassmateContext()); return; } catch (Exception e) { log.logBadHbmAttributeConverterType(typeName, e.getMessage()); } } this.typeName = typeName; } public void makeVersion() { this.isVersion = true; } public boolean isVersion() { return isVersion; } public void makeNationalized() { this.isNationalized = true; } public boolean isNationalized() { return isNationalized; } public void makeLob() { this.isLob = true; } public boolean isLob() { return isLob; } public void setTable(Table table) { this.table = table; } @Override public void createForeignKey() throws MappingException { } @Override public void createForeignKeyOfEntity(String entityName) { if (!hasFormula() && !"none".equals(getForeignKeyName())) { ForeignKey fk = table.createForeignKey(getForeignKeyName(), getConstraintColumns(), entityName, getForeignKeyDefinition()); fk.setCascadeDeleteEnabled(cascadeDeleteEnabled); } } private IdentifierGenerator identifierGenerator; @Override public IdentifierGenerator createIdentifierGenerator(IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect, String defaultCatalog, String defaultSchema, RootClass rootClass) throws MappingException { if (identifierGenerator != null) { return identifierGenerator; } Properties params = new Properties(); //if the hibernate-mapping did not specify a schema/catalog, use the defaults //specified by properties - but note that if the schema/catalog were specified //in hibernate-mapping, or as params, they will already be initialized and //will override the values set here (they are in identifierGeneratorProperties) if (defaultSchema != null) { params.setProperty(PersistentIdentifierGenerator.SCHEMA, defaultSchema); } if (defaultCatalog != null) { params.setProperty(PersistentIdentifierGenerator.CATALOG, defaultCatalog); } //pass the entity-name, if not a collection-id if (rootClass != null) { params.setProperty(IdentifierGenerator.ENTITY_NAME, rootClass.getEntityName()); params.setProperty(IdentifierGenerator.JPA_ENTITY_NAME, rootClass.getJpaEntityName()); } //init the table here instead of earlier, so that we can get a quoted table name //TODO: would it be better to simply pass the qualified table name, instead of // splitting it up into schema/catalog/table names String tableName = getTable().getQuotedName(dialect); params.setProperty(PersistentIdentifierGenerator.TABLE, tableName); //pass the column name (a generated id almost always has a single column) String columnName = ((Column) getColumnIterator().next()).getQuotedName(dialect); params.setProperty(PersistentIdentifierGenerator.PK, columnName); if (rootClass != null) { StringBuilder tables = new StringBuilder(); Iterator iter = rootClass.getIdentityTables().iterator(); while (iter.hasNext()) { Table table = (Table) iter.next(); tables.append(table.getQuotedName(dialect)); if (iter.hasNext()) { tables.append(", "); } } params.setProperty(PersistentIdentifierGenerator.TABLES, tables.toString()); } else { params.setProperty(PersistentIdentifierGenerator.TABLES, tableName); } if (identifierGeneratorProperties != null) { params.putAll(identifierGeneratorProperties); } // TODO : we should pass along all settings once "config lifecycle" is hashed out... final ConfigurationService cs = metadata.getMetadataBuildingOptions().getServiceRegistry() .getService(ConfigurationService.class); params.put(AvailableSettings.PREFER_POOLED_VALUES_LO, cs.getSetting(AvailableSettings.PREFER_POOLED_VALUES_LO, StandardConverters.BOOLEAN, false)); if (cs.getSettings().get(AvailableSettings.PREFERRED_POOLED_OPTIMIZER) != null) { params.put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, cs.getSettings().get(AvailableSettings.PREFERRED_POOLED_OPTIMIZER)); } identifierGeneratorFactory.setDialect(dialect); identifierGenerator = identifierGeneratorFactory.createIdentifierGenerator(identifierGeneratorStrategy, getType(), params); return identifierGenerator; } public boolean isUpdateable() { //needed to satisfy KeyValue return true; } public FetchMode getFetchMode() { return FetchMode.SELECT; } public Properties getIdentifierGeneratorProperties() { return identifierGeneratorProperties; } public String getNullValue() { return nullValue; } public Table getTable() { return table; } /** * Returns the identifierGeneratorStrategy. * @return String */ public String getIdentifierGeneratorStrategy() { return identifierGeneratorStrategy; } public boolean isIdentityColumn(IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect) { identifierGeneratorFactory.setDialect(dialect); return IdentityGenerator.class.isAssignableFrom( identifierGeneratorFactory.getIdentifierGeneratorClass(identifierGeneratorStrategy)); } /** * Sets the identifierGeneratorProperties. * @param identifierGeneratorProperties The identifierGeneratorProperties to set */ public void setIdentifierGeneratorProperties(Properties identifierGeneratorProperties) { this.identifierGeneratorProperties = identifierGeneratorProperties; } /** * Sets the identifierGeneratorStrategy. * @param identifierGeneratorStrategy The identifierGeneratorStrategy to set */ public void setIdentifierGeneratorStrategy(String identifierGeneratorStrategy) { this.identifierGeneratorStrategy = identifierGeneratorStrategy; } /** * Sets the nullValue. * @param nullValue The nullValue to set */ public void setNullValue(String nullValue) { this.nullValue = nullValue; } public String getForeignKeyName() { return foreignKeyName; } public void setForeignKeyName(String foreignKeyName) { this.foreignKeyName = foreignKeyName; } public String getForeignKeyDefinition() { return foreignKeyDefinition; } public void setForeignKeyDefinition(String foreignKeyDefinition) { this.foreignKeyDefinition = foreignKeyDefinition; } public boolean isAlternateUniqueKey() { return alternateUniqueKey; } public void setAlternateUniqueKey(boolean unique) { this.alternateUniqueKey = unique; } public boolean isNullable() { Iterator itr = getColumnIterator(); while (itr.hasNext()) { final Object selectable = itr.next(); if (selectable instanceof Formula) { // if there are *any* formulas, then the Value overall is // considered nullable return true; } else if (!((Column) selectable).isNullable()) { // if there is a single non-nullable column, the Value // overall is considered non-nullable. return false; } } // nullable by default return true; } public boolean isSimpleValue() { return true; } public boolean isValid(Mapping mapping) throws MappingException { return getColumnSpan() == getType().getColumnSpan(mapping); } public Type getType() throws MappingException { if (type != null) { return type; } if (typeName == null) { throw new MappingException("No type name"); } if (typeParameters != null && Boolean.valueOf(typeParameters.getProperty(DynamicParameterizedType.IS_DYNAMIC)) && typeParameters.get(DynamicParameterizedType.PARAMETER_TYPE) == null) { createParameterImpl(); } Type result = getMetadata().getTypeConfiguration().getTypeResolver().heuristicType(typeName, typeParameters); // if this is a byte[] version/timestamp, then we need to use RowVersionType // instead of BinaryType (HHH-10413) if (isVersion && BinaryType.class.isInstance(result)) { log.debug("version is BinaryType; changing to RowVersionType"); result = RowVersionType.INSTANCE; } if (result == null) { String msg = "Could not determine type for: " + typeName; if (table != null) { msg += ", at table: " + table.getName(); } if (columns != null && columns.size() > 0) { msg += ", for columns: " + columns; } throw new MappingException(msg); } return result; } @Override public void setTypeUsingReflection(String className, String propertyName) throws MappingException { // NOTE : this is called as the last piece in setting SimpleValue type information, and implementations // rely on that fact, using it as a signal that all information it is going to get is defined at this point... if (typeName != null) { // assume either (a) explicit type was specified or (b) determine was already performed return; } if (type != null) { return; } if (attributeConverterDescriptor == null) { // this is here to work like legacy. This should change when we integrate with metamodel to // look for SqlTypeDescriptor and JavaTypeDescriptor individually and create the BasicType (well, really // keep a registry of [SqlTypeDescriptor,JavaTypeDescriptor] -> BasicType...) if (className == null) { throw new MappingException( "Attribute types for a dynamic entity must be explicitly specified: " + propertyName); } typeName = ReflectHelper.reflectedPropertyClass(className, propertyName, getMetadata() .getMetadataBuildingOptions().getServiceRegistry().getService(ClassLoaderService.class)) .getName(); // todo : to fully support isNationalized here we need do the process hinted at above // essentially, much of the logic from #buildAttributeConverterTypeAdapter wrt resolving // a (1) SqlTypeDescriptor, a (2) JavaTypeDescriptor and dynamically building a BasicType // combining them. return; } // we had an AttributeConverter... type = buildAttributeConverterTypeAdapter(); } /** * Build a Hibernate Type that incorporates the JPA AttributeConverter. AttributeConverter works totally in * memory, meaning it converts between one Java representation (the entity attribute representation) and another * (the value bound into JDBC statements or extracted from results). However, the Hibernate Type system operates * at the lower level of actually dealing directly with those JDBC objects. So even though we have an * AttributeConverter, we still need to "fill out" the rest of the BasicType data and bridge calls * to bind/extract through the converter. * <p/> * Essentially the idea here is that an intermediate Java type needs to be used. Let's use an example as a means * to illustrate... Consider an {@code AttributeConverter<Integer,String>}. This tells Hibernate that the domain * model defines this attribute as an Integer value (the 'entityAttributeJavaType'), but that we need to treat the * value as a String (the 'databaseColumnJavaType') when dealing with JDBC (aka, the database type is a * VARCHAR/CHAR):<ul> * <li> * When binding values to PreparedStatements we need to convert the Integer value from the entity * into a String and pass that String to setString. The conversion is handled by calling * {@link AttributeConverter#convertToDatabaseColumn(Object)} * </li> * <li> * When extracting values from ResultSets (or CallableStatement parameters) we need to handle the * value via getString, and convert that returned String to an Integer. That conversion is handled * by calling {@link AttributeConverter#convertToEntityAttribute(Object)} * </li> * </ul> * * @return The built AttributeConverter -> Type adapter * * @todo : ultimately I want to see attributeConverterJavaType and attributeConverterJdbcTypeCode specify-able separately * then we can "play them against each other" in terms of determining proper typing * * @todo : see if we already have previously built a custom on-the-fly BasicType for this AttributeConverter; see note below about caching */ @SuppressWarnings("unchecked") private Type buildAttributeConverterTypeAdapter() { // todo : validate the number of columns present here? final JpaAttributeConverter jpaAttributeConverter = attributeConverterDescriptor .createJpaAttributeConverter(new JpaAttributeConverterCreationContext() { @Override public ManagedBeanRegistry getManagedBeanRegistry() { return getMetadata().getMetadataBuildingOptions().getServiceRegistry() .getService(ManagedBeanRegistry.class); } @Override public org.hibernate.type.descriptor.java.spi.JavaTypeDescriptorRegistry getJavaTypeDescriptorRegistry() { return metadata.getTypeConfiguration().getJavaTypeDescriptorRegistry(); } }); final BasicJavaDescriptor entityAttributeJavaTypeDescriptor = jpaAttributeConverter .getDomainJavaTypeDescriptor(); // build the SqlTypeDescriptor adapter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Going back to the illustration, this should be a SqlTypeDescriptor that handles the Integer <-> String // conversions. This is the more complicated piece. First we need to determine the JDBC type code // corresponding to the AttributeConverter's declared "databaseColumnJavaType" (how we read that value out // of ResultSets). See JdbcTypeJavaClassMappings for details. Again, given example, this should return // VARCHAR/CHAR final SqlTypeDescriptor recommendedSqlType = jpaAttributeConverter.getRelationalJavaTypeDescriptor() .getJdbcRecommendedSqlType( // todo (6.0) : handle the other JdbcRecommendedSqlTypeMappingContext methods metadata::getTypeConfiguration); int jdbcTypeCode = recommendedSqlType.getSqlType(); if (isLob()) { if (LobTypeMappings.isMappedToKnownLobCode(jdbcTypeCode)) { jdbcTypeCode = LobTypeMappings.getLobCodeTypeMapping(jdbcTypeCode); } else { if (Serializable.class.isAssignableFrom(entityAttributeJavaTypeDescriptor.getJavaType())) { jdbcTypeCode = Types.BLOB; } else { throw new IllegalArgumentException(String.format(Locale.ROOT, "JDBC type-code [%s (%s)] not known to have a corresponding LOB equivalent, and Java type is not Serializable (to use BLOB)", jdbcTypeCode, JdbcTypeNameMapper.getTypeName(jdbcTypeCode))); } } } if (isNationalized()) { jdbcTypeCode = NationalizedTypeMappings.toNationalizedTypeCode(jdbcTypeCode); } // find the standard SqlTypeDescriptor for that JDBC type code (allow itr to be remapped if needed!) final SqlTypeDescriptor sqlTypeDescriptor = getMetadata().getMetadataBuildingOptions().getServiceRegistry() .getService(JdbcServices.class).getJdbcEnvironment().getDialect().remapSqlTypeDescriptor( metadata.getTypeConfiguration().getSqlTypeDescriptorRegistry().getDescriptor(jdbcTypeCode)); // and finally construct the adapter, which injects the AttributeConverter calls into the binding/extraction // process... final SqlTypeDescriptor sqlTypeDescriptorAdapter = new AttributeConverterSqlTypeDescriptorAdapter( jpaAttributeConverter, sqlTypeDescriptor, jpaAttributeConverter.getRelationalJavaTypeDescriptor()); // todo : cache the AttributeConverterTypeAdapter in case that AttributeConverter is applied multiple times. final String name = AttributeConverterTypeAdapter.NAME_PREFIX + jpaAttributeConverter.getConverterJavaTypeDescriptor().getJavaType().getName(); final String description = String.format("BasicType adapter for AttributeConverter<%s,%s>", jpaAttributeConverter.getDomainJavaTypeDescriptor().getJavaType().getSimpleName(), jpaAttributeConverter.getRelationalJavaTypeDescriptor().getJavaType().getSimpleName()); return new AttributeConverterTypeAdapter(name, description, jpaAttributeConverter, sqlTypeDescriptorAdapter, jpaAttributeConverter.getDomainJavaTypeDescriptor().getJavaType(), jpaAttributeConverter.getRelationalJavaTypeDescriptor().getJavaType(), entityAttributeJavaTypeDescriptor); } public boolean isTypeSpecified() { return typeName != null; } public void setTypeParameters(Properties parameterMap) { this.typeParameters = parameterMap; } public Properties getTypeParameters() { return typeParameters; } public void copyTypeFrom(SimpleValue sourceValue) { setTypeName(sourceValue.getTypeName()); setTypeParameters(sourceValue.getTypeParameters()); type = sourceValue.type; attributeConverterDescriptor = sourceValue.attributeConverterDescriptor; } @Override public boolean isSame(Value other) { return this == other || other instanceof SimpleValue && isSame((SimpleValue) other); } protected static boolean isSame(Value v1, Value v2) { return v1 == v2 || v1 != null && v2 != null && v1.isSame(v2); } public boolean isSame(SimpleValue other) { return Objects.equals(columns, other.columns) && Objects.equals(typeName, other.typeName) && Objects.equals(typeParameters, other.typeParameters) && Objects.equals(table, other.table) && Objects.equals(foreignKeyName, other.foreignKeyName) && Objects.equals(foreignKeyDefinition, other.foreignKeyDefinition); } @Override public String toString() { return getClass().getName() + '(' + columns.toString() + ')'; } public Object accept(ValueVisitor visitor) { return visitor.accept(this); } public boolean[] getColumnInsertability() { return extractBooleansFromList(insertability); } public boolean[] getColumnUpdateability() { return extractBooleansFromList(updatability); } private static boolean[] extractBooleansFromList(List<Boolean> list) { final boolean[] array = new boolean[list.size()]; int i = 0; for (Boolean value : list) { array[i++] = value; } return array; } public void setJpaAttributeConverterDescriptor(ConverterDescriptor descriptor) { this.attributeConverterDescriptor = descriptor; } private void createParameterImpl() { try { String[] columnsNames = new String[columns.size()]; for (int i = 0; i < columns.size(); i++) { Selectable column = columns.get(i); if (column instanceof Column) { columnsNames[i] = ((Column) column).getName(); } } final XProperty xProperty = (XProperty) typeParameters.get(DynamicParameterizedType.XPROPERTY); // todo : not sure this works for handling @MapKeyEnumerated final Annotation[] annotations = xProperty == null ? null : xProperty.getAnnotations(); final ClassLoaderService classLoaderService = getMetadata().getMetadataBuildingOptions() .getServiceRegistry().getService(ClassLoaderService.class); typeParameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterTypeImpl( classLoaderService.classForName( typeParameters.getProperty(DynamicParameterizedType.RETURNED_CLASS)), annotations, table.getCatalog(), table.getSchema(), table.getName(), Boolean.valueOf(typeParameters.getProperty(DynamicParameterizedType.IS_PRIMARY_KEY)), columnsNames)); } catch (ClassLoadingException e) { throw new MappingException("Could not create DynamicParameterizedType for type: " + typeName, e); } } private static final class ParameterTypeImpl implements DynamicParameterizedType.ParameterType { private final Class returnedClass; private final Annotation[] annotationsMethod; private final String catalog; private final String schema; private final String table; private final boolean primaryKey; private final String[] columns; private ParameterTypeImpl(Class returnedClass, Annotation[] annotationsMethod, String catalog, String schema, String table, boolean primaryKey, String[] columns) { this.returnedClass = returnedClass; this.annotationsMethod = annotationsMethod; this.catalog = catalog; this.schema = schema; this.table = table; this.primaryKey = primaryKey; this.columns = columns; } @Override public Class getReturnedClass() { return returnedClass; } @Override public Annotation[] getAnnotationsMethod() { return annotationsMethod; } @Override public String getCatalog() { return catalog; } @Override public String getSchema() { return schema; } @Override public String getTable() { return table; } @Override public boolean isPrimaryKey() { return primaryKey; } @Override public String[] getColumns() { return columns; } } }