/* * 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.embedded.dialect.impl; import static java.util.Collections.singletonMap; import org.hibernate.boot.model.relational.Sequence; import org.hibernate.internal.util.collections.BoundedConcurrentHashMap; import org.hibernate.ogm.datastore.neo4j.dialect.impl.BaseNeo4jSequenceGenerator; import org.hibernate.ogm.datastore.neo4j.dialect.impl.NodeLabel; import org.hibernate.ogm.datastore.neo4j.logging.impl.Log; import org.hibernate.ogm.datastore.neo4j.logging.impl.LoggerFactory; import org.hibernate.ogm.dialect.spi.NextValueRequest; import org.hibernate.ogm.id.impl.OgmSequenceGenerator; import org.hibernate.ogm.id.impl.OgmTableGenerator; 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; import org.neo4j.graphdb.DynamicLabel; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Lock; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.schema.ConstraintDefinition; import org.neo4j.graphdb.schema.ConstraintType; /** * Generates the next value of an id sequence as represented by {@link IdSourceKey}. * <p> * Both, {@link IdSourceType#TABLE} and {@link IdSourceType#SEQUENCE} are supported. For the table strategy, nodes in * the following form are used (the exact property names and the label value can be configured using the options exposed * by {@link OgmTableGenerator}): * * <pre> * (:hibernate_sequences:TABLE_BASED_SEQUENCE { sequence_name = 'ExampleSequence', current_value : 3 }) * </pre> * * For the sequence strategy, nodes in the following form are used (the sequence name can be configured using the option * exposed by {@link OgmSequenceGenerator}): * * <pre> * (:SEQUENCE { sequence_name = 'ExampleSequence', next_val : 3 }) * </pre> * * Sequences are created at startup. * <p> * A write lock is acquired on the node every time the sequence needs to be updated. * * @author Davide D'Alto <davide@hibernate.org> * @author Gunnar Morling */ public class EmbeddedNeo4jSequenceGenerator extends BaseNeo4jSequenceGenerator { private static final Log logger = LoggerFactory.getLogger(); /** * Query for creating SEQUENCE nodes. */ private static final String SEQUENCE_CREATION_QUERY = "MERGE (n:" + NodeLabel.SEQUENCE.name() + " {" + SEQUENCE_NAME_PROPERTY + ": {sequenceName}} ) ON CREATE SET n." + SEQUENCE_VALUE_PROPERTY + " = {initialValue} RETURN n"; /** * Query for retrieving the next value from SEQUENCE nodes. */ private static final String SEQUENCE_VALUE_QUERY = "MATCH (n:" + NodeLabel.SEQUENCE.name() + ") WHERE n." + SEQUENCE_NAME_PROPERTY + " = {sequenceName} RETURN n"; private final BoundedConcurrentHashMap<String, String> queryCache; private final GraphDatabaseService neo4jDb; public EmbeddedNeo4jSequenceGenerator(GraphDatabaseService neo4jDb, int sequenceCacheMaxSize) { this.neo4jDb = neo4jDb; this.queryCache = new BoundedConcurrentHashMap<String, String>( sequenceCacheMaxSize, 20, BoundedConcurrentHashMap.Eviction.LIRS ); } /** * Create the sequence nodes setting the initial value if the node does not exists already. * <p> * All nodes are created inside the same transaction * * @param sequences the generators representing the sequences */ public void createSequences(Iterable<Sequence> sequences) { addUniqueConstraintForSequences(); addSequences( sequences ); } public void createUniqueConstraintsForTableSequences(Iterable<IdSourceKeyMetadata> tableIdGenerators) { Transaction tx = null; try { tx = neo4jDb.beginTx(); for ( IdSourceKeyMetadata idSourceKeyMetadata : tableIdGenerators ) { if ( idSourceKeyMetadata.getType() == IdSourceType.TABLE ) { addUniqueConstraintForTableBasedSequence( idSourceKeyMetadata ); } } tx.success(); } finally { tx.close(); } } private void addUniqueConstraintForSequences() { Transaction tx = null; try { tx = neo4jDb.beginTx(); if ( isMissingUniqueConstraint( NodeLabel.SEQUENCE ) ) { neo4jDb.schema() .constraintFor( NodeLabel.SEQUENCE ) .assertPropertyIsUnique( SEQUENCE_NAME_PROPERTY ) .create(); } tx.success(); } finally { tx.close(); } } /** * Adds a unique constraint to make sure that each node of the same "sequence table" is unique. */ private void addUniqueConstraintForTableBasedSequence(IdSourceKeyMetadata generatorKeyMetadata) { Label generatorKeyLabel = DynamicLabel.label( generatorKeyMetadata.getName() ); if ( isMissingUniqueConstraint( generatorKeyLabel ) ) { neo4jDb.schema().constraintFor( generatorKeyLabel ).assertPropertyIsUnique( generatorKeyMetadata.getKeyColumnName() ).create(); } } private boolean isMissingUniqueConstraint(Label generatorKeyLabel) { Iterable<ConstraintDefinition> constraints = neo4jDb.schema().getConstraints( generatorKeyLabel ); for ( ConstraintDefinition constraint : constraints ) { if ( constraint.isConstraintType( ConstraintType.UNIQUENESS ) ) { return false; } } return true; } /** * Adds a node for each generator of type {@link IdSourceType#SEQUENCE}. Table-based generators are created lazily * at runtime. * * @param identifierGenerators the generators to process */ private void addSequences(Iterable<Sequence> sequences) { Transaction tx = null; try { tx = neo4jDb.beginTx(); for ( Sequence sequence : sequences ) { addSequence( sequence ); } tx.success(); } finally { tx.close(); } } /** * Ex.: * <pre> * MERGE (n:hibernate_sequences:TABLE_BASED_SEQUENCE {sequence_name: {sequenceName}}) ON CREATE SET n.current_value = {initialValue} RETURN n * </pre> */ private void addTableSequence(NextValueRequest request) { IdSourceKeyMetadata idSourceKeyMetadata = request.getKey().getMetadata(); String query = "MERGE (n" + labels( idSourceKeyMetadata.getName(), NodeLabel.TABLE_BASED_SEQUENCE.name() ) + " { " + idSourceKeyMetadata.getKeyColumnName() + ": {" + SEQUENCE_NAME_QUERY_PARAM + "}} ) ON CREATE SET n." + idSourceKeyMetadata.getValueColumnName() + " = {" + INITIAL_VALUE_QUERY_PARAM + "} RETURN n"; neo4jDb.execute( query, params( request ) ); } /** * Ex.: * <pre> * MERGE (n:SEQUENCE {sequence_name: {sequenceName}}) ON CREATE SET n.current_value = {initialValue} RETURN n * </pre> */ private void addSequence(Sequence sequence) { neo4jDb.execute( SEQUENCE_CREATION_QUERY, params( sequence ) ); } /** * Generate the next value in a sequence for a given {@link IdSourceKey}. * * @param request the details about how to obtain the next value * @return the next value in a sequence */ @Override public Long nextValue(NextValueRequest request) { Transaction tx = neo4jDb.beginTx(); Lock lock = null; try { Node sequence = getSequence( request.getKey() ); if ( sequence == null ) { // sequence nodes are expected to have been created up-front if ( request.getKey().getMetadata().getType() == IdSourceType.SEQUENCE ) { throw logger.sequenceNotFound( sequenceName( request.getKey() ) ); } // table sequence nodes (think of them as rows in a generator table) are created upon first usage else { addTableSequence( request ); sequence = getSequence( request.getKey() ); } } lock = tx.acquireWriteLock( sequence ); long nextValue = updateSequenceValue( request.getKey(), sequence, request.getIncrement() ); tx.success(); lock.release(); return nextValue; } finally { tx.close(); } } /** * Given a {@link IdSourceKey}, get the corresponding sequence node. * * @param idSourceKey the {@link IdSourceKey} identifying the sequence * @return the node representing the sequence */ private Node getSequence(IdSourceKey idSourceKey) { String updateSequenceQuery = getQuery( idSourceKey ); Result result = neo4jDb.execute( updateSequenceQuery, singletonMap( SEQUENCE_NAME_QUERY_PARAM, (Object) sequenceName( idSourceKey ) ) ); ResourceIterator<Node> column = result.columnAs( "n" ); Node node = null; if ( column.hasNext() ) { node = column.next(); } column.close(); return node; } private String getQuery(IdSourceKey idSourceKey) { return idSourceKey.getMetadata().getType() == IdSourceType.TABLE ? getTableQuery( idSourceKey ) : SEQUENCE_VALUE_QUERY; } /** * Ex.: * <pre> * MATCH (n:hibernate_sequences:TABLE_BASED_SEQUENCE) WHERE n.sequence_name = {sequenceName} RETURN n * </pre> */ private String getTableQuery(IdSourceKey idSourceKey) { String query = queryCache.get( idSourceKey.getTable() ); if ( query == null ) { query = "MATCH (n" + labels( idSourceKey.getTable(), NodeLabel.TABLE_BASED_SEQUENCE.name() ) + ") WHERE n." + idSourceKey.getMetadata().getKeyColumnName() + " = {" + SEQUENCE_NAME_QUERY_PARAM + "} RETURN n"; String cached = queryCache.putIfAbsent( idSourceKey.getTable(), query ); if ( cached != null ) { query = cached; } } return query; } private Long updateSequenceValue(IdSourceKey idSourceKey, Node sequence, int increment) { String valueProperty = idSourceKey.getMetadata().getType() == IdSourceType.TABLE ? idSourceKey.getMetadata().getValueColumnName() : SEQUENCE_VALUE_PROPERTY; Number currentValue = (Number) sequence.getProperty( valueProperty ); long updatedValue = currentValue.longValue() + increment; sequence.setProperty( valueProperty, updatedValue ); return currentValue.longValue(); } }