/* * #%L * BroadleafCommerce Common Libraries * %% * Copyright (C) 2009 - 2014 Broadleaf Commerce * %% * 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. * #L% */ package org.broadleafcommerce.common.util; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.SQLQuery; import org.hibernate.Session; import org.hibernate.type.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.persistence.EntityManager; /** * The purpose for this class is to provide an alternate approach to an HQL UPDATE query for batch updates on Hibernate filtered * entities (such as sandboxable and multi-tenant entities). * </p> * This class takes an interesting approach to the use of update queries. To explain, a bit of background is required. * First, Hibernate will create a temporary table and fill it will ids to use in a where clause when it executs an HQL UPDATE * query. However, it will only create this temporary table when the target entity has Hibernate filters applied * (i.e. sandboxable or multi-tenant entities). When creating this temporary table, a ‘insert into select’ is used to * populate the values. It is my understanding that this ends up creating some locks on the original table. Because of * these locks, we were seeing some instances of deadlocks during concurrent admin usage. The key was to avoid * the temporary table creation. We did this by first selecting for ids (so that the filters were still honored) and then * using a simple, native sql statement to execute the update on entities matching those ids. The native sql needs to be basic * enough that it’s portable across platforms. * </p> * This class is responsible for building the native sql based on a template String. It does it in a way using a standard * parameterized query (rather than string concatenation) to avoid the possibility of any sql injection exploit. * </p> * This implementation has the added benefit of breaking up large IN clauses into smaller chunks to avoid maximum * IN clause lengths enforced by some database platforms. * * @author Jeff Fischer */ public class UpdateExecutor { /** * Perform an update query using a String template and params. Note, this is only intended for special * usage with update queries that have an IN clause at the end. This implementation uses Hibernate Session * directly to avoid a problem with assigning NULL values. The query should be written in native SQL. * </p> * An example looks like: 'UPDATE BLC_SNDBX_WRKFLW_ITEM SET SCHEDULED_DATE = ? WHERE WRKFLW_SNDBX_ITEM_ID IN (%s)' * * @param em The entity manager to use for the persistence operation * @param template the overall update sql template. The IN clause parameter should be written using 'IN (%s)'. * @param params any other params that are present in the sql template, other than the IN clause. Should be written using '?'. Should be in order. Can be null. * @param types the {@link org.hibernate.type.Type} instances that identify the types for the params. Should be in order and match the length of params. Can be null. * @param ids the ids to include in the IN clause. * @return the total number of records updated in the database */ public static int executeUpdateQuery(EntityManager em, String template, Object[] params, Type[] types, List<Long> ids) { int response = 0; List<Long[]> runs = buildRuns(ids); for (Long[] run : runs) { String queryString = String.format(template, buildInClauseTemplate(run.length)); SQLQuery query = em.unwrap(Session.class).createSQLQuery(queryString); int counter = 0; if (!ArrayUtils.isEmpty(params)) { for (Object param : params) { query.setParameter(counter, param, types[counter]); counter++; } } for (Long id : run) { query.setLong(counter, id); counter++; } response += query.executeUpdate(); } return response; } /** * Quickly build up the sql IN clause template * * @param length * @return */ private static String buildInClauseTemplate(int length) { String[] temp = new String[length]; Arrays.fill(temp, "?"); return StringUtils.join(temp, ","); } /** * This breaks up our IN clause into multiple runs of 800 or less in order * to guarantee compatibility across platforms (i.e. some db platforms will throw a error if there are more * than a 1000 entries in an sql IN clause). * * @param ids * @return */ private static List<Long[]> buildRuns(List<Long> ids) { List<Long[]> runs = new ArrayList<Long[]>(); Long[] all = ids.toArray(new Long[ids.size()]); int test = all.length; int pos = 0; boolean eof = false; while (!eof) { int arraySize; if (test < 800) { arraySize = test; eof = true; } else { arraySize = 800; test -= arraySize; if (test == 0) { eof = true; } } Long[] temp = new Long[arraySize]; System.arraycopy(all, pos, temp, 0, arraySize); pos += arraySize; runs.add(temp); } return runs; } }