Skip to content

Commit

Permalink
OGM-887 Don't map elements within embeddable collections using dot names
Browse files Browse the repository at this point in the history
This change also removes common prefixes from the JSON representation that is stored within Redis
  • Loading branch information
mp911de committed Aug 5, 2015
1 parent a904c32 commit a04ad99
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 29 deletions.
@@ -0,0 +1,95 @@
/*
* 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.dialect.impl;

import java.util.Map;
import java.util.regex.Pattern;

/**
* Provides functionality for dealing with (nested) fields of Map documents.
*
* @author Alan Fitton &lt;alan at eth0.org.uk&gt;
* @author Emmanuel Bernard &lt;emmanuel@hibernate.org&gt;
* @author Gunnar Morling
*/
public class DotPatternMapHelpers {

private static final Pattern DOT_SEPARATOR_PATTERN = Pattern.compile( "\\." );

/**
* Remove a column from the Map
*
* @param entity the {@link Map} with the column
* @param column the column to remove
*/
public static void resetValue(Map<?,?> entity, String column) {
// fast path for non-embedded case
if ( !column.contains( "." ) ) {
entity.remove( column );
}
else {
String[] path = DOT_SEPARATOR_PATTERN.split( column );
Object field = entity;
int size = path.length;
for (int index = 0 ; index < size ; index++) {
String node = path[index];
Map parent = (Map) field;
field = parent.get( node );
if ( field == null && index < size - 1 ) {
//TODO clean up the hierarchy of empty containers
// no way to reach the leaf, nothing to do
return;
}
if ( index == size - 1 ) {
parent.remove( node );
}
}
}
}

public static boolean hasField(Map entity, String dotPath) {
return getValueOrNull( entity, dotPath ) != null;
}

public static <T> T getValueOrNull(Map entity, String dotPath, Class<T> type) {
Object value = getValueOrNull( entity, dotPath );
return type.isInstance( value ) ? type.cast( value ) : null;
}

public static Object getValueOrNull(Map entity, String dotPath) {
// fast path for simple properties
if ( !dotPath.contains( "." ) ) {
return entity.get( dotPath );
}

String[] path = DOT_SEPARATOR_PATTERN.split( dotPath );
int size = path.length;

for (int index = 0 ; index < size - 1; index++) {
Object next = entity.get( path[index] );
if ( next == null || !( next instanceof Map ) ) {
return null;
}
entity = (Map) next;
}

String field = path[size - 1];
return entity.get( field );
}

/**
* Links the two field names into a single left.right field name.
* If the left field is empty, right is returned
*
* @param left one field name
* @param right the other field name
* @return left.right or right if left is an empty string
*/
public static String flatten(String left, String right) {
return left == null || left.isEmpty() ? right : left + "." + right;
}
}
Expand Up @@ -16,6 +16,7 @@
import java.util.Set;
import java.util.TreeMap;

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.MapHelpers;
Expand Down Expand Up @@ -324,35 +325,56 @@ else if ( currentTtl != null && currentTtl > 0 ) {
}
}

