-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hydrate Relationship #987
Merged
aklish
merged 47 commits into
yahoo:AggregationDataStore
from
hellohanchen:relationshiphydration
Sep 27, 2019
Merged
Hydrate Relationship #987
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
84638ee
AggregationDataStore: Schema (#846)
QubitPi 823cbe5
Added basic H2 DB test harness
a1e9d8e
Started breaking out projections
8a4b70a
Moved getValue and setValue from PersistentResource to EntityDictionary
542240e
Added basic logic to hydrate entities
3e6a549
Added FromTable and FromSubquery annotations. Add explicit exclusion…
2b41fa2
Refactored HQLFilterOperation to take an alias generator
2e32ee5
Added test support for RSQL filter generation. Some cleanup
fac27e4
Added basic support for WHERE clause filtering on the fact table
e5977a3
Added working test for subquery SQL
3057a9a
Added basic join logic for filters
ac3c518
Added a test with a subquery and a filter join
c4094df
Refactored Schema classes and Query to support metric aggregation SQL…
fef2e6d
Added group by support
0ea8182
Added logic for ID generation
adaa4a6
Added sorting logic and test
e749cca
Added pagination support and testing
1414bb2
All column references use proper name now for SQL
fd6e9ee
Removed calcite as a query engine
284a57e
Refactored HQLFilterOperation so it can be used for Having and Where …
009c2c5
Added HAVING clause support
1fbb6fc
Changed Query to take schema instead of entityClass
080ae92
First pass at cleanup
2699dff
Fixed checkstyles
763dfa8
Cleanup
29329f9
Cleanup
cd169db
Added a complex SQL expression test and fixed bugs
50ac206
Fixed merge issues. Added another test. Added better logging
848463f
Hydrate Relationship
QubitPi dae947e
Self-review
QubitPi 75b03ff
Self-review
QubitPi c8ff674
Self-review
QubitPi 1123067
Self-review
QubitPi 316c79b
Self-review
QubitPi 673774f
Address comments from @aklish
QubitPi 686c156
Refactor EntityHydrator (#893)
hellohanchen 968556a
rebase
2d0f76c
keep Jiaqi's changes
abee6bd
fix id
1ef3696
fix maven verify
ea712b5
Remove HQLFilterOperation
70729a3
fix dictionary
24b7748
fix SqlEngineTest
ac0e4a1
remove unused part
623a288
make codacy happy
293f8b5
should use getParametrizedType
9fcaf6f
address comments
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
...n/src/main/java/com/yahoo/elide/datastores/aggregation/engine/AbstractEntityHydrator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
/* | ||
* 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 {@code joinField} in an entity of type {@code entityClass}, 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; | ||
* | ||
* @OneToOne | ||
* @JoinColumn(name = "country_id") | ||
* public Country getCountry() { | ||
* return country; | ||
* } | ||
* } | ||
* </pre> | ||
* In this case {@code entityClass = PlayerStats.class}; {@code joinField = "country"}. 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 entityClass The type of relationship | ||
* @param joinField The relationship field name | ||
* @param joinFieldIds The specified list of join ID's against the relationshiop | ||
* | ||
* @return a list of hydrating values | ||
*/ | ||
protected abstract Map<Object, Object> getRelationshipValues( | ||
Class<?> entityClass, | ||
String joinField, | ||
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<?> entityType = getEntityDictionary().getParameterizedType( | ||
getQuery().getSchema().getEntityClass(), | ||
joinField); | ||
|
||
getStitchList().populateLookup(entityType, getRelationshipValues(entityType, joinField, joinFieldIds)); | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
...gation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLEntityHydrator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* 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<?> entityClass, | ||
String joinField, | ||
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)", | ||
entityClass.getCanonicalName(), | ||
getEntityDictionary().getIdFieldName(entityClass) | ||
) | ||
) | ||
.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)); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unused. We should remove it from the contract.