Skip to content

Commit

Permalink
[Blazebit#571] Support nested id class binding in CTE's
Browse files Browse the repository at this point in the history
  • Loading branch information
jwgmeligmeyling committed Sep 4, 2018
1 parent 60363fa commit a13b13f
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@

import com.blazebit.persistence.BaseCTECriteriaBuilder;
import com.blazebit.persistence.SelectBuilder;
import com.blazebit.persistence.parser.expression.Expression;
import com.blazebit.persistence.parser.expression.NullExpression;
import com.blazebit.persistence.parser.expression.PathExpression;
import com.blazebit.persistence.parser.expression.PropertyExpression;
import com.blazebit.persistence.impl.query.CTEQuerySpecification;
import com.blazebit.persistence.impl.query.CustomSQLQuery;
import com.blazebit.persistence.impl.query.EntityFunctionNode;
import com.blazebit.persistence.impl.query.QuerySpecification;
import com.blazebit.persistence.parser.expression.Expression;
import com.blazebit.persistence.parser.expression.NullExpression;
import com.blazebit.persistence.parser.expression.PathExpression;
import com.blazebit.persistence.parser.expression.PropertyExpression;
import com.blazebit.persistence.spi.DbmsStatementType;
import com.blazebit.persistence.spi.ExtendedAttribute;
import com.blazebit.persistence.spi.ExtendedManagedType;
Expand All @@ -35,11 +35,13 @@
import javax.persistence.Query;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
Expand Down Expand Up @@ -201,41 +203,67 @@ public CTEInfo createCTEInfo() {
}

protected List<String> prepareAndGetAttributes() {
List<String> attributes = new ArrayList<String>(bindingMap.size());
for (Map.Entry<String, Integer> bindingEntry : bindingMap.entrySet()) {
final String attributeName = bindingEntry.getKey();
final Queue<String> attributeQueue = new ArrayDeque<>(bindingMap.keySet());
while (!attributeQueue.isEmpty()) {
final String attributeName = attributeQueue.remove();
Integer tupleIndex = bindingMap.get(attributeName);

ExtendedAttribute attributeEntry = attributeEntries.get(attributeName);
List<Attribute<?, ?>> attributePath = attributeEntry.getAttributePath();
JpaMetamodelAccessor jpaMetamodelAccessor = mainQuery.jpaProvider.getJpaMetamodelAccessor();
attributes.add(attributeName);
Attribute<?, ?> lastAttribute = attributePath.get(attributePath.size() - 1);

if (jpaMetamodelAccessor.isJoinable(attributePath.get(attributePath.size() - 1))) {
if (jpaMetamodelAccessor.isJoinable(lastAttribute) && !lastAttribute.isCollection()) {
List<String> idOrUniqueKeyProps = cbf.getJpaProvider().getIdentifierOrUniqueKeyEmbeddedPropertyNames(cteType, attributeName);
// We have to map *-to-one relationships to their id or unique props
// NOTE: Since we are talking about *-to-ones, the expression can only be a path to an object
// so it is safe to just append the id to the path
Expression selectExpression = selectManager.getSelectInfos().get(bindingEntry.getValue()).getExpression();
Expression selectExpression = selectManager.getSelectInfos().get(tupleIndex).getExpression();

// TODO: Maybe also allow Treat, Case-When, Array?
if (selectExpression instanceof NullExpression) {
// When binding null, we don't have to adapt anything
} else if (selectExpression instanceof PathExpression && idOrUniqueKeyProps.size() == 1) {
PathExpression pathExpression = (PathExpression) selectExpression;
String idOrUniqueKeyProp = idOrUniqueKeyProps.get(0);
// Only append the id if it's not already there
if (!idOrUniqueKeyProp.equals(pathExpression.getExpressions().get(pathExpression.getExpressions().size() - 1).toString())) {
pathExpression.getExpressions().add(new PropertyExpression(idOrUniqueKeyProp));
} else if (selectExpression instanceof PathExpression) {
boolean firstBinding = true;
PathExpression baseExpression = ((PathExpression) selectExpression).clone(false);
for (String idOrUniqueKeyProp : idOrUniqueKeyProps) {
// For the first id prop, alter the existing expression, for consecutive props
// add additional selects
PathExpression pathExpression = firstBinding ?
((PathExpression) selectExpression) : baseExpression.clone(false);

if (!idOrUniqueKeyProp.equals(pathExpression.getExpressions().get(pathExpression.getExpressions().size() - 1).toString())) {
pathExpression.getExpressions().add(new PropertyExpression(idOrUniqueKeyProp));
String nestedAttributePath = attributeName + "." + idOrUniqueKeyProp;
attributeEntry = attributeEntries.get(nestedAttributePath);

for (String column : attributeEntry.getColumnNames()) {
bindingMap.remove(attributeName);
// Put this binding in the binding map, recursive calls need to retrieve the tuple index
bindingMap.put(nestedAttributePath, firstBinding ? tupleIndex : selectManager.getSelectInfos().size());
columnBindingMap.put(column, nestedAttributePath);
}

if (!firstBinding) {
selectManager.select(pathExpression, null);
attributeQueue.add(nestedAttributePath);
} else {
firstBinding = false;
}

}
}

// TODO: Handle >2 case
} else {
throw new IllegalArgumentException("Illegal expression '" + selectExpression.toString() + "' for binding relation '" + attributeName + "'!");
}
}
}

return attributes;
String[] attributes = new String[bindingMap.size()];
for (Map.Entry<String, Integer> entry : bindingMap.entrySet()) {
attributes[entry.getValue()] = entry.getKey();
}
return Arrays.asList(attributes);
}

protected List<String> prepareAndGetColumnNames() {
Expand All @@ -261,7 +289,11 @@ protected List<String> prepareAndGetColumnNames() {
throw new IllegalStateException(sb.toString());
}

return new ArrayList<>(columnBindingMap.keySet());
String[] columns = new String[columnBindingMap.size()];
for (Map.Entry<String, String> columnBinding : columnBindingMap.entrySet()) {
columns[bindingMap.get(columnBinding.getValue())] = columnBinding.getKey();
}
return Arrays.asList(columns);
}

protected BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> createFinalSetOperationBuilder(SetOperationType operator, boolean nested, boolean isSubquery) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,7 @@ public EntityMetamodelImpl(EntityManagerFactory emf, JpaProviderFactory jpaProvi
}
collectColumnNames(e, attributeMap, attribute.getName(), parents, embeddableType, temporaryExtendedManagedTypes);
} else if (isAssociation(attribute) && !attribute.isCollection() && !jpaProvider.isForeignJoinColumn(e, attribute.getName())) {
List<String> identifierOrUniqueKeyEmbeddedPropertyNames = jpaProvider.getIdentifierOrUniqueKeyEmbeddedPropertyNames(e, attribute.getName());

for (String name : identifierOrUniqueKeyEmbeddedPropertyNames) {
EntityType<?> fieldEntityType = delegate.entity(fieldType);
Attribute<?, ?> idAttribute = JpaMetamodelUtils.getAttribute(fieldEntityType, name);
Class<?> idType = JpaMetamodelUtils.resolveFieldClass(fieldType, idAttribute);
String idPath = attribute.getName() + "." + name;
attributeMap.put(idPath, new AttributeEntry(jpaProvider, e, idAttribute, idPath, idType, Arrays.asList(attribute, idAttribute)));
}
collectIdColumns(attribute.getName(), e, e, attributeMap, attribute, fieldType);
}

attributeMap.put(attribute.getName(), new AttributeEntry(jpaProvider, e, attribute, attribute.getName(), fieldType, parents));
Expand Down Expand Up @@ -195,6 +187,27 @@ public EntityMetamodelImpl(EntityManagerFactory emf, JpaProviderFactory jpaProvi
this.extendedManagedTypes = Collections.unmodifiableMap(extendedManagedTypes);
}

private void collectIdColumns(String prefix, EntityType<?> ownerType, EntityType<?> currentType, Map<String, AttributeEntry<?, ?>> attributeMap, Attribute<?, ?> attribute, Class<?> fieldType) {
List<String> identifierOrUniqueKeyEmbeddedPropertyNames = jpaProvider.getIdentifierOrUniqueKeyEmbeddedPropertyNames(currentType, attribute.getName());

for (String name : identifierOrUniqueKeyEmbeddedPropertyNames) {
EntityType<?> fieldEntityType = delegate.entity(fieldType);
Attribute<?, ?> idAttribute = JpaMetamodelUtils.getAttribute(fieldEntityType, name);
try {
Class<?> idType = JpaMetamodelUtils.resolveFieldClass(fieldType, idAttribute);
String idPath = prefix + "." + name;
attributeMap.put(idPath, new AttributeEntry(jpaProvider, ownerType, idAttribute, idPath, idType, Arrays.asList(attribute, idAttribute)));

if (isAssociation(idAttribute) && !idAttribute.isCollection() && !jpaProvider.isForeignJoinColumn(ownerType, idAttribute.getName())) {
collectIdColumns(idPath, ownerType, fieldEntityType, attributeMap, idAttribute, idType);
}
}
catch (NullPointerException e1) {
throw e1;
}
}
}

private void detectCascadingDeleteCycles(TemporaryExtendedManagedType ownerManagedType, Map<String, TemporaryExtendedManagedType> extendedManagedTypes, Set<Class<?>> cascadingDeleteCycleSet, Class<?> ownerClass, AttributeEntry<?, ?> attributeEntry, Map<Class<?>, Type<?>> classToType) {
Class<?> targetClass = attributeEntry.getElementClass();
Type<?> type = classToType.get(targetClass);
Expand Down Expand Up @@ -595,7 +608,12 @@ public AttributeEntry(JpaProvider jpaProvider, ManagedType<X> ownerType, Attribu
JpaProvider.ConstraintType[] requiresTreatFilter = new JpaProvider.ConstraintType[joinTypes.length];
if (ownerType instanceof EntityType<?>) {
EntityType<?> entityType = (EntityType<?>) ownerType;
this.isForeignJoinColumn = jpaProvider.isForeignJoinColumn(entityType, attributeName);
try {
this.isForeignJoinColumn = jpaProvider.isForeignJoinColumn(entityType, attributeName);
}
catch (Exception e) {
throw e;
}
this.isColumnShared = jpaProvider.isColumnShared(entityType, attributeName);
this.isBag = jpaProvider.isBag(entityType, attributeName);
this.mappedBy = jpaProvider.getMappedBy(entityType, attributeName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public void testCTEAdvanced() {
.where("e.parent").isNull()
.end();
String expected = ""
+ "WITH " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity, level, parent) AS(\n"
+ "WITH " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity.id, level, parent.id) AS(\n"
// NOTE: The parent relation select gets transformed to an id select!
+ "SELECT e.id, e.name, 'desc', e.id, 0, e.parent.id FROM RecursiveEntity e WHERE e.parent IS NULL"
+ "\n)\n"
Expand Down Expand Up @@ -306,7 +306,7 @@ public void testRecursiveCTEAdvanced() {
.bind("parent").select("e.parent")
.end();
String expected = ""
+ "WITH RECURSIVE " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity, level, parentId) AS(\n"
+ "WITH RECURSIVE " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity.id, level, parentId) AS(\n"
+ "SELECT e.id, e.name, 'desc', e.id, 0, e.parent.id FROM RecursiveEntity e WHERE e.parent IS NULL"
+ "\nUNION ALL\n"
// NOTE: The parent relation select gets transformed to an id select!
Expand Down Expand Up @@ -611,7 +611,7 @@ public void testBindEmbeddable() {
.end()
.orderByAsc("id");
String expected = ""
+ "WITH " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity, level, parent) AS(\n"
+ "WITH " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity.id, level, parent.id) AS(\n"
+ "SELECT e.id, e.name, 'desc', e.id, 0, e.parent.id FROM RecursiveEntity e\n"
+ "), " + TestAdvancedCTE2.class.getSimpleName() + "(id, embeddable) AS(\n" +
"SELECT testAdvancedCTE1.id, testAdvancedCTE1.embeddable FROM TestAdvancedCTE1 testAdvancedCTE1\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQL;
import com.blazebit.persistence.testsuite.base.jpa.category.NoOpenJPA;
import com.blazebit.persistence.testsuite.base.jpa.category.NoOracle;
import com.blazebit.persistence.testsuite.entity.IdClassEntity;
import com.blazebit.persistence.testsuite.entity.IdClassEntityId;
import com.blazebit.persistence.testsuite.tx.TxVoidWork;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -38,6 +40,7 @@
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;

import static org.junit.Assert.assertEquals;

Expand All @@ -46,7 +49,7 @@ public class Issue571Test extends AbstractCoreTest {

@Override
protected Class<?>[] getEntityClasses() {
return new Class<?>[] { Cte.class, MyEntity.class };
return new Class<?>[] { Cte.class, MyEntity.class, IdClassEntity.class, IdClassReferencingIdClass.class, EntityWithNestedIdClass.class };
}

@Before
Expand Down Expand Up @@ -142,10 +145,8 @@ public void work(EntityManager em) {
});
}


@Test
@Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class, NoMySQL.class, NoOracle.class })

public void testBindingCteAssociationToEntityId2() {
transactional(new TxVoidWork() {
@Override
Expand Down Expand Up @@ -185,6 +186,29 @@ public void work(EntityManager em) {
});
}

@Test
@Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class, NoMySQL.class, NoOracle.class })
public void testBindingCteUsingNestedIdClass() {
transactional(new TxVoidWork() {
@Override
public void work(EntityManager em) {
CriteriaBuilder<IdClassEntity> select = cbf.create(em, IdClassEntity.class)
.with(IdClassReferencingIdClass.class)
.from(EntityWithNestedIdClass.class, "a")
.bind("id").select("a.idClassEntity.value")
.bind("idClassEntity").select("a")
.bind("someOtherCol").select("'str'")
.end()
.from(IdClassReferencingIdClass.class, "b")
.select("b.idClassEntity");


System.out.println(select.getQueryString());
select.getResultList();
}
});
}

@CTE
@Entity
@IdClass(Cte.CteIdClass.class)
Expand All @@ -205,10 +229,84 @@ public static class CteIdClass implements Serializable {

Long id;
Long myEntity;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CteIdClass that = (CteIdClass) o;
return Objects.equals(id, that.id) &&
Objects.equals(myEntity, that.myEntity);
}

@Override
public int hashCode() {

return Objects.hash(id, myEntity);
}
}

}

@Entity
@IdClass(EntityWithNestedIdClass.EntityWithNestedIdClassId.class)
public static class EntityWithNestedIdClass {

@Id private Long id;
@Id @ManyToOne private IdClassEntity idClassEntity;

public static class EntityWithNestedIdClassId implements Serializable {

private Long id;
private IdClassEntityId idClassEntity;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EntityWithNestedIdClassId that = (EntityWithNestedIdClassId) o;
return Objects.equals(id, that.id) &&
Objects.equals(idClassEntity, that.idClassEntity);
}

@Override
public int hashCode() {

return Objects.hash(id, idClassEntity);
}
}
}

@CTE
@Entity
@IdClass(IdClassReferencingIdClass.IdClassReferencingIdClassId.class)
public static class IdClassReferencingIdClass {

@Id private Long id;
@Id @ManyToOne private EntityWithNestedIdClass idClassEntity;
private String someOtherCol;

public static class IdClassReferencingIdClassId implements Serializable {

private Long id;
private EntityWithNestedIdClass.EntityWithNestedIdClassId idClassEntity;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IdClassReferencingIdClassId that = (IdClassReferencingIdClassId) o;
return Objects.equals(id, that.id) &&
Objects.equals(idClassEntity, that.idClassEntity);
}

@Override
public int hashCode() {
return Objects.hash(id, idClassEntity);
}
}
}

@Entity
public static class MyEntity {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,8 +868,14 @@ public List<String> getIdentifierOrUniqueKeyEmbeddedPropertyNames(EntityType<?>
boolean supportsNaturalIdAccessOptimization = false;
AbstractEntityPersister entityPersister = getEntityPersister(owner);
Type propertyType = entityPersister.getPropertyType(attributeName);
org.hibernate.type.EntityType entityType = (org.hibernate.type.EntityType) propertyType;

// For nested embedded id's, the nested id is mapped as a component type
if (propertyType instanceof ComponentType) {
return Arrays.asList(((ComponentType) propertyType).getPropertyNames());
}

List<String> identifierOrUniqueKeyPropertyNames;
org.hibernate.type.EntityType entityType = (org.hibernate.type.EntityType) propertyType;
Type identifierOrUniqueKeyType = entityType.getIdentifierOrUniqueKeyType(entityPersister.getFactory());

if (identifierOrUniqueKeyType instanceof EmbeddedComponentType) {
Expand Down
Loading

0 comments on commit a13b13f

Please sign in to comment.