private List<Object> getAssociationRows(
org.hibernate.ogm.model.spi.Association association,
AssociationKey associationKey) {
/**
* 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 Entity}s with keys/values for all row key columns which are not part of the association key</li>
* </ul>
*/
private List<Object> getAssociationRows(org.hibernate.ogm.model.spi.Association association, AssociationKey key) {
List<Object> rows = new ArrayList<Object>( association.size() );

for ( RowKey rowKey : association.getKeys() ) {
Tuple tuple = association.get( rowKey );
rows.add( getAssociationRow( association.get( rowKey ), key ) );
}

String[] columnsToPersist = associationKey.getMetadata()
.getColumnsWithoutKeyColumns( tuple.getColumnNames() );
return rows;
}

// return value itself if there is only a single column to store
if ( columnsToPersist.length == 1 ) {
Object row = tuple.get( columnsToPersist[0] );
rows.add( row );
}
else {
Map<String, Object> row = new HashMap<String, Object>( columnsToPersist.length );
for ( String columnName : columnsToPersist ) {
Object value = tuple.get( columnName );
if ( value != null ) {
row.put( columnName, value );
}
}
private Object getAssociationRow(Tuple row, AssociationKey associationKey) {
String[] columnsToPersist = associationKey.getMetadata()
.getColumnsWithoutKeyColumns( row.getColumnNames() );

rows.add( row );
// return value itself if there is only a single column to store
if ( columnsToPersist.length == 1 ) {
return row.get( columnsToPersist[0] );
}
Entity rowObject = new Entity();
String prefix = getColumnSharedPrefixOfAssociatedEntityLink( associationKey );
for ( String column : columnsToPersist ) {
Object value = row.get( column );
if ( value != null ) {
String columnName = column.startsWith( prefix ) ? column.substring( prefix.length() ) : column;
rowObject.set( columnName, value );
}
}
return rows;

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
Expand Down
Expand Up @@ -8,13 +8,15 @@

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.ogm.datastore.document.association.spi.AssociationRow.AssociationRowAccessor;
import org.hibernate.ogm.datastore.document.association.spi.AssociationRowFactory;
import org.hibernate.ogm.datastore.document.association.spi.StructureOptimizerAssociationRowFactory;
import org.hibernate.ogm.dialect.impl.DotPatternMapHelpers;

/**
* {@link AssociationRowFactory} which creates association rows based on the map based representation used in Redis.
Expand All @@ -40,7 +42,10 @@ protected Map<String, Object> getSingleColumnRow(String columnName, Object value
protected AssociationRowAccessor<Map<String, Object>> getAssociationRowAccessor(
String[] prefixedColumns,
String prefix) {
return RedisAssociationRowAccessor.INSTANCE;

return prefix != null ?
new RedisAssociationRowAccessor( prefixedColumns, prefix ) :
RedisAssociationRowAccessor.INSTANCE;
}

private static class RedisAssociationRowAccessor implements AssociationRowAccessor<Map<String, Object>> {
Expand Down Expand Up @@ -71,7 +76,8 @@ private String unprefix(String prefixedColumn) {

@Override
public Set<String> getColumnNames(Map<String, Object> row) {
Set<String> columnNames = row.keySet();
Set<String> columnNames = new HashSet<>( row.keySet() );
addColumnNames( row, columnNames, "" );
for ( String prefixedColumn : prefixedColumns ) {
String unprefixedColumn = unprefix( prefixedColumn );
if ( columnNames.contains( unprefixedColumn ) ) {
Expand All @@ -82,12 +88,29 @@ public Set<String> getColumnNames(Map<String, Object> row) {
return columnNames;
}

private void addColumnNames(Map<String, Object> row, Set<String> columnNames, String prefix) {
for ( String field : row.keySet() ) {
Object sub = row.get( field );
if ( sub instanceof Map ) {
addColumnNames( (Map) sub, columnNames, DotPatternMapHelpers.flatten( prefix, field ) );
}
else {
columnNames.add( DotPatternMapHelpers.flatten( prefix, field ) );
}
}
}

@Override
public Object get(Map<String, Object> row, String column) {
if ( prefixedColumns.contains( column ) ) {
column = unprefix( column );
}
return row.get( column );

if ( row.containsKey( column ) ) {
return row.get( column );
}

return DotPatternMapHelpers.getValueOrNull( row, column );
}
}
}
Expand Up @@ -11,6 +11,7 @@
import java.util.Map.Entry;
import java.util.regex.Pattern;

import org.hibernate.ogm.dialect.impl.DotPatternMapHelpers;
import org.hibernate.ogm.model.spi.Tuple;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
Expand Down Expand Up @@ -160,6 +161,11 @@ private void setMapValue(String name, Map<String, Object> value) {

@JsonIgnore
public void removeAssociation(String name) {
properties.remove( name );
if ( properties.containsKey( name ) ) {
properties.remove( name );
}
else {
DotPatternMapHelpers.resetValue( properties, name );
}
}
}
Expand Up @@ -80,8 +80,8 @@ public void testDefaultBiDirManyToOneCompositeKeyTest() throws Exception {

// then
JSONAssert.assertEquals(
"{\"games\":[{\"id.gameSequenceNo\":456,\"id.category\":\"primary\"}," +
"{\"id.gameSequenceNo\":457,\"id.category\":\"primary\"}]," +
"{\"games\":[{\"gameSequenceNo\":456,\"category\":\"primary\"}," +
"{\"gameSequenceNo\":457,\"category\":\"primary\"}]," +
"\"name\":\"Hamburg Court\"}",
representation,
JSONCompareMode.NON_EXTENSIBLE
Expand Down
Expand Up @@ -20,7 +20,6 @@
import org.hibernate.ogm.backendtck.queries.StoryBranch;
import org.hibernate.ogm.backendtck.queries.StoryGame;
import org.hibernate.ogm.utils.OgmTestCase;
import org.junit.Ignore;
import org.junit.Test;

/**
Expand Down Expand Up @@ -195,7 +194,6 @@ public void testEmbeddable() throws Exception {
}

@Test
@Ignore("TODO OGM-887: Fix created and expected mapping")
public void testEmbeddableCollection() throws Exception {
OgmSession session = openSession();
Transaction transaction = session.beginTransaction();
Expand Down

0 comments on commit a04ad99

Please sign in to comment.