/*
* 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 org.hibernate.ogm.datastore.neo4j.dialect.impl.NodeLabel.EMBEDDED;
import static org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL.escapeIdentifier;
import static org.hibernate.ogm.util.impl.EmbeddedHelper.isPartOfEmbedded;
import static org.hibernate.ogm.util.impl.EmbeddedHelper.split;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.hibernate.ogm.datastore.neo4j.dialect.impl.BaseNeo4jAssociationQueries;
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.RowKey;
import org.hibernate.ogm.util.impl.ArrayHelper;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Result;
/**
* Container for the queries related to one association family in Neo4j. Unfortunately, we cannot use the same queries
* for all associations, as Neo4j does not allow to parameterize on node labels which would be required, as the
* association table is stored as a label.
*
* @author Davide D'Alto
*/
public class EmbeddedNeo4jAssociationQueries extends BaseNeo4jAssociationQueries {
public EmbeddedNeo4jAssociationQueries(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) {
super( ownerEntityKeyMetadata, associationKeyMetadata );
}
/**
* Removes the relationship(s) representing the given association. If the association refers to an embedded entity
* (collection), the referenced entities are removed as well.
*
* @param executionEngine the {@link GraphDatabaseService} used to run the query
* @param associationKey represents the association
*/
@Override
public void removeAssociation(GraphDatabaseService executionEngine, AssociationKey associationKey) {
executionEngine.execute( removeAssociationQuery, params( associationKey.getEntityKey().getColumnValues() ) );
}
/**
* Returns the relationship corresponding to the {@link AssociationKey} and {@link RowKey}.
*
* @param executionEngine the {@link GraphDatabaseService} used to run the query
* @param associationKey represents the association
* @param rowKey represents a row in an association
* @return the corresponding relationship
*/
@Override
public Relationship findRelationship(GraphDatabaseService executionEngine, AssociationKey associationKey, RowKey rowKey) {
Object[] queryValues = relationshipValues( associationKey, rowKey );
Result result = executionEngine.execute( findRelationshipQuery, params( queryValues ) );
return singleResult( result );
}
@Override
protected Object[] relationshipValues(AssociationKey associationKey, RowKey rowKey) {
Object[] relationshipValues;
if ( associationKey.getMetadata().getRowKeyIndexColumnNames().length > 0 ) {
int length = associationKey.getMetadata().getRowKeyIndexColumnNames().length;
relationshipValues = new Object[length];
String[] indexColumnNames = associationKey.getMetadata().getRowKeyIndexColumnNames();
for ( int i = 0; i < indexColumnNames.length; i++ ) {
for ( int j = 0; j < rowKey.getColumnNames().length; j++ ) {
if ( indexColumnNames[i].equals( rowKey.getColumnNames()[j] ) ) {
relationshipValues[i] = rowKey.getColumnValues()[j];
}
}
}
}
else {
relationshipValues = getEntityKey( associationKey, rowKey ).getColumnValues();
}
Object[] queryValues = ArrayHelper.concat( associationKey.getEntityKey().getColumnValues(), relationshipValues );
return queryValues;
}
/**
* Remove an association row
*
* @param executionEngine the {@link GraphDatabaseService} used to run the query
* @param associationKey represents the association
* @param rowKey represents a row in an association
*/
@Override
public void removeAssociationRow(GraphDatabaseService executionEngine, AssociationKey associationKey, RowKey rowKey) {
Object[] queryValues = relationshipValues( associationKey, rowKey );
executionEngine.execute( removeAssociationRowQuery, params( queryValues ) );
}
/**
* Returns the entity key on the other side of association row represented by the given row key.
* <p>
* <b>Note:</b> May only be invoked if the row key actually contains all the columns making up that entity key.
* Specifically, it may <b>not</b> be invoked if the association has index columns (maps, ordered collections), as
* the entity key columns will not be part of the row key in this case.
*/
private EntityKey getEntityKey(AssociationKey associationKey, RowKey rowKey) {
String[] associationKeyColumns = associationKey.getMetadata().getAssociatedEntityKeyMetadata().getAssociationKeyColumns();
Object[] columnValues = new Object[associationKeyColumns.length];
int i = 0;
for ( String associationKeyColumn : associationKeyColumns ) {
columnValues[i] = rowKey.getColumnValue( associationKeyColumn );
i++;
}
EntityKeyMetadata entityKeyMetadata = associationKey.getMetadata().getAssociatedEntityKeyMetadata().getEntityKeyMetadata();
return new EntityKey( entityKeyMetadata, columnValues );
}
/**
* Give an embedded association, creates all the nodes and relationships required to represent it.
* It assumes that the entity node containing the association already exists in the db.
*
* @param executionEngine the {@link GraphDatabaseService} to run the query
* @param associationKey the {@link AssociationKey} identifying the association
* @param embeddedKey the {@link EntityKey} identifying the embedded component
* @return the created {@link Relationship} that represents the association
*/
public Relationship createRelationshipForEmbeddedAssociation(GraphDatabaseService executionEngine, AssociationKey associationKey, EntityKey embeddedKey) {
String query = initCreateEmbeddedAssociationQuery( associationKey, embeddedKey );
Object[] queryValues = createRelationshipForEmbeddedQueryValues( associationKey, embeddedKey );
return executeQuery( executionEngine, query, queryValues );
}
@Override
protected String initCreateEmbeddedAssociationQuery(AssociationKey associationKey, EntityKey embeddedKey) {
String collectionRole = associationKey.getMetadata().getCollectionRole();
String[] embeddedColumnNames = embeddedKey.getColumnNames();
Object[] embeddedColumnValues = embeddedKey.getColumnValues();
String[] columnNames = associationKey.getEntityKey().getMetadata().getColumnNames();
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append( matchOwnerEntityNode );
if ( isCollectionOfPrimitives( collectionRole, embeddedColumnNames ) ) {
createRelationshipForCollectionOfPrimitivesOrMap( associationKey, collectionRole, columnNames, queryBuilder );
}
else {
createRelationshipforCollectionOfComponents( associationKey, collectionRole, embeddedColumnNames, embeddedColumnValues, queryBuilder );
}
return queryBuilder.toString();
}
protected Object[] createRelationshipForEmbeddedQueryValues(AssociationKey associationKey, EntityKey embeddedKey) {
String collectionRole = associationKey.getMetadata().getCollectionRole();
Object[] columnValues = associationKey.getEntityKey().getColumnValues();
if ( isCollectionOfPrimitives( collectionRole, embeddedKey.getColumnNames() ) ) {
return ArrayHelper.concat( columnValues, embeddedKey.getColumnValues()[0] );
}
else {
return ArrayHelper.concat( columnValues, embeddedKey.getColumnValues() );
}
}
private boolean isCollectionOfPrimitives(String collectionRole, String[] embeddedColumnNames) {
return embeddedColumnNames.length == 1 && collectionRole.equals( embeddedColumnNames[0] );
}
/*
* Example 1:
* MATCH (owner:ENTITY:MultiAddressAccount {login: {0}})
* CREATE (owner) -[r:addresses]-> (target:EMBEDDED:`MultiAddressAccount_addresses` {city: {1}, country: {2}, street1: {3}, `postal_code`: {6}})
* RETURN r
*
* Example 2:
* MATCH (owner:ENTITY:StoryGame {id: {0}}) - [:goodBranch] -> (e:EMBEDDED)
* CREATE (e) -[r:additionalEndings]-> (target:EMBEDDED:`StoryGame_goodBranch.additionalEndings` {score: {1}, text: {2}})
*/
private void createRelationshipforCollectionOfComponents(AssociationKey associationKey, String collectionRole, String[] embeddedColumnNames, Object[] embeddedColumnValues, StringBuilder queryBuilder) {
int offset = associationKey.getEntityKey().getColumnNames().length;
EmbeddedNodesTree tree = createEmbeddedTree( collectionRole, embeddedColumnNames, embeddedColumnValues, offset );
if ( isPartOfEmbedded( collectionRole ) ) {
String[] pathToEmbedded = appendEmbeddedNodes( collectionRole, queryBuilder );
queryBuilder.append( " CREATE (e) -[r:" );
appendRelationshipType( queryBuilder, pathToEmbedded[ pathToEmbedded.length - 1] );
}
else {
queryBuilder.append( " CREATE (owner) -[r:" );
appendRelationshipType( queryBuilder, collectionRole );
}
queryBuilder.append( "]-> " );
queryBuilder.append( "(target:" );
queryBuilder.append( EMBEDDED );
queryBuilder.append( ":" );
escapeIdentifier( queryBuilder, associationKey.getMetadata().getAssociatedEntityKeyMetadata().getEntityKeyMetadata().getTable() );
int index = 0;
int embeddedNumber = 0;
// Append primitive properties
if ( !tree.getProperties().isEmpty() ) {
queryBuilder.append( " {" );
for ( EmbeddedNodeProperty property : tree.getProperties() ) {
escapeIdentifier( queryBuilder, property.getColumn() );
queryBuilder.append( ": {" );
queryBuilder.append( property.getParam() );
queryBuilder.append( "}" );
if ( index++ < tree.getProperties().size() - 1 ) {
queryBuilder.append( ", " );
}
}
queryBuilder.append( "}" );
}
queryBuilder.append( ")" );
// Append relationships representing embedded properties
Map<String, EmbeddedNodesTree> children = tree.getChildren();
boolean first = true;
for ( Entry<String, EmbeddedNodesTree> entry : children.entrySet() ) {
index = 0;
String relationshipType = entry.getKey();
EmbeddedNodesTree child = entry.getValue();
if ( first ) {
first = false;
}
else {
queryBuilder.append( ", (target)" );
}
queryBuilder.append( " - [:" );
appendRelationshipType( queryBuilder, relationshipType );
queryBuilder.append( "] -> " );
queryBuilder.append( "(a" );
queryBuilder.append( embeddedNumber++ );
queryBuilder.append( ":" );
queryBuilder.append( EMBEDDED );
if ( !child.getProperties().isEmpty() ) {
queryBuilder.append( " {" );
for ( EmbeddedNodeProperty property : child.getProperties() ) {
escapeIdentifier( queryBuilder, property.getColumn() );
queryBuilder.append( ": {" );
queryBuilder.append( property.getParam() );
queryBuilder.append( "}" );
if ( index++ < child.getProperties().size() - 1 ) {
queryBuilder.append( ", " );
}
}
queryBuilder.append( "}" );
}
queryBuilder.append( ")" );
}
queryBuilder.append( " RETURN r" );
}
/*
* Append query part related to the creation of a relationship for a collection of primitive or a Map like in the following examples:
*
* 1) @ElemntCollection List<String> alternatives
* 2) @MapKeyColumn(name = "addressType") Map<String, Address> addresses
* Query example for embedded collection of primitives or Map:
*
* MATCH (owner:ENTITY:table {id: {0}})
* MERGE (owner) -[:relType]-> (e0:EMBEDDED) -[:relType2]-> (e:EMBEDDED)
* CREATE (e) -[r:relType2]-> (new:EMBEDDED { property: {1}})
* RETURN r
*
* MATCH (owner:ENTITY:table {id: {0}})
* CREATE (owner) -[r:relType2]-> (target:EMBEDDED { property: {1}})
* RETURN r
*/
private void createRelationshipForCollectionOfPrimitivesOrMap(AssociationKey associationKey, String collectionRole, String[] columnNames, StringBuilder queryBuilder) {
String relationshipType = collectionRole;
if ( isPartOfEmbedded( collectionRole ) ) {
queryBuilder.append( " MERGE (owner) " );
String[] pathToEmbedded = appendEmbeddedNodes( collectionRole, queryBuilder );
relationshipType = pathToEmbedded[pathToEmbedded.length - 1];
queryBuilder.append( " CREATE (e) -[r:" );
}
else {
queryBuilder.append( " CREATE (owner) -[r:" );
}
escapeIdentifier( queryBuilder, relationshipType );
queryBuilder.append( "]->(new:" );
queryBuilder.append( EMBEDDED );
queryBuilder.append( ":" );
escapeIdentifier( queryBuilder, associationKey.getTable() );
queryBuilder.append( " {" );
// THe name of the property is the same as the relationship type
escapeIdentifier( queryBuilder, relationshipType );
queryBuilder.append( ": {" );
queryBuilder.append( columnNames.length );
queryBuilder.append( "}" );
queryBuilder.append( "}" );
queryBuilder.append( ")" );
queryBuilder.append( " RETURN r" );
}
private Relationship executeQuery(GraphDatabaseService executionEngine, String query, Object[] queryValues) {
Map<String, Object> params = params( queryValues );
Result result = executionEngine.execute( query, params );
return singleResult( result );
}
/*
* If the association is connected to embedded elements we also need to create the embedded relationships for this elements.
* This method will create an tree containing the information about the path to the embedded in a more managabel way.
*/
private EmbeddedNodesTree createEmbeddedTree(String collectionRole, String[] embeddedColumnNames, Object[] embeddedColumnValues, int offset) {
EmbeddedNodesTree tree = new EmbeddedNodesTree();
for ( int i = 0; i < embeddedColumnNames.length; i++ ) {
String embeddedColumnName;
if ( embeddedColumnNames[i].startsWith( collectionRole ) ) {
embeddedColumnName = embeddedColumnNames[i].substring( collectionRole.length() + 1 );
}
else {
embeddedColumnName = embeddedColumnNames[i];
}
if ( embeddedColumnValues[i] != null ) {
if ( embeddedColumnName.contains( "." ) ) {
int firstDot = embeddedColumnName.indexOf( "." );
String relationshipType = embeddedColumnName.substring( 0, firstDot );
String currentProperty = embeddedColumnName.substring( firstDot + 1 );
appendSubTree( tree, currentProperty, relationshipType, offset + i );
}
else {
EmbeddedNodeProperty property = new EmbeddedNodeProperty();
property.setParam( offset + i );
property.setColumn( embeddedColumnName );
tree.addProperty( property );
}
}
}
return tree;
}
private void appendSubTree(EmbeddedNodesTree tree, String currentProperty, String relationshipType, int index) {
EmbeddedNodesTree subTree = tree.getChild( relationshipType );
if ( subTree == null ) {
subTree = new EmbeddedNodesTree();
tree.addChild( relationshipType, subTree );
}
if ( isPartOfEmbedded( currentProperty ) ) {
int firstDot = currentProperty.indexOf( "." );
String relType = currentProperty.substring( 0, firstDot );
String subProperty = currentProperty.substring( firstDot + 1 );
appendSubTree( tree, subProperty, relType, index );
}
else {
EmbeddedNodeProperty property = new EmbeddedNodeProperty();
property.setColumn( currentProperty );
property.setParam( index );
subTree.addProperty( property );
}
}
/*
* Given an embedded properties path returns the cypher representation that can be appended to a MERGE or CREATE
* query.
*/
private static String[] appendEmbeddedNodes(String path, StringBuilder queryBuilder) {
String[] columns = split( path );
for ( int i = 0; i < columns.length - 1; i++ ) {
queryBuilder.append( " - [:" );
appendRelationshipType( queryBuilder, columns[i] );
queryBuilder.append( "] ->" );
if ( i < columns.length - 2 ) {
queryBuilder.append( " (e" );
queryBuilder.append( i );
queryBuilder.append( ":" );
queryBuilder.append( EMBEDDED );
queryBuilder.append( ") MERGE (e" );
queryBuilder.append( i );
queryBuilder.append( ")" );
}
}
queryBuilder.append( " (e:" );
queryBuilder.append( EMBEDDED );
queryBuilder.append( ")" );
return columns;
}
private static class EmbeddedNodesTree {
final List<EmbeddedNodeProperty> properties = new ArrayList<EmbeddedNodeProperty>();
final Map<String, EmbeddedNodesTree> children = new HashMap<String, EmbeddedNodesTree>();
public void addProperty(EmbeddedNodeProperty property) {
properties.add( property );
}
public List<EmbeddedNodeProperty> getProperties() {
return properties;
}
public void addChild(String relationshipType, EmbeddedNodesTree subTree) {
children.put( relationshipType, subTree );
}
public EmbeddedNodesTree getChild(String relationshipType) {
return children.get( relationshipType );
}
public Map<String, EmbeddedNodesTree> getChildren() {
return children;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append( "EmbeddedTree [properties=" );
builder.append( properties );
builder.append( ", children=" );
builder.append( children );
builder.append( "]" );
return builder.toString();
}
}
private class EmbeddedNodeProperty {
private String column;
private int param;
public String getColumn() {
return column;
}
public void setColumn(String column) {
this.column = column;
}
public int getParam() {
return param;
}
public void setParam(int param) {
this.param = param;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append( "[" );
builder.append( column );
builder.append( ", " );
builder.append( param );
builder.append( "]" );
return builder.toString();
}
}
}