/* * Copyright 2014 - 2017 Blazebit. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.blazebit.persistence.impl.hibernate; import com.blazebit.persistence.JoinType; import com.blazebit.persistence.spi.JpaProvider; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Joinable; import org.hibernate.persister.entity.SingleTableEntityPersister; import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; import javax.persistence.EntityManager; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.PluralAttribute; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * * @author Christian Beikov * @since 1.0 */ public class HibernateJpaProvider implements JpaProvider { private final DB db; private final Map<String, EntityPersister> entityPersisters; private final Map<String, CollectionPersister> collectionPersisters; private final ConcurrentMap<ColumnSharingCacheKey, Boolean> columnSharingCache = new ConcurrentHashMap<>(); private static enum DB { OTHER, MY_SQL, DB2, MSSQL; } public HibernateJpaProvider(EntityManager em, String dbms, Map<String, EntityPersister> entityPersisters, Map<String, CollectionPersister> collectionPersisters) { try { if (em == null) { db = DB.OTHER; } else if ("mysql".equals(dbms)) { db = DB.MY_SQL; } else if ("db2".equals(dbms)) { db = DB.DB2; } else if ("microsoft".equals(dbms)) { db = DB.MSSQL; } else { db = DB.OTHER; } this.entityPersisters = entityPersisters; this.collectionPersisters = collectionPersisters; } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean supportsJpa21() { return false; } @Override public boolean supportsEntityJoin() { return false; } @Override public boolean supportsInsertStatement() { return true; } @Override public boolean needsBracketsForListParamter() { return true; } @Override public boolean needsJoinSubqueryRewrite() { return true; } @Override public String getBooleanExpression(boolean value) { return value ? "CASE WHEN 1 = 1 THEN true ELSE false END" : "CASE WHEN 1 = 1 THEN false ELSE true END"; } @Override public String getBooleanConditionalExpression(boolean value) { return value ? "1 = 1" : "1 = 0"; } @Override public String getNullExpression() { return "NULLIF(1,1)"; } @Override public String escapeCharacter(char character) { if (character == '\\' && db == DB.MY_SQL) { return "\\\\"; } else { return Character.toString(character); } } @Override public boolean supportsNullPrecedenceExpression() { return db == DB.OTHER; } @Override public void renderNullPrecedence(StringBuilder sb, String expression, String resolvedExpression, String order, String nulls) { if (nulls != null) { if (db != DB.OTHER) { if (db == DB.DB2) { if (("FIRST".equals(nulls) && "DESC".equalsIgnoreCase(order)) || ("LAST".equals(nulls) && "ASC".equalsIgnoreCase(order))) { // The following are ok according to DB2 docs // ASC + NULLS LAST // DESC + NULLS FIRST sb.append(expression); if (order != null) { sb.append(" ").append(order).append(" NULLS ").append(nulls); } return; } } else if (db == DB.MSSQL) { if (("ASC".equalsIgnoreCase(order) && "FIRST".equals(nulls)) || ("DESC".equalsIgnoreCase(order) && "LAST".equals(nulls))) { // The following are the defaults, so just let them through // ASC + NULLS FIRST // DESC + NULLS LAST sb.append(expression); if (order != null) { sb.append(" ").append(order); } return; } } // Unfortunately we have to take care of that our selves because the SQL generation has a bug for MySQL: HHH-10241 sb.append("CASE WHEN ").append(resolvedExpression != null ? resolvedExpression : expression).append(" IS NULL THEN "); if ("FIRST".equals(nulls)) { sb.append("0 ELSE 1"); } else { sb.append("1 ELSE 0"); } sb.append(" END, "); sb.append(expression); if (order != null) { sb.append(" ").append(order); } } else { sb.append(expression); if (order != null) { sb.append(' ').append(order).append(" NULLS ").append(nulls); } } } else { sb.append(expression); if (order != null) { sb.append(' ').append(order); } } } @Override public String getOnClause() { return "WITH"; } @Override public String getCollectionValueFunction() { return null; } @Override public boolean supportsCollectionValueDereference() { return false; } @Override public Class<?> getDefaultQueryResultType() { return Object.class; } @Override public String getCustomFunctionInvocation(String functionName, int argumentCount) { // Careful, PaginatedCriteriaBuilder has some dependency on the "length" of the string for rendering in the count query return functionName + "("; } @Override public boolean supportsRootTreat() { return false; } @Override public boolean supportsTreatJoin() { return false; } @Override public boolean supportsTreatCorrelation() { return false; } @Override public boolean supportsRootTreatJoin() { return false; } @Override public boolean supportsRootTreatTreatJoin() { return false; } @Override public boolean supportsSubtypePropertyResolving() { return true; } @Override public boolean supportsSubtypeRelationResolving() { return true; } @Override public boolean supportsCountStar() { return true; } @Override public boolean isForeignJoinColumn(ManagedType<?> ownerType, String attributeName) { AbstractEntityPersister persister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); Type propertyType = persister.getPropertyType(attributeName); if (propertyType instanceof OneToOneType) { ForeignKeyDirection direction = ((OneToOneType) propertyType).getForeignKeyDirection(); // Types changed between 4 and 5 so we check it like this. Essentially we check if the TO_PARENT direction is used return direction.toString().regionMatches(true, 0, "to", 0, 2); } // Every entity persister has "owned" properties on table number 0, others have higher numbers int tableNumber = persister.getSubclassPropertyTableNumber(attributeName); return tableNumber >= persister.getEntityMetamodel().getSubclassEntityNames().size(); } @Override public boolean isColumnShared(ManagedType<?> ownerType, String attributeName) { AbstractEntityPersister persister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); if (!(persister instanceof SingleTableEntityPersister) && !(persister instanceof UnionSubclassEntityPersister)) { return false; } ColumnSharingCacheKey cacheKey = new ColumnSharingCacheKey(ownerType, attributeName); Boolean result = columnSharingCache.get(cacheKey); if (result != null) { return result; } if (persister instanceof SingleTableEntityPersister) { SingleTableEntityPersister singleTableEntityPersister = (SingleTableEntityPersister) persister; SingleTableEntityPersister rootPersister = (SingleTableEntityPersister) entityPersisters.get(singleTableEntityPersister.getRootEntityName()); result = isColumnShared(singleTableEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); } else if (persister instanceof UnionSubclassEntityPersister) { UnionSubclassEntityPersister unionSubclassEntityPersister = (UnionSubclassEntityPersister) persister; UnionSubclassEntityPersister rootPersister = (UnionSubclassEntityPersister) entityPersisters.get(unionSubclassEntityPersister.getRootEntityName()); result = isColumnShared(unionSubclassEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); } columnSharingCache.put(cacheKey, result); return result; } @Override public ConstraintType requiresTreatFilter(ManagedType<?> type, String attributeName, JoinType joinType) { // When we don't support treat joins(Hibernate 4.2), we need to put the constraints into the WHERE clause if (!supportsTreatJoin() && joinType == JoinType.INNER) { return ConstraintType.WHERE; } AbstractEntityPersister persister = (AbstractEntityPersister) entityPersisters.get(type.getJavaType().getName()); Type propertyType = persister.getPropertyType(attributeName); if (!(propertyType instanceof AssociationType)) { return ConstraintType.NONE; } // When the inner treat joined element is collection, we always need the constraint, see HHH-??? TODO: report issue if (joinType == JoinType.INNER && propertyType instanceof CollectionType) { CollectionType collectionType = (CollectionType) propertyType; // When the inner treat joined element is an inverse collection, we currently have to put the constraint to the WHERE clause, see HHH-??? TODO: report issue ForeignKeyDirection direction = collectionType.getForeignKeyDirection(); // Types changed between 4 and 5 so we check it like this. Essentially we check if the TO_PARENT direction is used if (direction.toString().regionMatches(true, 0, "to", 0, 2)) { return ConstraintType.WHERE; } return ConstraintType.ON; } String propertyEntityName = ((AssociationType) propertyType).getAssociatedEntityName(persister.getFactory()); AbstractEntityPersister propertyTypePersister = (AbstractEntityPersister) entityPersisters.get(propertyEntityName); // When the treat joined element is a union subclass, we always need the constraint, see HHH-??? TODO: report issue if (propertyTypePersister instanceof UnionSubclassEntityPersister) { return ConstraintType.ON; } return ConstraintType.NONE; } private boolean isColumnShared(AbstractEntityPersister persister, String rootName, String[] subclassNames, String attributeName) { String[] columnNames = persister.getSubclassPropertyColumnNames(attributeName); for (String subclass: subclassNames) { if (!subclass.equals(persister.getName()) && !subclass.equals(rootName)) { AbstractEntityPersister subclassPersister = (AbstractEntityPersister) entityPersisters.get(subclass); if (isColumnShared(subclassPersister, columnNames)) { return true; } } } return false; } private boolean isColumnShared(AbstractEntityPersister subclassPersister, String[] columnNames) { List<String> propertiesToCheck = new ArrayList<>(Arrays.asList(subclassPersister.getPropertyNames())); while (!propertiesToCheck.isEmpty()) { String propertyName = propertiesToCheck.remove(propertiesToCheck.size() - 1); Type propertyType = subclassPersister.getPropertyType(propertyName); if (propertyType instanceof ComponentType) { ComponentType componentType = (ComponentType) propertyType; for (String subPropertyName : componentType.getPropertyNames()) { propertiesToCheck.add(propertyName + "." + subPropertyName); } } else { String[] subclassColumnNames = subclassPersister.getSubclassPropertyColumnNames(propertyName); if (Arrays.deepEquals(columnNames, subclassColumnNames)) { return true; } } } return false; } @Override public boolean isJoinTable(Attribute<?, ?> attribute) { StringBuilder sb = new StringBuilder(200); sb.append(attribute.getDeclaringType().getJavaType().getName()); sb.append('.'); sb.append(attribute.getName()); CollectionPersister persister = collectionPersisters.get(sb.toString()); if (persister instanceof QueryableCollection) { QueryableCollection queryableCollection = (QueryableCollection) persister; if (queryableCollection.getElementPersister() instanceof Joinable) { String elementTableName = ((Joinable) queryableCollection.getElementPersister()).getTableName(); return !queryableCollection.getTableName().equals(elementTableName); } } return false; } @Override public boolean isBag(Attribute<?, ?> attribute) { if (attribute instanceof PluralAttribute) { PluralAttribute.CollectionType collectionType = ((PluralAttribute<?, ?, ?>) attribute).getCollectionType(); if (collectionType == PluralAttribute.CollectionType.COLLECTION) { return true; } else if (collectionType == PluralAttribute.CollectionType.LIST) { StringBuilder sb = new StringBuilder(200); sb.append(attribute.getDeclaringType().getJavaType().getName()); sb.append('.'); sb.append(attribute.getName()); CollectionPersister persister = collectionPersisters.get(sb.toString()); return !persister.hasIndex(); } } return false; } @Override public boolean supportsSingleValuedAssociationIdExpressions() { return true; } @Override public boolean supportsForeignAssociationInOnClause() { return false; } @Override public boolean supportsTransientEntityAsParameter() { return true; } @Override public boolean needsAssociationToIdRewriteInOnClause() { return true; } @Override public boolean needsBrokenAssociationToIdRewriteInOnClause() { return true; } @Override public boolean needsTypeConstraintForColumnSharing() { return true; } private static class ColumnSharingCacheKey { private final ManagedType<?> managedType; private final String attributeName; public ColumnSharingCacheKey(ManagedType<?> managedType, String attributeName) { this.managedType = managedType; this.attributeName = attributeName; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ColumnSharingCacheKey that = (ColumnSharingCacheKey) o; if (!managedType.equals(that.managedType)) { return false; } return attributeName.equals(that.attributeName); } @Override public int hashCode() { int result = managedType.hashCode(); result = 31 * result + attributeName.hashCode(); return result; } } }