Java tutorial
/* * 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; import static org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoDBTupleSnapshot.SnapshotType.INSERT; import static org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoDBTupleSnapshot.SnapshotType.UPDATE; import static org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoHelpers.hasField; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.bson.types.ObjectId; import org.hibernate.HibernateException; import org.hibernate.annotations.common.AssertionFailure; import org.hibernate.engine.spi.QueryParameters; import org.hibernate.ogm.datastore.document.association.spi.impl.DocumentHelpers; import org.hibernate.ogm.datastore.document.options.AssociationStorageType; import org.hibernate.ogm.datastore.document.options.spi.AssociationStorageOption; import org.hibernate.ogm.datastore.map.impl.MapTupleSnapshot; import org.hibernate.ogm.datastore.mongodb.configuration.impl.MongoDBConfiguration; import org.hibernate.ogm.datastore.mongodb.dialect.impl.AssociationStorageStrategy; import org.hibernate.ogm.datastore.document.impl.EmbeddableStateFinder; import org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoDBAssociationSnapshot; import org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoDBTupleSnapshot; import org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoDBTupleSnapshot.SnapshotType; import org.hibernate.ogm.datastore.mongodb.dialect.impl.MongoHelpers; import org.hibernate.ogm.datastore.mongodb.impl.MongoDBDatastoreProvider; import org.hibernate.ogm.datastore.mongodb.logging.impl.Log; import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory; import org.hibernate.ogm.datastore.mongodb.options.AssociationDocumentStorageType; import org.hibernate.ogm.datastore.mongodb.options.impl.AssociationDocumentStorageOption; import org.hibernate.ogm.datastore.mongodb.options.impl.ReadPreferenceOption; import org.hibernate.ogm.datastore.mongodb.options.impl.WriteConcernOption; import org.hibernate.ogm.datastore.mongodb.query.impl.MongoDBQueryDescriptor; import org.hibernate.ogm.datastore.mongodb.query.parsing.nativequery.impl.MongoDBQueryDescriptorBuilder; import org.hibernate.ogm.datastore.mongodb.query.parsing.nativequery.impl.NativeQueryParser; import org.hibernate.ogm.datastore.mongodb.type.impl.ByteStringType; import org.hibernate.ogm.datastore.mongodb.type.impl.ObjectIdGridType; import org.hibernate.ogm.datastore.mongodb.type.impl.StringAsObjectIdGridType; import org.hibernate.ogm.datastore.mongodb.type.impl.StringAsObjectIdType; import org.hibernate.ogm.dialect.batch.spi.BatchableGridDialect; import org.hibernate.ogm.dialect.batch.spi.InsertOrUpdateAssociationOperation; import org.hibernate.ogm.dialect.batch.spi.InsertOrUpdateTupleOperation; import org.hibernate.ogm.dialect.batch.spi.Operation; import org.hibernate.ogm.dialect.batch.spi.OperationsQueue; import org.hibernate.ogm.dialect.batch.spi.RemoveAssociationOperation; import org.hibernate.ogm.dialect.batch.spi.RemoveTupleOperation; import org.hibernate.ogm.dialect.identity.spi.IdentityColumnAwareGridDialect; import org.hibernate.ogm.dialect.optimisticlock.spi.OptimisticLockingAwareGridDialect; import org.hibernate.ogm.dialect.query.spi.BackendQuery; import org.hibernate.ogm.dialect.query.spi.ClosableIterator; import org.hibernate.ogm.dialect.query.spi.NoOpParameterMetadataBuilder; import org.hibernate.ogm.dialect.query.spi.ParameterMetadataBuilder; import org.hibernate.ogm.dialect.query.spi.QueryableGridDialect; import org.hibernate.ogm.dialect.spi.AssociationContext; import org.hibernate.ogm.dialect.spi.AssociationTypeContext; import org.hibernate.ogm.dialect.spi.BaseGridDialect; import org.hibernate.ogm.dialect.spi.DuplicateInsertPreventionStrategy; import org.hibernate.ogm.dialect.spi.ModelConsumer; import org.hibernate.ogm.dialect.spi.NextValueRequest; import org.hibernate.ogm.dialect.spi.TupleAlreadyExistsException; import org.hibernate.ogm.dialect.spi.TupleContext; import org.hibernate.ogm.model.key.spi.AssociationKey; import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata; import org.hibernate.ogm.model.key.spi.EntityKey; import org.hibernate.ogm.model.key.spi.EntityKeyMetadata; import org.hibernate.ogm.model.key.spi.IdSourceKey; import org.hibernate.ogm.model.key.spi.RowKey; import org.hibernate.ogm.model.spi.Association; import org.hibernate.ogm.model.spi.AssociationKind; import org.hibernate.ogm.model.spi.Tuple; import org.hibernate.ogm.model.spi.TupleOperation; import org.hibernate.ogm.type.impl.StringCalendarDateType; import org.hibernate.ogm.type.spi.GridType; import org.hibernate.ogm.util.impl.CollectionHelper; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.Type; import org.parboiled.Parboiled; import org.parboiled.errors.ErrorUtils; import org.parboiled.parserunners.RecoveringParseRunner; import org.parboiled.support.ParsingResult; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.DuplicateKeyException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; /** * Each Tuple entry is stored as a property in a MongoDB document. * * Each association is stored in an association document containing three properties: * - the association table name (optionally) * - the RowKey column names and values * - the tuples as an array of elements * * Associations can be stored as: * - one MongoDB collection per association class. The collection name is prefixed. * - one MongoDB collection for all associations (the association table name property in then used) * - embed the collection info in the owning entity document is planned but not supported at the moment (OGM-177) * * Collection of embeddable are stored within the owning entity document under the * unqualified collection role * * In MongoDB is possible to batch operations but only for the creation of new documents * and only if they don't have invalid characters in the field name. * If these conditions are not met, the MongoDB mechanism for batch operations * is not going to be used. * * @author Guillaume Scheibel <guillaume.scheibel@gmail.com> * @author Alan Fitton <alan at eth0.org.uk> * @author Emmanuel Bernard <emmanuel@hibernate.org> */ public class MongoDBDialect extends BaseGridDialect implements QueryableGridDialect<MongoDBQueryDescriptor>, BatchableGridDialect, IdentityColumnAwareGridDialect, OptimisticLockingAwareGridDialect { public static final String ID_FIELDNAME = "_id"; public static final String PROPERTY_SEPARATOR = "."; public static final String ROWS_FIELDNAME = "rows"; public static final String TABLE_FIELDNAME = "table"; public static final String ASSOCIATIONS_COLLECTION_PREFIX = "associations_"; private static final Log log = LoggerFactory.getLogger(); private static final List<String> ROWS_FIELDNAME_LIST = Collections.singletonList(ROWS_FIELDNAME); private final MongoDBDatastoreProvider provider; private final DB currentDB; public MongoDBDialect(MongoDBDatastoreProvider provider) { this.provider = provider; this.currentDB = this.provider.getDatabase(); } @Override public Tuple getTuple(EntityKey key, TupleContext tupleContext) { DBObject found = this.getObject(key, tupleContext); if (found != null) { return new Tuple(new MongoDBTupleSnapshot(found, key.getMetadata(), UPDATE)); } else if (isInTheQueue(key, tupleContext)) { // The key has not been inserted in the db but it is in the queue return new Tuple(new MongoDBTupleSnapshot(prepareIdObject(key), key.getMetadata(), INSERT)); } else { return null; } } private boolean isInTheQueue(EntityKey key, TupleContext tupleContext) { OperationsQueue queue = tupleContext.getOperationsQueue(); return queue != null && queue.contains(key); } @Override public Tuple createTuple(EntityKeyMetadata entityKeyMetadata, TupleContext tupleContext) { return new Tuple(new MongoDBTupleSnapshot(new BasicDBObject(), entityKeyMetadata, SnapshotType.INSERT)); } @Override public Tuple createTuple(EntityKey key, TupleContext tupleContext) { DBObject toSave = this.prepareIdObject(key); return new Tuple(new MongoDBTupleSnapshot(toSave, key.getMetadata(), SnapshotType.INSERT)); } /** * Returns a {@link DBObject} representing the entity which embeds the specified association. */ private DBObject getEmbeddingEntity(AssociationKey key, AssociationContext associationContext) { DBObject embeddingEntityDocument = associationContext.getEntityTuple() != null ? ((MongoDBTupleSnapshot) associationContext.getEntityTuple().getSnapshot()).getDbObject() : null; if (embeddingEntityDocument != null) { return embeddingEntityDocument; } else { ReadPreference readPreference = getReadPreference(associationContext); DBCollection collection = getCollection(key.getEntityKey()); DBObject searchObject = prepareIdObject(key.getEntityKey()); DBObject projection = getProjection(key, true); return collection.findOne(searchObject, projection, readPreference); } } private DBObject getObject(EntityKey key, TupleContext tupleContext) { ReadPreference readPreference = getReadPreference(tupleContext); DBCollection collection = getCollection(key); DBObject searchObject = prepareIdObject(key); BasicDBObject projection = getProjection(tupleContext); return collection.findOne(searchObject, projection, readPreference); } private BasicDBObject getProjection(TupleContext tupleContext) { return getProjection(tupleContext.getSelectableColumns()); } /** * Returns a projection object for specifying the fields to retrieve during a specific find operation. */ private BasicDBObject getProjection(List<String> fieldNames) { BasicDBObject projection = new BasicDBObject(fieldNames.size()); for (String column : fieldNames) { projection.put(column, 1); } return projection; } /** * Create a DBObject which represents the _id field. * In case of simple id objects the json representation will look like {_id: "theIdValue"} * In case of composite id objects the json representation will look like {_id: {author: "Guillaume", title: "What this method is used for?"}} * * @param key * * @return the DBObject which represents the id field */ private BasicDBObject prepareIdObject(EntityKey key) { return this.prepareIdObject(key.getColumnNames(), key.getColumnValues()); } private BasicDBObject prepareIdObject(IdSourceKey key) { return this.prepareIdObject(key.getColumnNames(), key.getColumnValues()); } private BasicDBObject prepareIdObject(String[] columnNames, Object[] columnValues) { BasicDBObject object; if (columnNames.length == 1) { object = new BasicDBObject(ID_FIELDNAME, columnValues[0]); } else { object = new BasicDBObject(); DBObject idObject = new BasicDBObject(); for (int i = 0; i < columnNames.length; i++) { String columnName = columnNames[i]; Object columnValue = columnValues[i]; if (columnName.contains(PROPERTY_SEPARATOR)) { int dotIndex = columnName.indexOf(PROPERTY_SEPARATOR); String shortColumnName = columnName.substring(dotIndex + 1); idObject.put(shortColumnName, columnValue); } else { idObject.put(columnNames[i], columnValue); } } object.put(ID_FIELDNAME, idObject); } return object; } private DBCollection getCollection(String table) { return currentDB.getCollection(table); } private DBCollection getCollection(EntityKey key) { return getCollection(key.getTable()); } private DBCollection getCollection(EntityKeyMetadata entityKeyMetadata) { return getCollection(entityKeyMetadata.getTable()); } private DBCollection getAssociationCollection(AssociationKey key, AssociationStorageStrategy storageStrategy) { if (storageStrategy == AssociationStorageStrategy.GLOBAL_COLLECTION) { return getCollection(MongoDBConfiguration.DEFAULT_ASSOCIATION_STORE); } else { return getCollection(ASSOCIATIONS_COLLECTION_PREFIX + key.getTable()); } } private BasicDBObject getSubQuery(String operator, BasicDBObject query) { return query.get(operator) != null ? (BasicDBObject) query.get(operator) : new BasicDBObject(); } private void addSubQuery(String operator, BasicDBObject query, String column, Object value) { BasicDBObject subQuery = this.getSubQuery(operator, query); query.append(operator, subQuery.append(column, value)); } @Override public void insertOrUpdateTuple(EntityKey key, Tuple tuple, TupleContext tupleContext) { BasicDBObject idObject = this.prepareIdObject(key); DBObject updater = objectForUpdate(tuple, idObject, tupleContext); WriteConcern writeConcern = getWriteConcern(tupleContext); try { getCollection(key).update(idObject, updater, true, false, writeConcern); } catch (DuplicateKeyException dke) { throw new TupleAlreadyExistsException(key.getMetadata(), tuple, dke); } } @Override //TODO deal with dotted column names once this method is used for ALL / Dirty optimistic locking public boolean updateTupleWithOptimisticLock(EntityKey entityKey, Tuple oldLockState, Tuple tuple, TupleContext tupleContext) { BasicDBObject idObject = this.prepareIdObject(entityKey); for (String versionColumn : oldLockState.getColumnNames()) { idObject.put(versionColumn, oldLockState.get(versionColumn)); } DBObject updater = objectForUpdate(tuple, idObject, tupleContext); DBObject doc = getCollection(entityKey).findAndModify(idObject, updater); return doc != null; } @Override public void insertTuple(EntityKeyMetadata entityKeyMetadata, Tuple tuple, TupleContext tupleContext) { WriteConcern writeConcern = getWriteConcern(tupleContext); DBObject objectWithId = insertDBObject(entityKeyMetadata, tuple, writeConcern); String idColumnName = entityKeyMetadata.getColumnNames()[0]; tuple.put(idColumnName, objectWithId.get(ID_FIELDNAME)); } /* * Insert the tuple and return an object containing the id in the field ID_FIELDNAME */ private DBObject insertDBObject(EntityKeyMetadata entityKeyMetadata, Tuple tuple, WriteConcern writeConcern) { DBObject dbObject = objectForInsert(tuple, ((MongoDBTupleSnapshot) tuple.getSnapshot()).getDbObject()); getCollection(entityKeyMetadata).insert(dbObject, writeConcern); return dbObject; } /** * Creates a dbObject that can be pass to the mongoDB batch insert function */ private DBObject objectForInsert(Tuple tuple, DBObject dbObject) { MongoDBTupleSnapshot snapshot = (MongoDBTupleSnapshot) tuple.getSnapshot(); for (TupleOperation operation : tuple.getOperations()) { String column = operation.getColumn(); if (notInIdField(snapshot, column)) { switch (operation.getType()) { case PUT: MongoHelpers.setValue(dbObject, column, operation.getValue()); break; case PUT_NULL: case REMOVE: MongoHelpers.resetValue(dbObject, column); break; } } } return dbObject; } private DBObject objectForUpdate(Tuple tuple, DBObject idObject, TupleContext tupleContext) { MongoDBTupleSnapshot snapshot = (MongoDBTupleSnapshot) tuple.getSnapshot(); EmbeddableStateFinder embeddableStateFinder = new EmbeddableStateFinder(tuple, tupleContext); Set<String> nullEmbeddables = new HashSet<String>(); BasicDBObject updater = new BasicDBObject(); for (TupleOperation operation : tuple.getOperations()) { String column = operation.getColumn(); if (notInIdField(snapshot, column)) { switch (operation.getType()) { case PUT: this.addSubQuery("$set", updater, column, operation.getValue()); break; case PUT_NULL: case REMOVE: // try and find if this column is within an embeddable and if that embeddable is null // if true, unset the full embeddable String nullEmbeddable = embeddableStateFinder.getOuterMostNullEmbeddableIfAny(column); if (nullEmbeddable != null) { // we have a null embeddable if (!nullEmbeddables.contains(nullEmbeddable)) { // we have not processed it yet this.addSubQuery("$unset", updater, nullEmbeddable, Integer.valueOf(1)); nullEmbeddables.add(nullEmbeddable); } } else { // simply unset the column this.addSubQuery("$unset", updater, column, Integer.valueOf(1)); } break; } } } /* * Needed because in case of object with only an ID field * the "_id" won't be persisted properly. * With this adjustment, it will work like this: * if the object (from snapshot) doesn't exist so create the one represented by updater * so if at this moment the "_id" is not enforce properly an ObjectID will be created by the server instead * of the custom id */ if (updater.size() == 0) { return idObject; } return updater; } private boolean notInIdField(MongoDBTupleSnapshot snapshot, String column) { return !column.equals(ID_FIELDNAME) && !column.endsWith(PROPERTY_SEPARATOR + ID_FIELDNAME) && !snapshot.isKeyColumn(column); } @Override public void removeTuple(EntityKey key, TupleContext tupleContext) { DBCollection collection = getCollection(key); DBObject toDelete = prepareIdObject(key); WriteConcern writeConcern = getWriteConcern(tupleContext); collection.remove(toDelete, writeConcern); } @Override public boolean removeTupleWithOptimisticLock(EntityKey entityKey, Tuple oldLockState, TupleContext tupleContext) { DBObject toDelete = prepareIdObject(entityKey); for (String versionColumn : oldLockState.getColumnNames()) { toDelete.put(versionColumn, oldLockState.get(versionColumn)); } DBCollection collection = getCollection(entityKey); DBObject deleted = collection.findAndRemove(toDelete); return deleted != null; } //not for embedded private DBObject findAssociation(AssociationKey key, AssociationContext associationContext, AssociationStorageStrategy storageStrategy) { ReadPreference readPreference = getReadPreference(associationContext); final DBObject associationKeyObject = associationKeyToObject(key, storageStrategy); return getAssociationCollection(key, storageStrategy).findOne(associationKeyObject, getProjection(key, false), readPreference); } private DBObject getProjection(AssociationKey key, boolean embedded) { if (embedded) { return getProjection(Collections.singletonList(key.getMetadata().getCollectionRole())); } else { return getProjection(ROWS_FIELDNAME_LIST); } } private boolean isInTheQueue(EntityKey key, AssociationContext associationContext) { OperationsQueue queue = associationContext.getOperationsQueue(); return queue != null && queue.contains(key); } @Override public Association getAssociation(AssociationKey key, AssociationContext associationContext) { AssociationStorageStrategy storageStrategy = getAssociationStorageStrategy(key, associationContext); if (isEmbeddedAssociation(key) && isInTheQueue(key.getEntityKey(), associationContext)) { // The association is embedded and the owner of the association is in the insertion queue DBObject idObject = prepareIdObject(key.getEntityKey()); return new Association(new MongoDBAssociationSnapshot(idObject, key, storageStrategy)); } // We need to execute the previous operations first or it won't be able to find the key that should have // been created executeBatch(associationContext.getOperationsQueue()); if (storageStrategy == AssociationStorageStrategy.IN_ENTITY) { DBObject entity = getEmbeddingEntity(key, associationContext); if (entity != null && hasField(entity, key.getMetadata().getCollectionRole())) { return new Association(new MongoDBAssociationSnapshot(entity, key, storageStrategy)); } else { return null; } } final DBObject result = findAssociation(key, associationContext, storageStrategy); if (result == null) { return null; } else { return new Association(new MongoDBAssociationSnapshot(result, key, storageStrategy)); } } private boolean isEmbeddedAssociation(AssociationKey key) { return AssociationKind.EMBEDDED_COLLECTION == key.getMetadata().getAssociationKind(); } @Override public Association createAssociation(AssociationKey key, AssociationContext associationContext) { AssociationStorageStrategy storageStrategy = getAssociationStorageStrategy(key, associationContext); DBObject document = storageStrategy == AssociationStorageStrategy.IN_ENTITY ? getEmbeddingEntity(key, associationContext) : associationKeyToObject(key, storageStrategy); return new Association(new MongoDBAssociationSnapshot(document, key, storageStrategy)); } /** * Returns the rows of the given association as to be stored in the database. Elements of the returned list are * either * <ul> * <li>plain values such as {@code String}s, {@code int}s etc. in case there is exactly one row key column which is * not part of the association key (in this case we don't need to persist the key name as it can be restored from * the association key upon loading) or</li> * <li>{@code DBObject}s with keys/values for all row key columns which are not part of the association key</li> * </ul> */ private List<?> getAssociationRows(Association association, AssociationKey key) { List<Object> rows = new ArrayList<Object>(); for (RowKey rowKey : association.getKeys()) { rows.add(getAssociationRow(association.get(rowKey), key)); } return rows; } private Object getAssociationRow(Tuple row, AssociationKey associationKey) { String[] rowKeyColumnsToPersist = associationKey.getMetadata() .getColumnsWithoutKeyColumns(row.getColumnNames()); // return value itself if there is only a single column to store if (rowKeyColumnsToPersist.length == 1) { return row.get(rowKeyColumnsToPersist[0]); } // otherwise a DBObject with the row contents else { // if the columns are only made of the embedded id columns, remove the embedded id property prefix // collectionrole: [ { id: { id1: "foo", id2: "bar" } } ] becomes collectionrole: [ { id1: "foo", id2: "bar" } ] String prefix = getColumnSharedPrefixOfAssociatedEntityLink(associationKey); DBObject rowObject = new BasicDBObject(rowKeyColumnsToPersist.length); for (String column : rowKeyColumnsToPersist) { Object value = row.get(column); if (value != null) { // remove the prefix if present String columnName = column.startsWith(prefix) ? column.substring(prefix.length()) : column; MongoHelpers.setValue(rowObject, columnName, value); } } return rowObject; } } private String getColumnSharedPrefixOfAssociatedEntityLink(AssociationKey associationKey) { String[] associationKeyColumns = associationKey.getMetadata().getAssociatedEntityKeyMetadata() .getAssociationKeyColumns(); // we used to check that columns are the same (in an ordered fashion) // but to handle List and Map and store indexes / keys at the same level as the id columns // this check is removed String prefix = DocumentHelpers.getColumnSharedPrefix(associationKeyColumns); return prefix == null ? "" : prefix + "."; } @Override public void insertOrUpdateAssociation(AssociationKey key, Association association, AssociationContext associationContext) { DBCollection collection; DBObject query; MongoDBAssociationSnapshot assocSnapshot = (MongoDBAssociationSnapshot) association.getSnapshot(); String associationField; AssociationStorageStrategy storageStrategy = getAssociationStorageStrategy(key, associationContext); WriteConcern writeConcern = getWriteConcern(associationContext); List<?> rows = getAssociationRows(association, key); Object toStore = key.getMetadata().isOneToOne() ? rows.get(0) : rows; if (storageStrategy == AssociationStorageStrategy.IN_ENTITY) { collection = this.getCollection(key.getEntityKey()); query = this.prepareIdObject(key.getEntityKey()); associationField = key.getMetadata().getCollectionRole(); //TODO would that fail if getCollectionRole has dots? ((MongoDBTupleSnapshot) associationContext.getEntityTuple().getSnapshot()).getDbObject() .put(key.getMetadata().getCollectionRole(), toStore); } else { collection = getAssociationCollection(key, storageStrategy); query = assocSnapshot.getQueryObject(); associationField = ROWS_FIELDNAME; } DBObject update = new BasicDBObject("$set", new BasicDBObject(associationField, toStore)); collection.update(query, update, true, false, writeConcern); } @Override public void removeAssociation(AssociationKey key, AssociationContext associationContext) { AssociationStorageStrategy storageStrategy = getAssociationStorageStrategy(key, associationContext); WriteConcern writeConcern = getWriteConcern(associationContext); if (storageStrategy == AssociationStorageStrategy.IN_ENTITY) { DBObject entity = this.prepareIdObject(key.getEntityKey()); if (entity != null) { BasicDBObject updater = new BasicDBObject(); addSubQuery("$unset", updater, key.getMetadata().getCollectionRole(), Integer.valueOf(1)); DBObject dbObject = getEmbeddingEntity(key, associationContext); if (dbObject != null) { dbObject.removeField(key.getMetadata().getCollectionRole()); getCollection(key.getEntityKey()).update(entity, updater, true, false, writeConcern); } } } else { DBCollection collection = getAssociationCollection(key, storageStrategy); DBObject query = associationKeyToObject(key, storageStrategy); int nAffected = collection.remove(query, writeConcern).getN(); log.removedAssociation(nAffected); } } @Override public Number nextValue(NextValueRequest request) { DBCollection currentCollection = getCollection(request.getKey().getTable()); DBObject query = this.prepareIdObject(request.getKey()); //all columns should match to find the value String valueColumnName = request.getKey().getMetadata().getValueColumnName(); BasicDBObject update = new BasicDBObject(); //FIXME how to set the initialValue if the document is not present? It seems the inc value is used as initial new value Integer incrementObject = Integer.valueOf(request.getIncrement()); this.addSubQuery("$inc", update, valueColumnName, incrementObject); DBObject result = currentCollection.findAndModify(query, null, null, false, update, false, true); Object idFromDB; idFromDB = result == null ? null : result.get(valueColumnName); if (idFromDB == null) { //not inserted yet so we need to add initial value to increment to have the right next value in the DB //FIXME that means there is a small hole as when there was not value in the DB, we do add initial value in a non atomic way BasicDBObject updateForInitial = new BasicDBObject(); this.addSubQuery("$inc", updateForInitial, valueColumnName, request.getInitialValue()); currentCollection.findAndModify(query, null, null, false, updateForInitial, false, true); idFromDB = request.getInitialValue(); //first time we ask this value } else { idFromDB = result.get(valueColumnName); } if (idFromDB.getClass().equals(Integer.class) || idFromDB.getClass().equals(Long.class)) { Number id = (Number) idFromDB; //idFromDB is the one used and the BD contains the next available value to use return id; } else { throw new HibernateException("Cannot increment a non numeric field"); } } @Override public boolean isStoredInEntityStructure(AssociationKeyMetadata associationKeyMetadata, AssociationTypeContext associationTypeContext) { return getAssociationStorageStrategy(associationKeyMetadata, associationTypeContext) == AssociationStorageStrategy.IN_ENTITY; } @Override public GridType overrideType(Type type) { // Override handling of calendar types if (type == StandardBasicTypes.CALENDAR || type == StandardBasicTypes.CALENDAR_DATE) { return StringCalendarDateType.INSTANCE; } else if (type == StandardBasicTypes.BYTE) { return ByteStringType.INSTANCE; } else if (type.getReturnedClass() == ObjectId.class) { return ObjectIdGridType.INSTANCE; } else if (type instanceof StringAsObjectIdType) { return StringAsObjectIdGridType.INSTANCE; } return null; // all other types handled as in hibernate-ogm-core } @Override public void forEachTuple(ModelConsumer consumer, EntityKeyMetadata... entityKeyMetadatas) { DB db = provider.getDatabase(); for (EntityKeyMetadata entityKeyMetadata : entityKeyMetadatas) { DBCollection collection = db.getCollection(entityKeyMetadata.getTable()); for (DBObject dbObject : collection.find()) { consumer.consume(new Tuple(new MongoDBTupleSnapshot(dbObject, entityKeyMetadata, UPDATE))); } } } @Override public ClosableIterator<Tuple> executeBackendQuery(BackendQuery<MongoDBQueryDescriptor> backendQuery, QueryParameters queryParameters) { MongoDBQueryDescriptor queryDescriptor = backendQuery.getQuery(); EntityKeyMetadata entityKeyMetadata = backendQuery.getSingleEntityKeyMetadataOrNull(); String collectionName = getCollectionName(backendQuery, queryDescriptor, entityKeyMetadata); DBCollection collection = provider.getDatabase().getCollection(collectionName); switch (queryDescriptor.getOperation()) { case FIND: return doFind(queryDescriptor, queryParameters, collection, entityKeyMetadata); case COUNT: return doCount(queryDescriptor, collection); default: throw new IllegalArgumentException("Unexpected query operation: " + queryDescriptor); } } @Override public MongoDBQueryDescriptor parseNativeQuery(String nativeQuery) { NativeQueryParser parser = Parboiled.createParser(NativeQueryParser.class); ParsingResult<MongoDBQueryDescriptorBuilder> parseResult = new RecoveringParseRunner<MongoDBQueryDescriptorBuilder>( parser.Query()).run(nativeQuery); if (parseResult.hasErrors()) { throw new IllegalArgumentException( "Unsupported native query: " + ErrorUtils.printParseErrors(parseResult.parseErrors)); } return parseResult.resultValue.build(); } @Override public DuplicateInsertPreventionStrategy getDuplicateInsertPreventionStrategy( EntityKeyMetadata entityKeyMetadata) { return DuplicateInsertPreventionStrategy.NATIVE; } private ClosableIterator<Tuple> doFind(MongoDBQueryDescriptor query, QueryParameters queryParameters, DBCollection collection, EntityKeyMetadata entityKeyMetadata) { DBCursor cursor = collection.find(query.getCriteria(), query.getProjection()); if (query.getOrderBy() != null) { cursor.sort(query.getOrderBy()); } // apply firstRow/maxRows if present if (queryParameters.getRowSelection().getFirstRow() != null) { cursor.skip(queryParameters.getRowSelection().getFirstRow()); } if (queryParameters.getRowSelection().getMaxRows() != null) { cursor.limit(queryParameters.getRowSelection().getMaxRows()); } return new MongoDBResultsCursor(cursor, entityKeyMetadata); } private ClosableIterator<Tuple> doCount(MongoDBQueryDescriptor query, DBCollection collection) { long count = collection.count(query.getCriteria()); MapTupleSnapshot snapshot = new MapTupleSnapshot(Collections.<String, Object>singletonMap("n", count)); return CollectionHelper.newClosableIterator(Collections.singletonList(new Tuple(snapshot))); } /** * Returns the name of the MongoDB collection to execute the given query against. Will either be retrieved * <ul> * <li>from the given query descriptor (in case the query has been translated from JP-QL or it is a native query * using the extended syntax {@code db.<COLLECTION>.<OPERATION>(...)}</li> * <li>or from the single mapped entity type if it is a native query using the criteria-only syntax * * @param customQuery the original query to execute * @param queryDescriptor descriptor for the query * @param entityKeyMetadata meta-data in case this is a query with exactly one entity return * @return the name of the MongoDB collection to execute the given query against */ private String getCollectionName(BackendQuery<?> customQuery, MongoDBQueryDescriptor queryDescriptor, EntityKeyMetadata entityKeyMetadata) { if (queryDescriptor.getCollectionName() != null) { return queryDescriptor.getCollectionName(); } else if (entityKeyMetadata != null) { return entityKeyMetadata.getTable(); } else { throw log.unableToDetermineCollectionName(customQuery.getQuery().toString()); } } private DBObject associationKeyToObject(AssociationKey key, AssociationStorageStrategy storageStrategy) { if (storageStrategy == AssociationStorageStrategy.IN_ENTITY) { throw new AssertionFailure(MongoHelpers.class.getName() + ".associationKeyToObject should not be called for associations embedded in entity documents"); } Object[] columnValues = key.getColumnValues(); DBObject columns = new BasicDBObject(columnValues.length); // if the columns are only made of the embedded id columns, remove the embedded id property prefix // _id: [ { id: { id1: "foo", id2: "bar" } } ] becomes _id: [ { id1: "foo", id2: "bar" } ] String prefix = DocumentHelpers.getColumnSharedPrefix(key.getColumnNames()); prefix = prefix == null ? "" : prefix + "."; int i = 0; for (String name : key.getColumnNames()) { MongoHelpers.setValue(columns, name.substring(prefix.length()), columnValues[i++]); } BasicDBObject idObject = new BasicDBObject(1); if (storageStrategy == AssociationStorageStrategy.GLOBAL_COLLECTION) { columns.put(MongoDBDialect.TABLE_FIELDNAME, key.getTable()); } idObject.put(MongoDBDialect.ID_FIELDNAME, columns); return idObject; } private AssociationStorageStrategy getAssociationStorageStrategy(AssociationKey key, AssociationContext associationContext) { return getAssociationStorageStrategy(key.getMetadata(), associationContext.getAssociationTypeContext()); } /** * Returns the {@link AssociationStorageStrategy} effectively applying for the given association. If a setting is * given via the option mechanism, that one will be taken, otherwise the default value as given via the * corresponding configuration property is applied. */ private AssociationStorageStrategy getAssociationStorageStrategy(AssociationKeyMetadata keyMetadata, AssociationTypeContext associationTypeContext) { AssociationStorageType associationStorage = associationTypeContext.getOptionsContext() .getUnique(AssociationStorageOption.class); AssociationDocumentStorageType associationDocumentStorageType = associationTypeContext.getOptionsContext() .getUnique(AssociationDocumentStorageOption.class); return AssociationStorageStrategy.getInstance(keyMetadata, associationStorage, associationDocumentStorageType); } @Override public void executeBatch(OperationsQueue queue) { if (!queue.isClosed()) { Operation operation = queue.poll(); Map<DBCollection, BatchInsertionTask> inserts = new HashMap<DBCollection, BatchInsertionTask>(); List<MongoDBTupleSnapshot> insertSnapshots = new ArrayList<MongoDBTupleSnapshot>(); while (operation != null) { if (operation instanceof InsertOrUpdateTupleOperation) { InsertOrUpdateTupleOperation update = (InsertOrUpdateTupleOperation) operation; executeBatchUpdate(inserts, update); MongoDBTupleSnapshot snapshot = (MongoDBTupleSnapshot) update.getTuple().getSnapshot(); if (snapshot.getSnapshotType() == INSERT) { insertSnapshots.add(snapshot); } } else if (operation instanceof RemoveTupleOperation) { RemoveTupleOperation tupleOp = (RemoveTupleOperation) operation; executeBatchRemove(inserts, tupleOp); } else if (operation instanceof InsertOrUpdateAssociationOperation) { InsertOrUpdateAssociationOperation update = (InsertOrUpdateAssociationOperation) operation; executeBatchUpdateAssociation(inserts, update); } else if (operation instanceof RemoveAssociationOperation) { RemoveAssociationOperation remove = (RemoveAssociationOperation) operation; removeAssociation(remove.getAssociationKey(), remove.getContext()); } else { throw new UnsupportedOperationException( "Operation not supported on MongoDB: " + operation.getClass().getName()); } operation = queue.poll(); } flushInserts(inserts); for (MongoDBTupleSnapshot insertSnapshot : insertSnapshots) { insertSnapshot.setSnapshotType(UPDATE); } queue.close(); } } private void executeBatchRemove(Map<DBCollection, BatchInsertionTask> inserts, RemoveTupleOperation tupleOperation) { EntityKey entityKey = tupleOperation.getEntityKey(); DBCollection collection = getCollection(entityKey); BatchInsertionTask batchedInserts = inserts.get(collection); if (batchedInserts != null && batchedInserts.containsKey(entityKey)) { batchedInserts.remove(entityKey); } else { removeTuple(entityKey, tupleOperation.getTupleContext()); } } private void executeBatchUpdate(Map<DBCollection, BatchInsertionTask> inserts, InsertOrUpdateTupleOperation tupleOperation) { EntityKey entityKey = tupleOperation.getEntityKey(); Tuple tuple = tupleOperation.getTuple(); MongoDBTupleSnapshot snapshot = (MongoDBTupleSnapshot) tupleOperation.getTuple().getSnapshot(); WriteConcern writeConcern = getWriteConcern(tupleOperation.getTupleContext()); if (INSERT == snapshot.getSnapshotType()) { prepareForInsert(inserts, snapshot, entityKey, tuple, writeConcern); } else { // Object already exists in the db or has invalid fields: insertOrUpdateTuple(entityKey, tuple, tupleOperation.getTupleContext()); } } private void executeBatchUpdateAssociation(Map<DBCollection, BatchInsertionTask> inserts, InsertOrUpdateAssociationOperation updateOp) { AssociationKey associationKey = updateOp.getAssociationKey(); if (isEmbeddedAssociation(associationKey)) { DBCollection collection = getCollection(associationKey.getEntityKey()); BatchInsertionTask batchInserts = inserts.get(collection); if (batchInserts != null && batchInserts.containsKey(associationKey.getEntityKey())) { // The owner of the association is in the insertion queue, // we are going to update it with the collection of elements WriteConcern writeConcern = getWriteConcern(updateOp.getContext()); BatchInsertionTask insertTask = getOrCreateBatchInsertionTask(inserts, associationKey.getEntityKey().getMetadata(), collection, writeConcern); DBObject documentForInsertion = insertTask.get(associationKey.getEntityKey()); List<?> embeddedElements = getAssociationRows(updateOp.getAssociation(), updateOp.getAssociationKey()); String collectionRole = associationKey.getMetadata().getCollectionRole(); MongoHelpers.setValue(documentForInsertion, collectionRole, embeddedElements); } else { insertOrUpdateAssociation(updateOp.getAssociationKey(), updateOp.getAssociation(), updateOp.getContext()); } } else { insertOrUpdateAssociation(updateOp.getAssociationKey(), updateOp.getAssociation(), updateOp.getContext()); } } @Override public ParameterMetadataBuilder getParameterMetadataBuilder() { return NoOpParameterMetadataBuilder.INSTANCE; } private void prepareForInsert(Map<DBCollection, BatchInsertionTask> inserts, MongoDBTupleSnapshot snapshot, EntityKey entityKey, Tuple tuple, WriteConcern writeConcern) { DBCollection collection = getCollection(entityKey); BatchInsertionTask batchInsertion = getOrCreateBatchInsertionTask(inserts, entityKey.getMetadata(), collection, writeConcern); DBObject document = getCurrentDocument(snapshot, batchInsertion, entityKey); DBObject newDocument = objectForInsert(tuple, document); inserts.get(collection).put(entityKey, newDocument); } private DBObject getCurrentDocument(MongoDBTupleSnapshot snapshot, BatchInsertionTask batchInsert, EntityKey entityKey) { DBObject fromBatchInsertion = batchInsert.get(entityKey); return fromBatchInsertion != null ? fromBatchInsertion : snapshot.getDbObject(); } private BatchInsertionTask getOrCreateBatchInsertionTask(Map<DBCollection, BatchInsertionTask> inserts, EntityKeyMetadata entityKeyMetadata, DBCollection collection, WriteConcern writeConcern) { BatchInsertionTask insertsForCollection = inserts.get(collection); if (insertsForCollection == null) { insertsForCollection = new BatchInsertionTask(entityKeyMetadata, writeConcern); inserts.put(collection, insertsForCollection); } return insertsForCollection; } private void flushInserts(Map<DBCollection, BatchInsertionTask> inserts) { for (Map.Entry<DBCollection, BatchInsertionTask> entry : inserts.entrySet()) { DBCollection collection = entry.getKey(); if (entry.getValue().isEmpty()) { // has been emptied due to subsequent removals before flushes continue; } try { collection.insert(entry.getValue().getAll(), entry.getValue().getWriteConcern()); } catch (DuplicateKeyException dke) { throw new TupleAlreadyExistsException(entry.getValue().getEntityKeyMetadata(), null, dke); } } inserts.clear(); } private WriteConcern getWriteConcern(TupleContext tupleContext) { return tupleContext.getOptionsContext().getUnique(WriteConcernOption.class); } private WriteConcern getWriteConcern(AssociationContext associationContext) { return associationContext.getAssociationTypeContext().getOptionsContext() .getUnique(WriteConcernOption.class); } private ReadPreference getReadPreference(TupleContext tupleContext) { return tupleContext.getOptionsContext().getUnique(ReadPreferenceOption.class); } private ReadPreference getReadPreference(AssociationContext associationContext) { return associationContext.getAssociationTypeContext().getOptionsContext() .getUnique(ReadPreferenceOption.class); } private static class MongoDBResultsCursor implements ClosableIterator<Tuple> { private final DBCursor cursor; private final EntityKeyMetadata metadata; public MongoDBResultsCursor(DBCursor cursor, EntityKeyMetadata metadata) { this.cursor = cursor; this.metadata = metadata; } @Override public boolean hasNext() { return cursor.hasNext(); } @Override public Tuple next() { DBObject dbObject = cursor.next(); return new Tuple(new MongoDBTupleSnapshot(dbObject, metadata, UPDATE)); } @Override public void remove() { cursor.remove(); } @Override public void close() { cursor.close(); } } private static class BatchInsertionTask { private final EntityKeyMetadata entityKeyMetadata; private final Map<EntityKey, DBObject> inserts; private final WriteConcern writeConcern; public BatchInsertionTask(EntityKeyMetadata entityKeyMetadata, WriteConcern writeConcern) { this.entityKeyMetadata = entityKeyMetadata; this.inserts = new HashMap<EntityKey, DBObject>(); this.writeConcern = writeConcern; } public EntityKeyMetadata getEntityKeyMetadata() { return entityKeyMetadata; } public List<DBObject> getAll() { return new ArrayList<DBObject>(inserts.values()); } public DBObject get(EntityKey entityKey) { return inserts.get(entityKey); } public boolean containsKey(EntityKey entityKey) { return inserts.containsKey(entityKey); } public DBObject remove(EntityKey entityKey) { return inserts.remove(entityKey); } public void put(EntityKey entityKey, DBObject object) { inserts.put(entityKey, object); } public WriteConcern getWriteConcern() { return writeConcern; } public boolean isEmpty() { return inserts.isEmpty(); } } }