/*
* Copyright (c) 2010-2016 Evolveum
*
* 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.evolveum.midpoint.repo.sql.util;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import org.apache.commons.lang.StringUtils;
import org.hibernate.type.ComponentType;
import org.hibernate.type.ManyToOneType;
import org.hibernate.type.Type;
/**
* Hibernate seems to load ManyToOne association targets when doing "merge" or "object load" operation,
* even though they are declared as LAZY.
*
* For recently-added "target" association in REmbeddedReference, RObjectReference and RCertCaseWorkItemReference
* this is very unfortunate, as it leads to fetching the whole RObject instance (the target of the association),
* which means joining approx. 20 tables - a huge overhead. For example, this adds an extra 50 milliseconds
* in case of locally-run PostgreSQL database.
*
* What is interesting, we don't use values of this association in java objects in any way.
* We created this association only to allow the use of left outer joins of referenced objects in HQL queries (see below).
*
* I've found no easy way how to prevent automatic loading of referenced objects.
* (Unsuccessful attempts are listed below.)
*
* So here is the solution - quite an ugly hack.
*
* We provide a custom persister for every entity that contains the "target" association. (It is the majority
* of entities, mainly because REmbeddedReference is used in many objects.) The persister modifies the hydrate() method,
* responsible for translation of database rows into Java entity objects. More specifically, after invoking
* original hydrate() method, we erase any traces of "target" ManyToOne association values, replacing them by nulls.
* Of course, this means that such values will not be accessible within hydrated POJOs. But we don't care: we
* do not need them there. The only reason of their existence is to allow their retrieval via HQL queries.
*
* Drawbacks/limitations:
* 1) As mentioned, association values cannot be retrieved from POJOs. Only by HQL.
* 2) Custom persister has to be manually declared on all entities that directly or indirectly contain
* the "target" association; otherwise the performance when getting/merging such entities will be (silently) degraded.
* 3) When declaring the persister, one has to correctly determine its type (Joined/SingleTable one).
* One can help himself by looking at which persister is used by hibernate itself
* (e.g. by calling sessionFactoryImpl.getEntityPersisters).
* 4) Current implementation is quite simple minded as it expects that the name of association is
* "target". This might collide with other ManyToOne associations with that name. (Although currently there are none.)
* In future we could create some annotation to select associations to be "killed".
* 5) It is unclear if this solution would work with persisters with batch loading enabled. Fortunately,
* we currently don't use this feature in midPoint.
*
* Alternative solutions:
* 1) One almost-working attempt was to declare the field as lazy in the "no-proxy" way
* (@LazyToOne(LazyToOneOption.NO_PROXY)). However, it has 2 drawbacks:
* A) there are still SELECTs executed in such situations (perhaps one per each association)
* B) it required build-time instrumentation, which failed on RObject for unclear reasons
* Even if B would be solved, problem A is still there, leading to performance penalties.
* 2) Elimination of associations altogether. It possible to fetch targets in HQL even without the associations by
* simply listing more (unrelated) classes in FROM clause. However, this leads to inner, not left outer joins.
* The effect is that objects that have targets with null or non-existent OIDs are not included in the result,
* which is obviously unacceptable. Explicit joins for unrelated classes cannot be used for now
* (https://hibernate.atlassian.net/browse/HHH-16).
* 3) Using "target" associations only for references that need them. This feature is currently used only
* for certifications, so it would be possible to create RResolvableEmbeddedReference and use it for
* certification case entity. (Plus at some other places.) The performance degradation would be limited
* to these entities. However, other modules would not be able to employ displaying/selecting/ordering by
* e.g. referenced object names (or other properties).
*
* If it would turn out that no harm is caused by this hack, it could be kept here.
* If not, alternative #3 should be employed. After HHH-16 is provided, we should implement alternative #2.
*
* @author mederly
*/
public class MidpointPersisterUtil {
private static final Trace LOGGER = TraceManager.getTrace(MidpointPersisterUtil.class);
public static final String ASSOCIATION_TO_REMOVE = "target";
public static void killUnwantedAssociationValues(String[] propertyNames, Type[] propertyTypes, Object[] values) {
killUnwantedAssociationValues(propertyNames, propertyTypes, values, 0);
}
private static void killUnwantedAssociationValues(String[] propertyNames, Type[] propertyTypes, Object[] values, int depth) {
if (values == null) {
return;
}
for (int i = 0; i < propertyTypes.length; i++) {
String name = propertyNames[i];
Type type = propertyTypes[i];
Object value = values[i];
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("{}- killUnwantedAssociationValues processing #{}: {} (type={}, value={})",
StringUtils.repeat(" ", depth), i, name, type, value);
}
if (type instanceof ComponentType) {
ComponentType componentType = (ComponentType) type;
killUnwantedAssociationValues(componentType.getPropertyNames(), componentType.getSubtypes(), (Object[]) value, depth+1);
} else if (type instanceof ManyToOneType) {
if (ASSOCIATION_TO_REMOVE.equals(name)) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("{}- killUnwantedAssociationValues KILLED #{}: {} (type={}, value={})",
StringUtils.repeat(" ", depth), i, name, type, value);
}
values[i] = null;
}
}
}
}
}