Skip to content

Commit

Permalink
Hydrate Relationship (yahoo#987) (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellohanchen authored Sep 30, 2019
1 parent 7d5a9f8 commit 6c04150
Show file tree
Hide file tree
Showing 21 changed files with 888 additions and 342 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ public ParseTree getPermissionsForClass(Class<?> resourceClass, Class<? extends
* or {@code null} if the permission is not specified on that field
*/
public ParseTree getPermissionsForField(Class<?> resourceClass,
String field,
Class<? extends Annotation> annotationClass) {
String field,
Class<? extends Annotation> annotationClass) {
EntityBinding binding = getEntityBinding(resourceClass);
return binding.entityPermissions.getFieldChecksForPermission(field, annotationClass);
}
Expand Down
27 changes: 23 additions & 4 deletions elide-datastore/elide-datastore-aggregation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>com.yahoo.elide</groupId>
<artifactId>elide-datastore-parent-pom</artifactId>
<version>4.4.5-SNAPSHOT</version>
<version>4.5.2-SNAPSHOT</version>
</parent>

<licenses>
Expand All @@ -38,6 +38,10 @@
<tag>HEAD</tag>
</scm>

<properties>
<junit.version>5.4.1</junit.version>
</properties>

<dependencies>
<dependency>
<groupId>com.yahoo.elide</groupId>
Expand Down Expand Up @@ -71,9 +75,25 @@
</dependency>

<!-- Test -->
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

Expand All @@ -92,7 +112,6 @@
<scope>test</scope>
</dependency>


</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ public EntityDimension(
/**
* Constructor.
*
* @param schema The schema this {@link Column} belongs to.
* @param dimensionField The entity field or relation that this {@link Dimension} represents
* @param annotation Provides static meta data about this {@link Dimension}
* @param fieldType The Java type for this entity field or relation
* @param dimensionType The physical storage structure backing this {@link Dimension}, such as a table or a column
* @param cardinality The estimated cardinality of this {@link Dimension} in SQL table
* @param friendlyName A human-readable name representing this {@link Dimension}
*
* @throws NullPointerException any argument, except for {@code annotation}, is {@code null}
*/
protected EntityDimension(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright 2019, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.datastores.aggregation.engine;

import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.datastores.aggregation.Query;
import com.yahoo.elide.datastores.aggregation.QueryEngine;
import com.yahoo.elide.datastores.aggregation.dimension.Dimension;
import com.yahoo.elide.datastores.aggregation.dimension.DimensionType;
import com.yahoo.elide.datastores.aggregation.metric.Metric;

import com.google.common.base.Preconditions;

import org.apache.commons.lang3.mutable.MutableInt;

import lombok.AccessLevel;
import lombok.Getter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* {@link AbstractEntityHydrator} hydrates the entity loaded by {@link QueryEngine#executeQuery(Query)}.
* <p>
* {@link AbstractEntityHydrator} is not thread-safe and should be accessed by only 1 thread in this application,
* because it uses {@link StitchList}. See {@link StitchList} for more details.
*/
public abstract class AbstractEntityHydrator {

@Getter(AccessLevel.PROTECTED)
private final EntityDictionary entityDictionary;

@Getter(AccessLevel.PRIVATE)
private final StitchList stitchList;

@Getter(AccessLevel.PROTECTED)
private final List<Map<String, Object>> results = new ArrayList<>();

@Getter(AccessLevel.PRIVATE)
private final Query query;

/**
* Constructor.
*
* @param results The loaded objects from {@link QueryEngine#executeQuery(Query)}
* @param query The query passed to {@link QueryEngine#executeQuery(Query)} to load the objects
* @param entityDictionary An object that sets entity instance values and provides entity metadata info
*/
public AbstractEntityHydrator(List<Object> results, Query query, EntityDictionary entityDictionary) {
this.stitchList = new StitchList(entityDictionary);
this.query = query;
this.entityDictionary = entityDictionary;

//Get all the projections from the client query.
List<String> projections = this.query.getMetrics().keySet().stream()
.map(Metric::getName)
.collect(Collectors.toList());

projections.addAll(this.query.getDimensions().stream()
.map(Dimension::getName)
.collect(Collectors.toList()));


results.forEach(result -> {
Map<String, Object> row = new HashMap<>();

Object[] resultValues = result instanceof Object[] ? (Object[]) result : new Object[] { result };

Preconditions.checkArgument(projections.size() == resultValues.length);

for (int idx = 0; idx < resultValues.length; idx++) {
Object value = resultValues[idx];
String fieldName = projections.get(idx);
row.put(fieldName, value);
}

this.results.add(row);
});
}

/**
* Loads a map of relationship object ID to relationship object instance.
* <p>
* Note the relationship cannot be toMany. This method will be invoked for every relationship field of the
* requested entity. Its implementation should return the result of the following query
* <p>
* <b>Given a relationship with type {@code relationshipType} in an entity, loads all relationship
* objects whose foreign keys are one of the specified list, {@code joinFieldIds}</b>.
* <p>
* For example, when the relationship is loaded from SQL and we have the following example identity:
* <pre>
* public class PlayerStats {
* private String id;
* private Country country;
*
* &#64;OneToOne
* &#64;JoinColumn(name = "country_id")
* public Country getCountry() {
* return country;
* }
* }
* </pre>
* In this case {@code relationshipType = Country.class}. If {@code country} is
* requested in {@code PlayerStats} query and 3 stats, for example, are found in database whose country ID's are
* {@code joinFieldIds = [840, 344, 840]}, then this method should effectively run the following query (JPQL as
* example)
* <pre>
* {@code
* SELECT e FROM country_table e WHERE country_id IN (840, 344);
* }
* </pre>
* and returns the map of [840: Country(id:840), 344: Country(id:344)]
*
* @param relationshipType The type of relationship
* @param joinFieldIds The specified list of join ID's against the relationship
*
* @return a list of hydrating values
*/
protected abstract Map<Object, Object> getRelationshipValues(
Class<?> relationshipType,
List<Object> joinFieldIds
);

public Iterable<Object> hydrate() {
//Coerce the results into entity objects.
MutableInt counter = new MutableInt(0);

List<Object> queryResults = getResults().stream()
.map((result) -> coerceObjectToEntity(result, counter))
.collect(Collectors.toList());

if (getStitchList().shouldStitch()) {
// relationship is requested, stitch relationship then
populateObjectLookupTable();
getStitchList().stitch();
}

return queryResults;
}

/**
* Coerces results from a {@link Query} into an Object.
*
* @param result a fieldName-value map
* @param counter Monotonically increasing number to generate IDs.
* @return A hydrated entity object.
*/
protected Object coerceObjectToEntity(Map<String, Object> result, MutableInt counter) {
Class<?> entityClass = query.getSchema().getEntityClass();

//Construct the object.
Object entityInstance;
try {
entityInstance = entityClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}

result.forEach((fieldName, value) -> {
Dimension dim = query.getSchema().getDimension(fieldName);

if (dim != null && dim.getDimensionType() == DimensionType.ENTITY) {
getStitchList().todo(entityInstance, fieldName, value); // We don't hydrate relationships here.
} else {
getEntityDictionary().setValue(entityInstance, fieldName, value);
}
});

//Set the ID (it must be coerced from an integer)
getEntityDictionary().setValue(
entityInstance,
getEntityDictionary().getIdFieldName(entityClass),
counter.getAndIncrement()
);

return entityInstance;
}

/**
* Foe each requested relationship, run a single query to load all relationship objects whose ID's are involved in
* the request.
*/
private void populateObjectLookupTable() {
// mapping: relationship field name -> join ID's
Map<String, List<Object>> hydrationIdsByRelationship = getStitchList().getHydrationMapping();

// hydrate each relationship
for (Map.Entry<String, List<Object>> entry : hydrationIdsByRelationship.entrySet()) {
String joinField = entry.getKey();
List<Object> joinFieldIds = entry.getValue();
Class<?> relationshipType = getEntityDictionary().getParameterizedType(
getQuery().getSchema().getEntityClass(),
joinField);

getStitchList().populateLookup(relationshipType, getRelationshipValues(relationshipType, joinFieldIds));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2019, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.datastores.aggregation.engine;

import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.datastores.aggregation.Query;
import lombok.AccessLevel;
import lombok.Getter;

import java.util.AbstractMap;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;

/**
* {@link SQLEntityHydrator} hydrates the entity loaded by {@link SQLQueryEngine#executeQuery(Query)}.
*/
public class SQLEntityHydrator extends AbstractEntityHydrator {

@Getter(AccessLevel.PRIVATE)
private final EntityManager entityManager;

/**
* Constructor.
*
* @param results The loaded objects from {@link SQLQueryEngine#executeQuery(Query)}
* @param query The query passed to {@link SQLQueryEngine#executeQuery(Query)} to load the objects
* @param entityDictionary An object that sets entity instance values and provides entity metadata info
* @param entityManager An service that issues JPQL queries to load relationship objects
*/
public SQLEntityHydrator(
List<Object> results,
Query query,
EntityDictionary entityDictionary,
EntityManager entityManager
) {
super(results, query, entityDictionary);
this.entityManager = entityManager;
}

@Override
protected Map<Object, Object> getRelationshipValues(
Class<?> relationshipType,
List<Object> joinFieldIds
) {
if (joinFieldIds.isEmpty()) {
return Collections.emptyMap();
}

List<Object> uniqueIds = joinFieldIds.stream().distinct().collect(Collectors.toCollection(LinkedList::new));

List<Object> loaded = getEntityManager()
.createQuery(
String.format(
"SELECT e FROM %s e WHERE %s IN (:idList)",
relationshipType.getCanonicalName(),
getEntityDictionary().getIdFieldName(relationshipType)
)
)
.setParameter("idList", uniqueIds)
.getResultList();

return loaded.stream()
.map(obj -> new AbstractMap.SimpleImmutableEntry<>((Object) getEntityDictionary().getId(obj), obj))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
Loading

0 comments on commit 6c04150

Please sign in to comment.