org.hibernate.ogm.datastore.couchdb.CouchDBDialect Maven / Gradle / Ivy
/*
* 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 .
*/
package org.hibernate.ogm.datastore.couchdb;
import static org.hibernate.ogm.datastore.document.impl.DotPatternMapHelpers.getColumnSharedPrefixOfAssociatedEntityLink;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.OptimisticLockException;
import org.hibernate.ogm.datastore.couchdb.dialect.backend.impl.CouchDBDatastore;
import org.hibernate.ogm.datastore.couchdb.dialect.backend.json.impl.AssociationDocument;
import org.hibernate.ogm.datastore.couchdb.dialect.backend.json.impl.Document;
import org.hibernate.ogm.datastore.couchdb.dialect.backend.json.impl.EntityDocument;
import org.hibernate.ogm.datastore.couchdb.dialect.impl.CouchDBTuplesSupplier;
import org.hibernate.ogm.datastore.couchdb.dialect.model.impl.CouchDBAssociation;
import org.hibernate.ogm.datastore.couchdb.dialect.model.impl.CouchDBAssociationSnapshot;
import org.hibernate.ogm.datastore.couchdb.dialect.model.impl.CouchDBTupleSnapshot;
import org.hibernate.ogm.datastore.couchdb.dialect.type.impl.CouchDBBlobType;
import org.hibernate.ogm.datastore.couchdb.dialect.type.impl.CouchDBByteType;
import org.hibernate.ogm.datastore.couchdb.dialect.type.impl.CouchDBLongType;
import org.hibernate.ogm.datastore.couchdb.dialect.type.impl.CouchDBStringType;
import org.hibernate.ogm.datastore.couchdb.impl.CouchDBDatastoreProvider;
import org.hibernate.ogm.datastore.couchdb.util.impl.Identifier;
import org.hibernate.ogm.datastore.document.impl.DotPatternMapHelpers;
import org.hibernate.ogm.datastore.document.impl.EmbeddableStateFinder;
import org.hibernate.ogm.datastore.document.options.AssociationStorageType;
import org.hibernate.ogm.datastore.document.options.spi.AssociationStorageOption;
import org.hibernate.ogm.dialect.batch.spi.GroupedChangesToEntityOperation;
import org.hibernate.ogm.dialect.batch.spi.GroupingByEntityDialect;
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.RemoveAssociationOperation;
import org.hibernate.ogm.dialect.impl.AbstractGroupingByEntityDialect;
import org.hibernate.ogm.dialect.spi.AssociationContext;
import org.hibernate.ogm.dialect.spi.AssociationTypeContext;
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.OperationContext;
import org.hibernate.ogm.dialect.spi.TupleAlreadyExistsException;
import org.hibernate.ogm.dialect.spi.TupleContext;
import org.hibernate.ogm.dialect.spi.TupleTypeContext;
import org.hibernate.ogm.entityentry.impl.TuplePointer;
import org.hibernate.ogm.model.key.spi.AssociationKey;
import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata;
import org.hibernate.ogm.model.key.spi.AssociationKind;
import org.hibernate.ogm.model.key.spi.AssociationType;
import org.hibernate.ogm.model.key.spi.EntityKey;
import org.hibernate.ogm.model.key.spi.EntityKeyMetadata;
import org.hibernate.ogm.model.key.spi.RowKey;
import org.hibernate.ogm.model.spi.Association;
import org.hibernate.ogm.model.spi.Tuple;
import org.hibernate.ogm.model.spi.Tuple.SnapshotType;
import org.hibernate.ogm.model.spi.TupleOperation;
import org.hibernate.ogm.options.spi.OptionsContext;
import org.hibernate.ogm.type.impl.Iso8601StringCalendarType;
import org.hibernate.ogm.type.impl.Iso8601StringDateType;
import org.hibernate.ogm.type.impl.SerializableAsStringType;
import org.hibernate.ogm.type.impl.StringType;
import org.hibernate.ogm.type.spi.GridType;
import org.hibernate.type.BinaryType;
import org.hibernate.type.SerializableToBlobType;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.Type;
/**
* Stores tuples and associations as JSON documents inside CouchDB.
*
* Tuples are stored in CouchDB documents obtained as a JSON serialization of a {@link EntityDocument} object.
* Associations are stored in CouchDB documents obtained as a JSON serialization of a {@link AssociationDocument} object.
*
* @author Andrea Boriero <[email protected]>
* @author Gunnar Morling
* @author Guillaume Smet
*/
public class CouchDBDialect extends AbstractGroupingByEntityDialect implements GroupingByEntityDialect {
private final CouchDBDatastoreProvider provider;
public CouchDBDialect(CouchDBDatastoreProvider provider) {
this.provider = provider;
}
@Override
public Tuple getTuple(EntityKey key, OperationContext operationContext) {
EntityDocument entity = getDataStore().getEntity( Identifier.createEntityId( key ) );
if ( entity != null ) {
return new Tuple( new CouchDBTupleSnapshot( entity ), SnapshotType.UPDATE );
}
else if ( isInTheInsertionQueue( key, operationContext ) ) {
return createTuple( key, operationContext );
}
else {
return null;
}
}
@Override
public Tuple createTuple(EntityKey key, OperationContext operationContext) {
return new Tuple( new CouchDBTupleSnapshot( new EntityDocument( key ) ), SnapshotType.INSERT );
}
@Override
public void executeGroupedChangesToEntity(GroupedChangesToEntityOperation groupedOperation) {
EntityKey entityKey = groupedOperation.getEntityKey();
EntityDocument owningEntity = null;
List associationsToRemove = new ArrayList<>();
OptionsContext optionsContext = null;
SnapshotType snapshotType = SnapshotType.UPDATE;
for ( Operation operation : groupedOperation.getOperations() ) {
if ( operation instanceof InsertOrUpdateTupleOperation ) {
InsertOrUpdateTupleOperation insertOrUpdateTupleOperation = (InsertOrUpdateTupleOperation) operation;
Tuple tuple = insertOrUpdateTupleOperation.getTuplePointer().getTuple();
TupleContext tupleContext = insertOrUpdateTupleOperation.getTupleContext();
if ( SnapshotType.INSERT.equals( tuple.getSnapshotType() ) ) {
snapshotType = SnapshotType.INSERT;
}
if ( owningEntity == null ) {
owningEntity = getEntityFromTuple( tuple );
}
String revision = (String) tuple.getSnapshot().get( Document.REVISION_FIELD_NAME );
// load the latest revision for updates without the revision being present; a warning about
// this mapping will have been issued at factory start-up
if ( revision == null && !SnapshotType.INSERT.equals( snapshotType ) ) {
owningEntity.setRevision( getDataStore().getCurrentRevision( Identifier.createEntityId( entityKey ), false ) );
}
EmbeddableStateFinder embeddableStateFinder = new EmbeddableStateFinder( tuple, tupleContext );
for ( TupleOperation tupleOperation : tuple.getOperations() ) {
String column = tupleOperation.getColumn();
if ( entityKey.getMetadata().isKeyColumn( column ) ) {
continue;
}
switch ( tupleOperation.getType() ) {
case PUT:
owningEntity.set( column, tupleOperation.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
owningEntity.unset( nullEmbeddable );
}
else {
// simply unset the column
owningEntity.unset( column );
}
break;
}
}
optionsContext = tupleContext.getTupleTypeContext().getOptionsContext();
}
else if ( operation instanceof InsertOrUpdateAssociationOperation ) {
InsertOrUpdateAssociationOperation insertOrUpdateAssociationOperation = (InsertOrUpdateAssociationOperation) operation;
AssociationKey associationKey = insertOrUpdateAssociationOperation.getAssociationKey();
org.hibernate.ogm.model.spi.Association association = insertOrUpdateAssociationOperation.getAssociation();
AssociationContext associationContext = insertOrUpdateAssociationOperation.getContext();
CouchDBAssociation couchDBAssociation = ( (CouchDBAssociationSnapshot) association.getSnapshot() ).getCouchDbAssociation();
Object rows = getAssociationRows( association, associationKey, associationContext );
couchDBAssociation.setRows( rows );
if ( isStoredInEntityStructure( associationKey.getMetadata(), associationContext.getAssociationTypeContext() ) ) {
if ( owningEntity == null ) {
owningEntity = (EntityDocument) couchDBAssociation.getOwningDocument();
optionsContext = associationContext.getAssociationTypeContext().getHostingEntityOptionsContext();
}
}
else {
// We don't want to remove the association anymore as it's superseded by an update
associationsToRemove.remove( associationKey );
getDataStore().saveDocument( couchDBAssociation.getOwningDocument() );
}
}
else if ( operation instanceof RemoveAssociationOperation ) {
RemoveAssociationOperation removeAssociationOperation = (RemoveAssociationOperation) operation;
AssociationKey associationKey = removeAssociationOperation.getAssociationKey();
AssociationContext associationContext = removeAssociationOperation.getContext();
if ( isStoredInEntityStructure( associationKey.getMetadata(), associationContext.getAssociationTypeContext() ) ) {
if ( owningEntity == null ) {
TuplePointer tuplePointer = getEmbeddingEntityTuplePointer( associationKey, associationContext );
owningEntity = getEntityFromTuple( tuplePointer.getTuple() );
}
if ( owningEntity != null ) {
owningEntity.unset( associationKey.getMetadata().getCollectionRole() );
optionsContext = associationContext.getAssociationTypeContext().getHostingEntityOptionsContext();
}
}
else {
associationsToRemove.add( associationKey );
}
}
else {
throw new IllegalStateException( operation.getClass().getSimpleName() + " not supported here" );
}
}
if ( owningEntity != null ) {
try {
storeEntity( entityKey, owningEntity, optionsContext );
}
catch (OptimisticLockException ole) {
if ( SnapshotType.INSERT.equals( snapshotType ) ) {
throw new TupleAlreadyExistsException( entityKey, ole );
}
else {
throw ole;
}
}
}
if ( associationsToRemove.size() > 0 ) {
removeAssociations( associationsToRemove );
}
}
@Override
public void removeTuple(EntityKey key, TupleContext tupleContext) {
removeDocumentIfPresent( Identifier.createEntityId( key ) );
}
@Override
public Association getAssociation(AssociationKey key, AssociationContext associationContext) {
CouchDBAssociation couchDBAssociation = null;
if ( isStoredInEntityStructure( key.getMetadata(), associationContext.getAssociationTypeContext() ) ) {
TuplePointer tuplePointer = getEmbeddingEntityTuplePointer( key, associationContext );
if ( tuplePointer == null ) {
// The entity associated with this association has already been removed
// see ManyToOneTest#testRemovalOfTransientEntityWithAssociation
return null;
}
EntityDocument owningEntity = getEntityFromTuple( tuplePointer.getTuple() );
if ( owningEntity != null && DotPatternMapHelpers.hasField(
owningEntity.getPropertiesAsHierarchy(),
key.getMetadata().getCollectionRole()
) ) {
couchDBAssociation = CouchDBAssociation.fromEmbeddedAssociation( tuplePointer, key.getMetadata() );
}
}
else {
AssociationDocument association = getDataStore().getAssociation( Identifier.createAssociationId( key ) );
if ( association != null ) {
couchDBAssociation = CouchDBAssociation.fromAssociationDocument( association );
}
}
return couchDBAssociation != null ? new Association( new CouchDBAssociationSnapshot( couchDBAssociation, key ) ) : null;
}
@Override
public Association createAssociation(AssociationKey key, AssociationContext associationContext) {
CouchDBAssociation couchDBAssociation = null;
if ( isStoredInEntityStructure( key.getMetadata(), associationContext.getAssociationTypeContext() ) ) {
TuplePointer tuplePointer = getEmbeddingEntityTuplePointer( key, associationContext );
EntityDocument owningEntity = getEntityFromTuple( tuplePointer.getTuple() );
if ( owningEntity == null ) {
owningEntity = (EntityDocument) getDataStore().saveDocument( new EntityDocument( key.getEntityKey() ) );
tuplePointer.setTuple( new Tuple( new CouchDBTupleSnapshot( owningEntity ), SnapshotType.UPDATE ) );
}
couchDBAssociation = CouchDBAssociation.fromEmbeddedAssociation( tuplePointer, key.getMetadata() );
}
else {
AssociationDocument association = new AssociationDocument( Identifier.createAssociationId( key ) );
couchDBAssociation = CouchDBAssociation.fromAssociationDocument( association );
}
Association association = new Association( new CouchDBAssociationSnapshot( couchDBAssociation, key ) );
// in the case of an association stored in the entity structure, we might end up with rows present in the current snapshot of the entity
// while we want an empty association here. So, in this case, we clear the snapshot to be sure the association created is empty.
if ( !association.isEmpty() ) {
association.clear();
}
return association;
}
private Object getAssociationRows(Association association, AssociationKey associationKey, AssociationContext associationContext) {
boolean organizeByRowKey = DotPatternMapHelpers.organizeAssociationMapByRowKey(
association,
associationKey,
associationContext
);
if ( isStoredInEntityStructure(
associationKey.getMetadata(),
associationContext.getAssociationTypeContext()
) && organizeByRowKey ) {
String rowKeyColumn = organizeByRowKey ? associationKey.getMetadata().getRowKeyIndexColumnNames()[0] : null;
Map rows = new HashMap<>();
for ( RowKey rowKey : association.getKeys() ) {
Map row = (Map) getAssociationRow( association.get( rowKey ), associationKey );
String rowKeyValue = (String) row.remove( rowKeyColumn );
// if there is a single column on the value side left, unwrap it
if ( row.keySet().size() == 1 ) {
rows.put( rowKeyValue, row.values().iterator().next() );
}
else {
rows.put( rowKeyValue, row );
}
}
return rows;
}
List