/* * 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.neo4j.dialect.impl; import java.util.Collections; import java.util.List; import java.util.Map; import org.hibernate.boot.model.relational.Sequence; import org.hibernate.internal.util.collections.BoundedConcurrentHashMap; import org.hibernate.ogm.datastore.neo4j.logging.impl.Log; import org.hibernate.ogm.datastore.neo4j.logging.impl.LoggerFactory; import org.hibernate.ogm.datastore.neo4j.remote.common.request.impl.RemoteStatement; import org.hibernate.ogm.datastore.neo4j.remote.common.request.impl.RemoteStatements; import org.hibernate.ogm.dialect.spi.NextValueRequest; import org.hibernate.ogm.model.key.spi.IdSourceKey; import org.hibernate.ogm.model.key.spi.IdSourceKeyMetadata; import org.hibernate.ogm.model.key.spi.IdSourceKeyMetadata.IdSourceType; /** * @author Davide D'Alto */ public abstract class RemoteNeo4jSequenceGenerator extends BaseNeo4jSequenceGenerator { /** * Query for creating SEQUENCE nodes. */ protected static final String SEQUENCE_CREATION_QUERY = "MERGE (n:" + NodeLabel.SEQUENCE.name() + " {" + SEQUENCE_NAME_PROPERTY + ": {sequenceName}} )" + " RETURN n"; protected static final String SEQUENCE_LOCK_QUERY = "MATCH (n:" + NodeLabel.SEQUENCE.name() + ")" + " WHERE n." + SEQUENCE_NAME_PROPERTY + " = {sequenceName} " + " SET n.__locked = true " + " RETURN n"; /** * Query for retrieving the next value from SEQUENCE nodes. */ protected static final String SEQUENCE_VALUE_QUERY = "MATCH (n:" + NodeLabel.SEQUENCE.name() + ")" + " WHERE n." + SEQUENCE_NAME_PROPERTY + " = {sequenceName} " + " REMOVE n.__locked " + " SET n." + SEQUENCE_VALUE_PROPERTY + " = coalesce(n." + SEQUENCE_VALUE_PROPERTY + ", {initialValue}) + {increment}" + " RETURN n." + SEQUENCE_VALUE_PROPERTY; private static final Log logger = LoggerFactory.getLogger(); private final BoundedConcurrentHashMap<String, RemoteStatements> queryCache; public RemoteNeo4jSequenceGenerator(int sequenceCacheMaxSize) { this.queryCache = new BoundedConcurrentHashMap<String, RemoteStatements>( sequenceCacheMaxSize, 20, BoundedConcurrentHashMap.Eviction.LIRS ); } /** * Generate the next value in a sequence for a given {@link IdSourceKey}. * * @return the next value in a sequence */ @Override public Long nextValue(NextValueRequest request) { String sequenceName = sequenceName( request.getKey() ); // This method return 2 statements: the first one to acquire a lock and the second one to update the sequence node RemoteStatements remoteStatements = updateNextValueQuery( request ); Number nextValue = nextValue( remoteStatements ); // sequence nodes are expected to have been created up-front if ( request.getKey().getMetadata().getType() == IdSourceType.SEQUENCE ) { if ( nextValue == null ) { throw logger.sequenceNotFound( sequenceName ); } } // The only way I found to make it work in a multi-threaded environment is to first increment the value and then read it. // Our API allows for an initial value and to make sure that I'm actually reading the correct one, // the first time I need to decrement the value I obtain from the db. return nextValue.longValue() - request.getIncrement(); } protected abstract Number nextValue(RemoteStatements remoteStatements); protected String increaseQuery(NextValueRequest request) { StringBuilder queryBuilder = new StringBuilder(); IdSourceKeyMetadata metadata = request.getKey().getMetadata(); queryBuilder.append( " MATCH (n " ); queryBuilder.append( labels( metadata.getName(), NodeLabel.TABLE_BASED_SEQUENCE.name() ) ); queryBuilder.append( " { " ); queryBuilder.append( metadata.getKeyColumnName() ); queryBuilder.append( ": {" ); queryBuilder.append( SEQUENCE_NAME_QUERY_PARAM ); queryBuilder.append( "}} )" ); queryBuilder.append( " SET n." ); queryBuilder.append( metadata.getValueColumnName() ); queryBuilder.append( " = n." ); queryBuilder.append( metadata.getValueColumnName() ); queryBuilder.append( " + " ); queryBuilder.append( request.getIncrement() ); queryBuilder.append( " REMOVE n.__locked RETURN n." ); queryBuilder.append( metadata.getValueColumnName() ); String query = queryBuilder.toString(); return query; } protected String acquireLockQuery(NextValueRequest request) { StringBuilder queryBuilder = new StringBuilder(); IdSourceKeyMetadata metadata = request.getKey().getMetadata(); queryBuilder.append( "MERGE (n" ); queryBuilder.append( labels( metadata.getName(), NodeLabel.TABLE_BASED_SEQUENCE.name() ) ); queryBuilder.append( " { " ); queryBuilder.append( metadata.getKeyColumnName() ); queryBuilder.append( ": {" ); queryBuilder.append( SEQUENCE_NAME_QUERY_PARAM ); queryBuilder.append( "}} ) " ); queryBuilder.append( " ON MATCH SET n.__locked=true " ); queryBuilder.append( " ON CREATE SET n.__locked=true, n." ); queryBuilder.append( metadata.getValueColumnName() ); queryBuilder.append( " = " ); queryBuilder.append( request.getInitialValue() ); queryBuilder.append( " RETURN n." ); queryBuilder.append( metadata.getValueColumnName() ); String query = queryBuilder.toString(); return query; } /* * This will always return 2 statements: the first one to acquire a lock and the second one to update the sequence value */ protected RemoteStatements updateNextValueQuery(NextValueRequest request) { return request.getKey().getMetadata().getType() == IdSourceType.TABLE ? getTableQuery( request ) : getSequenceIncrementQuery( request ); } private RemoteStatements getSequenceIncrementQuery(NextValueRequest request) { // Acquire a lock on the node String sequenceName = sequenceName( request.getKey() ); RemoteStatement lockStatement = new RemoteStatement( SEQUENCE_LOCK_QUERY, Collections.<String, Object>singletonMap( SEQUENCE_NAME_QUERY_PARAM, sequenceName ) ); RemoteStatements statements = new RemoteStatements(); statements.addStatement( lockStatement ); // Increment the value on the node String query = SEQUENCE_VALUE_QUERY.replace( "{increment}", String.valueOf( request.getIncrement() ) ).replace( "{initialValue}", String.valueOf( request.getInitialValue() ) ); RemoteStatement statement = new RemoteStatement( query, params( request ), true ); statements.addStatement( statement ); return statements; } private RemoteStatements getTableQuery(NextValueRequest request) { String key = key( request ); RemoteStatements statements = queryCache.get( key ); if ( statements == null ) { statements = new RemoteStatements(); getUpdateTableSequenceQuery( statements, request ); RemoteStatements cached = queryCache.putIfAbsent( key, statements ); if ( cached != null ) { statements = cached; } } return statements; } private void getUpdateTableSequenceQuery(RemoteStatements statements, NextValueRequest request) { Map<String, Object> params = params( request ); // Acquire lock String acquireLockQuery = acquireLockQuery( request ); RemoteStatement acquireLockStatement = new RemoteStatement( acquireLockQuery, params, true ); statements.addStatement( acquireLockStatement ); // Update value String updateQuery = increaseQuery( request ); RemoteStatement updateStatement = new RemoteStatement( updateQuery, params, true ); statements.addStatement( updateStatement ); } public abstract void createSequences(List<Sequence> sequences, Iterable<IdSourceKeyMetadata> idSourceKeyMetadata); }