/* * Hibernate OGM, Domain model persistence for NoSQL datastores * * 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.ogm.datastore.mongodb.impl; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.NamingHelper; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.cfg.Environment; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.mapping.Column; import org.hibernate.mapping.Index; import org.hibernate.mapping.Table; import org.hibernate.mapping.UniqueKey; import org.hibernate.ogm.datastore.mongodb.MongoDBDialect; import org.hibernate.ogm.datastore.mongodb.index.impl.MongoDBIndexSpec; import org.hibernate.ogm.datastore.mongodb.logging.impl.Log; import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory; import org.hibernate.ogm.datastore.spi.BaseSchemaDefiner; import org.hibernate.ogm.datastore.spi.DatastoreProvider; import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata; import org.hibernate.ogm.model.key.spi.EntityKeyMetadata; import org.hibernate.ogm.model.key.spi.IdSourceKeyMetadata; import org.hibernate.ogm.options.shared.impl.IndexOptionsOption; import org.hibernate.ogm.options.shared.spi.IndexOption; import org.hibernate.ogm.options.shared.spi.IndexOptions; import org.hibernate.ogm.options.spi.OptionsService; import org.hibernate.ogm.persister.impl.OgmEntityPersister; import org.hibernate.ogm.util.impl.Contracts; import org.hibernate.ogm.util.impl.StringHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.service.spi.ServiceRegistryImplementor; import org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy; import com.mongodb.MongoException; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import org.bson.Document; /** * Performs sanity checks of the mapped objects. * * @author Gunnar Morling * @author Sanne Grinovero * @author Francois Le Droff * @author Guillaume Smet */ public class MongoDBSchemaDefiner extends BaseSchemaDefiner { private static final Log log = LoggerFactory.getLogger(); private static final int INDEX_CREATION_ERROR_CODE = 85; private List<MongoDBIndexSpec> indexSpecs = new ArrayList<>(); @Override public void validateMapping(SchemaDefinitionContext context) { validateGenerators( context.getAllIdSourceKeyMetadata() ); validateEntityCollectionNames( context.getAllEntityKeyMetadata() ); validateAssociationNames( context.getAllAssociationKeyMetadata() ); validateAllPersisters( context.getSessionFactory().getEntityPersisters().values() ); validateIndexSpecs( context ); } @Override public void initializeSchema( SchemaDefinitionContext context) { SessionFactoryImplementor sessionFactoryImplementor = context.getSessionFactory(); ServiceRegistryImplementor registry = sessionFactoryImplementor.getServiceRegistry(); MongoDBDatastoreProvider provider = (MongoDBDatastoreProvider) registry.getService( DatastoreProvider.class ); for ( MongoDBIndexSpec indexSpec : indexSpecs ) { createIndex( provider.getDatabase(), indexSpec ); } } private void validateAllPersisters(Iterable<EntityPersister> persisters) { for ( EntityPersister persister : persisters ) { if ( persister instanceof OgmEntityPersister ) { OgmEntityPersister ogmPersister = (OgmEntityPersister) persister; int propertySpan = ogmPersister.getEntityMetamodel().getPropertySpan(); for ( int i = 0; i < propertySpan; i++ ) { String[] columnNames = ogmPersister.getPropertyColumnNames( i ); for ( String columnName : columnNames ) { validateAsMongoDBFieldName( columnName ); } } } } } private void validateAssociationNames(Iterable<AssociationKeyMetadata> allAssociationKeyMetadata) { for ( AssociationKeyMetadata associationKeyMetadata : allAssociationKeyMetadata ) { validateAsMongoDBCollectionName( associationKeyMetadata.getTable() ); for ( String column : associationKeyMetadata.getRowKeyColumnNames() ) { validateAsMongoDBFieldName( column ); } } } private void validateEntityCollectionNames(Iterable<EntityKeyMetadata> allEntityKeyMetadata) { for ( EntityKeyMetadata entityKeyMetadata : allEntityKeyMetadata ) { validateAsMongoDBCollectionName( entityKeyMetadata.getTable() ); for ( String column : entityKeyMetadata.getColumnNames() ) { validateAsMongoDBFieldName( column ); } } } private void validateGenerators(Iterable<IdSourceKeyMetadata> allIdSourceKeyMetadata) { for ( IdSourceKeyMetadata idSourceKeyMetadata : allIdSourceKeyMetadata ) { String keyColumn = idSourceKeyMetadata.getKeyColumnName(); if ( !keyColumn.equals( MongoDBDialect.ID_FIELDNAME ) ) { log.cannotUseGivenPrimaryKeyColumnName( keyColumn, MongoDBDialect.ID_FIELDNAME ); } } } private void validateIndexSpecs(SchemaDefinitionContext context) { OptionsService optionsService = context.getSessionFactory().getServiceRegistry().getService( OptionsService.class ); Map<String, Class<?>> tableEntityTypeMapping = context.getTableEntityTypeMapping(); Database database = context.getDatabase(); UniqueConstraintSchemaUpdateStrategy constraintMethod = UniqueConstraintSchemaUpdateStrategy.interpret( context.getSessionFactory().getProperties().get( Environment.UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY ) ); if ( constraintMethod == UniqueConstraintSchemaUpdateStrategy.SKIP ) { log.tracef( "Skipping generation of unique constraints" ); } for ( Namespace namespace : database.getNamespaces() ) { for ( Table table : namespace.getTables() ) { if ( table.isPhysicalTable() ) { Class<?> entityType = tableEntityTypeMapping.get( table.getName() ); if ( entityType == null ) { continue; } IndexOptions indexOptions = getIndexOptions( optionsService, entityType ); Set<String> forIndexNotReferenced = new HashSet<>( indexOptions.getReferencedIndexes() ); validateIndexSpecsForUniqueColumns( table, indexOptions, forIndexNotReferenced, constraintMethod ); validateIndexSpecsForUniqueKeys( table, indexOptions, forIndexNotReferenced, constraintMethod ); validateIndexSpecsForIndexes( table, indexOptions, forIndexNotReferenced ); for ( String forIndex : forIndexNotReferenced ) { log.indexOptionReferencingNonExistingIndex( table.getName(), forIndex ); } } } } } @SuppressWarnings("unchecked") private void validateIndexSpecsForUniqueColumns(Table table, IndexOptions indexOptions, Set<String> forIndexNotReferenced, UniqueConstraintSchemaUpdateStrategy constraintMethod) { Iterator<Column> columnIterator = table.getColumnIterator(); while ( columnIterator.hasNext() ) { Column column = columnIterator.next(); if ( column.isUnique() ) { String indexName = NamingHelper.INSTANCE.generateHashedConstraintName( "UK_", table.getNameIdentifier(), Identifier.toIdentifier( column.getName() ) ); forIndexNotReferenced.remove( indexName ); if ( constraintMethod != UniqueConstraintSchemaUpdateStrategy.SKIP ) { MongoDBIndexSpec indexSpec = new MongoDBIndexSpec( table.getName(), column.getName(), indexName, getIndexOptionDocument( table, indexOptions.getOptionForIndex( indexName ) ) ); if ( validateIndexSpec( indexSpec ) ) { indexSpecs.add( indexSpec ); } } } } } private void validateIndexSpecsForUniqueKeys(Table table, IndexOptions indexOptions, Set<String> forIndexNotReferenced, UniqueConstraintSchemaUpdateStrategy constraintMethod) { Iterator<UniqueKey> keys = table.getUniqueKeyIterator(); while ( keys.hasNext() ) { UniqueKey uniqueKey = keys.next(); forIndexNotReferenced.remove( uniqueKey.getName() ); if ( constraintMethod != UniqueConstraintSchemaUpdateStrategy.SKIP ) { MongoDBIndexSpec indexSpec = new MongoDBIndexSpec( uniqueKey, getIndexOptionDocument( table, indexOptions.getOptionForIndex( uniqueKey.getName() ) ) ); if ( validateIndexSpec( indexSpec ) ) { indexSpecs.add( indexSpec ); } } } } private void validateIndexSpecsForIndexes(Table table, IndexOptions indexOptions, Set<String> forIndexNotReferenced) { Iterator<Index> indexes = table.getIndexIterator(); while ( indexes.hasNext() ) { Index index = indexes.next(); forIndexNotReferenced.remove( index.getName() ); MongoDBIndexSpec indexSpec = new MongoDBIndexSpec( index, getIndexOptionDocument( table, indexOptions.getOptionForIndex( index.getName() ) ) ); if ( validateIndexSpec( indexSpec ) ) { indexSpecs.add( indexSpec ); } } } private Document getIndexOptionDocument(Table table, IndexOption indexOption) { try { Document options; if ( StringHelper.isNullOrEmptyString( indexOption.getOptions() ) ) { options = new Document(); } else { options = Document.parse( indexOption.getOptions() ); } options.put( "name", indexOption.getTargetIndexName() ); return options; } catch (Exception e) { throw log.invalidOptionsFormatForIndex( table.getName(), indexOption.getTargetIndexName(), e ); } } private IndexOptions getIndexOptions(OptionsService optionsService, Class<?> entityType) { IndexOptions options = optionsService.context().getEntityOptions( entityType ).getUnique( IndexOptionsOption.class ); if ( options == null ) { options = new IndexOptions(); } return options; } private boolean validateIndexSpec(MongoDBIndexSpec indexSpec) { boolean valid = true; if ( StringHelper.isNullOrEmptyString( indexSpec.getIndexName() ) ) { log.indexNameIsEmpty( indexSpec.getCollection() ); valid = false; } if ( indexSpec.getIndexKeysDocument().keySet().isEmpty() ) { log.noValidKeysForIndex( indexSpec.getCollection(), indexSpec.getIndexName() ); valid = false; } return valid; } public void createIndex(MongoDatabase database, MongoDBIndexSpec indexSpec) { MongoCollection<Document> collection = database.getCollection( indexSpec.getCollection() ); Map<String, Document> preexistingIndexes = getIndexes( collection ); String preexistingTextIndex = getPreexistingTextIndex( preexistingIndexes ); // if a text index already exists in the collection, MongoDB silently ignores the creation of the new text index // so we might as well log a warning about it if ( indexSpec.isTextIndex() && preexistingTextIndex != null && !preexistingTextIndex.equalsIgnoreCase( indexSpec.getIndexName() ) ) { throw log.unableToCreateTextIndex( collection.getNamespace().getCollectionName(), indexSpec.getIndexName(), preexistingTextIndex ); } try { // if the index is already present and with the same definition, MongoDB simply ignores the call // if the definition is not the same, MongoDB throws an error, except in the case of a text index // where it silently ignores the creation collection.createIndex( indexSpec.getIndexKeysDocument(), indexSpec.getOptions() ); } catch (MongoException e) { String indexName = indexSpec.getIndexName(); if ( e.getCode() == INDEX_CREATION_ERROR_CODE && !StringHelper.isNullOrEmptyString( indexName ) && preexistingIndexes.containsKey( indexName ) ) { // The index already exists with a different definition and has a name: we drop it and we recreate it collection.dropIndex( indexName ); collection.createIndex( indexSpec.getIndexKeysDocument(), indexSpec.getOptions() ); } else { throw log.unableToCreateIndex( collection.getNamespace().getCollectionName(), indexName, e ); } } } private Map<String, Document> getIndexes(MongoCollection<Document> collection) { Map<String, Document> indexMap = new HashMap<>(); MongoCursor<Document> it = collection.listIndexes().iterator(); while ( it.hasNext() ) { Document index = it.next(); indexMap.put( index.get( "name" ).toString(), index ); } return indexMap; } private String getPreexistingTextIndex(Map<String, Document> preexistingIndexes) { for ( Entry<String, Document> indexEntry : preexistingIndexes.entrySet() ) { Document keys = (Document) indexEntry.getValue().get( "key" ); if ( keys != null && keys.containsKey( "_fts" ) ) { return indexEntry.getKey(); } } return null; } /** * Validates a String to be a valid name to be used in MongoDB for a collection name. * * @param collectionName */ private static void validateAsMongoDBCollectionName(String collectionName) { Contracts.assertStringParameterNotEmpty( collectionName, "requestedName" ); //Yes it has some strange requirements. if ( collectionName.startsWith( "system." ) ) { throw log.collectionNameHasInvalidSystemPrefix( collectionName ); } else if ( collectionName.contains( "\u0000" ) ) { throw log.collectionNameContainsNULCharacter( collectionName ); } else if ( collectionName.contains( "$" ) ) { throw log.collectionNameContainsDollarCharacter( collectionName ); } } /** * Validates a String to be a valid name to be used in MongoDB for a field name. * * @param fieldName */ private void validateAsMongoDBFieldName(String fieldName) { if ( fieldName.startsWith( "$" ) ) { throw log.fieldNameHasInvalidDollarPrefix( fieldName ); } else if ( fieldName.contains( "\u0000" ) ) { throw log.fieldNameContainsNULCharacter( fieldName ); } } }