Skip to content
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
merged 47 commits into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
84638ee
AggregationDataStore: Schema (#846)
QubitPi Jul 13, 2019
823cbe5
Added basic H2 DB test harness
Jul 13, 2019
a1e9d8e
Started breaking out projections
Jul 13, 2019
8a4b70a
Moved getValue and setValue from PersistentResource to EntityDictionary
Jul 13, 2019
542240e
Added basic logic to hydrate entities
Jul 13, 2019
3e6a549
Added FromTable and FromSubquery annotations. Add explicit exclusion…
Jul 13, 2019
2b41fa2
Refactored HQLFilterOperation to take an alias generator
Jul 13, 2019
2e32ee5
Added test support for RSQL filter generation. Some cleanup
Jul 13, 2019
fac27e4
Added basic support for WHERE clause filtering on the fact table
Jul 14, 2019
e5977a3
Added working test for subquery SQL
Jul 14, 2019
3057a9a
Added basic join logic for filters
Jul 14, 2019
ac3c518
Added a test with a subquery and a filter join
Jul 14, 2019
c4094df
Refactored Schema classes and Query to support metric aggregation SQL…
Jul 14, 2019
fef2e6d
Added group by support
Jul 14, 2019
0ea8182
Added logic for ID generation
Jul 15, 2019
adaa4a6
Added sorting logic and test
Jul 15, 2019
e749cca
Added pagination support and testing
Jul 15, 2019
1414bb2
All column references use proper name now for SQL
Jul 16, 2019
fd6e9ee
Removed calcite as a query engine
Jul 16, 2019
284a57e
Refactored HQLFilterOperation so it can be used for Having and Where …
Jul 16, 2019
009c2c5
Added HAVING clause support
Jul 16, 2019
1fbb6fc
Changed Query to take schema instead of entityClass
Jul 16, 2019
080ae92
First pass at cleanup
Jul 16, 2019
2699dff
Fixed checkstyles
Jul 16, 2019
763dfa8
Cleanup
Jul 17, 2019
29329f9
Cleanup
Jul 17, 2019
cd169db
Added a complex SQL expression test and fixed bugs
Jul 17, 2019
50ac206
Fixed merge issues. Added another test. Added better logging
Jul 17, 2019
848463f
Hydrate Relationship
QubitPi Jul 15, 2019
dae947e
Self-review
QubitPi Jul 19, 2019
75b03ff
Self-review
QubitPi Jul 19, 2019
c8ff674
Self-review
QubitPi Jul 19, 2019
1123067
Self-review
QubitPi Jul 19, 2019
316c79b
Self-review
QubitPi Jul 19, 2019
673774f
Address comments from @aklish
QubitPi Jul 19, 2019
686c156
Refactor EntityHydrator (#893)
hellohanchen Aug 15, 2019
968556a
rebase
Sep 26, 2019
2d0f76c
keep Jiaqi's changes
Sep 26, 2019
abee6bd
fix id
Sep 26, 2019
1ef3696
fix maven verify
Sep 27, 2019
ea712b5
Remove HQLFilterOperation
Sep 27, 2019
70729a3
fix dictionary
Sep 27, 2019
24b7748
fix SqlEngineTest
Sep 27, 2019
ac0e4a1
remove unused part
Sep 27, 2019
623a288
make codacy happy
Sep 27, 2019
293f8b5
should use getParametrizedType
Sep 27, 2019
9fcaf6f
address comments
Sep 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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;
*
* &#64;OneToOne
* &#64;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));
}
}
}
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,
Copy link
Member

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.

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));
}
}
Loading