package er.extensions.eof; import java.util.Enumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOObjectStoreCoordinator; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSRange; /** * ERXBatchFetchUtilities provides a collection of methods to support efficiently * batch fetching arbitrarily deep keypaths on EOs. * * @author Lenny Marks (lenny@aps.org) */ public class ERXBatchFetchUtilities { private static final Logger log = LoggerFactory.getLogger(ERXBatchFetchUtilities.class); /** * Defaults skipFaultedSourceObjects to false for backwards compatibility * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObjects the array of source object to fault keypaths on. * @param keypaths the array of keypaths to fault */ public static void batchFetch(NSArray sourceObjects, NSArray keypaths) { ERXBatchFetchUtilities.batchFetch(sourceObjects, keypaths, false); } /** * Batch key the list of keys from the given source objects. No backwards compatibility * here, so skipFaultedSourceObject is true. * * @param sourceObjects the array of source object to fault keypaths on. * @param keys the array of ERXKeys to fault */ public static void batchFetch(NSArray<? extends EOEnterpriseObject> sourceObjects, ERXKey<?>... keys) { NSMutableArray<String> keypaths = new NSMutableArray<>(); for (ERXKey<?> key : keys) { keypaths.addObject(key.key()); } ERXBatchFetchUtilities.batchFetch(sourceObjects, keypaths, true); } /** * Shortcut for batch fetching a single source object * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObject source object to fault keypaths on. * @param keypaths the array of keypaths to fault * @param skipFaultedSourceObjects if true, all source objects that already have their relationships faulted will be skipped */ public static void batchFetch(EOEnterpriseObject sourceObject, NSArray keypaths, boolean skipFaultedSourceObjects) { ERXBatchFetchUtilities.batchFetch(new NSArray<EOEnterpriseObject>(sourceObject), keypaths, skipFaultedSourceObjects); } /** * Shortcut for batch fetching a single keypath. * Defaults skipFaultedSourceObjects to true * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObjects the array of source object to fault keypaths on. * @param keypath the keypath to fault */ public static void batchFetch(NSArray sourceObjects, String keypath) { ERXBatchFetchUtilities.batchFetch(sourceObjects, keypath, true); } /** * Shortcut for batch fetching a single keypath. * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObjects the array of source object to fault keypaths on. * @param keypath the keypath to fault * @param skipFaultedSourceObjects if true, all source objects that already have their relationships faulted will be skipped */ public static void batchFetch(NSArray sourceObjects, String keypath, boolean skipFaultedSourceObjects) { ERXBatchFetchUtilities.batchFetch(sourceObjects, new NSArray<String>(keypath), skipFaultedSourceObjects); } /** * Shortcut for batch fetching a single keypath and returns the fetched values. * Defaults skipFaultedSourceObjects to true * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObjects the array of source object to fault keypaths on. * @param keypath the keypath to fault */ public static NSArray batchFetchAndRetrieve(NSArray sourceObjects, String keypath) { return ERXBatchFetchUtilities.batchFetchAndRetrieve(sourceObjects, keypath, true); } /** * Shortcut for batch fetching a single keypath and returns the fetched values. * @see #batchFetch(NSArray, NSArray, boolean) * * @param sourceObjects the array of source object to fault keypaths on. * @param keypath the keypath to fault * @param skipFaultedSourceObjects if true, all source objects that already have their relationships faulted will be skipped */ public static NSArray batchFetchAndRetrieve(NSArray sourceObjects, String keypath, boolean skipFaultedSourceObjects) { ERXBatchFetchUtilities.batchFetch(sourceObjects, keypath, skipFaultedSourceObjects); return (NSArray) sourceObjects.valueForKeyPath(keypath); } /** * Batch fetch relationships specified by <i>keypaths </i> for * <i>sourceObjects </i>. * <p> * This method will use EODatabaseContext.batchFetchRelationship to * efficiently fetch like relationships for many objects in as few as one * database round trip per relationship. * <p> * For example, if fetching from Movie entities, you might specify paths of * the form: ("directors","roles.talent", "plotSummary"). This works much * like prefetching with fetch specifications, however this implementation * is able to work around inheritance where prefetching fails. * * @param sourceObjects the array of source objects to fault keypaths on. * @param keypaths the array of keypaths to fault * @param skipFaultedSourceObjects if true, all source objects that already have their relationships faulted will be skipped */ public static void batchFetch(NSArray sourceObjects, NSArray keypaths, boolean skipFaultedSourceObjects) { if (sourceObjects.count() == 0) return; EOEditingContext ec = null; for (Object sample : sourceObjects) { if (((EOEnterpriseObject)sample).editingContext() != null) { ec = ((EOEnterpriseObject)sample).editingContext(); break; } } if (ec == null) return; EOObjectStoreCoordinator osc = (EOObjectStoreCoordinator) ec.rootObjectStore(); osc.lock(); try { NSArray rootKeyPathObjects = KeyPath.parseKeyPathStrings(keypaths); Enumeration keyPathObjectsEnum = rootKeyPathObjects.objectEnumerator(); while (keyPathObjectsEnum.hasMoreElements()) { KeyPath kp = (KeyPath) keyPathObjectsEnum.nextElement(); kp.traverseForObjects(sourceObjects, skipFaultedSourceObjects); } } finally { osc.unlock(); } } /** * Overloads batchFetch(NSArray, NSArray, boolean) to batch through the * NSArray of sourceObjects batchSize at a time. * * @see #batchFetch(NSArray, NSArray, boolean) * * @author aman - Mar 11, 2009 * @param sourceObjects * @param keypaths * @param skipFaultedSourceObjects * @param batchSize */ public static void batchFetch(NSArray sourceObjects, NSArray keypaths, boolean skipFaultedSourceObjects, int batchSize) { for (int i = 0; i < sourceObjects.count(); i += batchSize) { int rangeSize = batchSize; if (i + batchSize > sourceObjects.count()) { rangeSize = sourceObjects.count() - i; } NSRange range = new NSRange(i, rangeSize); NSArray batchedSourceObjects = sourceObjects.subarrayWithRange(range); batchFetch(batchedSourceObjects, keypaths, skipFaultedSourceObjects); } } /** * This class represent a keypath as a hierarchical tree structure(path and * subPaths similar to directory and subDirectory). * * @author lenny * */ static class KeyPath { private String path; private NSArray subPaths; //NSArray of KeyPath objects public KeyPath(String path, NSArray subPaths) { this.path = path; this.subPaths = subPaths; } /** * Traverse through all relationships represented by this keypath for * <i>sourceObjects </i> using batchFetching to optimize roundtrips to * database. * * <p> * This method assumes the EOObjectStoreCoordinator has been externally * locked. * * @see EOFUtils#batchFetch * * @param sourceObjects * @param skipFaultedSourceObjects */ public void traverseForObjects(NSArray sourceObjects, boolean skipFaultedSourceObjects) { if (sourceObjects == null || sourceObjects.count() < 1) return; NSDictionary objectsByEntity = splitObjectsByEntity(sourceObjects); Enumeration e = objectsByEntity.allValues().objectEnumerator(); while (e.hasMoreElements()) { NSArray homogeniousObjects = (NSArray) e.nextElement(); traverseForHomogeniousObjects(homogeniousObjects, skipFaultedSourceObjects); } } public String path() { return path; } public NSArray subPaths() { return subPaths; } /** * Take a list of keypath strings and return a list or KeyPath objects. * * <pre> * * ex. from * manuscriptEvents.userAction.enteredBy * manuscriptEvents.userAction.changedBy * manuscriptEvents.correspondence.enteredBy * deniedIndividuals * * to: * * 1. manuscriptEvents. * userAction. * enteredBy * changedBy * 2. manuscriptEvents. * correspondence. * enteredBy * 3. deniedIndividuals * * </pre> * * @param keypathStrings * @return */ public static NSArray<KeyPath> parseKeyPathStrings(NSArray keypathStrings) { //keyed by top level so we can combine like root paths NSDictionary subPathsKeyedByTopLevel = subPathsKeyedByTopLevel(keypathStrings); NSMutableArray<KeyPath> keyPathObjects = new NSMutableArray<>(); Enumeration e = subPathsKeyedByTopLevel.keyEnumerator(); while (e.hasMoreElements()) { String path = (String) e.nextElement(); NSArray subPaths = (NSArray) subPathsKeyedByTopLevel.valueForKey(path); KeyPath kp = new KeyPath(path, KeyPath.parseKeyPathStrings(subPaths)); keyPathObjects.addObject(kp); } return keyPathObjects; } private void traverseForHomogeniousObjects(NSArray sourceObjects, boolean skipFaultedSourceObjects) { if (sourceObjects == null || sourceObjects.count() < 1) return; EOEnterpriseObject eo = (EOEnterpriseObject) sourceObjects.objectAtIndex(0); EOEditingContext ec = eo.editingContext(); EOEntity entity = EOUtilities.entityForObject(ec, eo); EORelationship relationship = entity.relationshipNamed(path); if (relationship == null) return; batchFetchRelationshipOnSourceObjects(relationship, sourceObjects, skipFaultedSourceObjects); Enumeration subPathsEnum = subPaths.objectEnumerator(); while (subPathsEnum.hasMoreElements()) { KeyPath subPath = (KeyPath) subPathsEnum.nextElement(); NSArray destinationObjects = destinationObjectsForRelationship(relationship, sourceObjects); subPath.traverseForObjects(destinationObjects, skipFaultedSourceObjects); } } private static NSDictionary subPathsKeyedByTopLevel(NSArray keypathStrings) { NSMutableDictionary subPathsKeyedByTopLevel = new NSMutableDictionary(); Enumeration e = keypathStrings.objectEnumerator(); while (e.hasMoreElements()) { String keypath = (String) e.nextElement(); String path = KeyPath.directPathFromKeyPath(keypath); NSMutableArray subPaths = (NSMutableArray) subPathsKeyedByTopLevel.valueForKey(path); if (subPaths == null) { subPaths = new NSMutableArray(); subPathsKeyedByTopLevel.takeValueForKey(subPaths, path); } String subPath = KeyPath.indirectPathFromKeyPath(keypath); if (subPath != null) subPaths.addObject(subPath); } return subPathsKeyedByTopLevel; } private NSArray destinationObjectsForRelationship(EORelationship relationship, NSArray sourceObjects) { NSMutableArray destinationObjects = new NSMutableArray(); Enumeration e = sourceObjects.objectEnumerator(); while (e.hasMoreElements()) { EOEnterpriseObject nextEO = (EOEnterpriseObject) e.nextElement(); if (relationship.isToMany()) { destinationObjects.addObjectsFromArray((NSArray) nextEO.valueForKey(path)); } else { Object o = nextEO.valueForKey(path); if (o != null) destinationObjects.addObject(o); } } return destinationObjects; } private void batchFetchRelationshipOnSourceObjects(EORelationship relationship, NSArray sourceObjects, boolean skipFaultedSourceObjects) { EOEnterpriseObject eo = (EOEnterpriseObject) sourceObjects.objectAtIndex(0); EOEditingContext ec = eo.editingContext(); log.debug("Batch fetching '{}' relationship on {}", path, sourceObjects); EODatabaseContext dbContext = ERXEOAccessUtilities.databaseContextForObject(eo); dbContext.lock(); try { ERXEOAccessUtilities.batchFetchRelationship(dbContext, relationship, sourceObjects, ec, skipFaultedSourceObjects); } finally { dbContext.unlock(); } } private NSDictionary splitObjectsByEntity(NSArray objects) { NSMutableDictionary objectsByEntityName = new NSMutableDictionary(); Enumeration e = objects.objectEnumerator(); while (e.hasMoreElements()) { EOEnterpriseObject eo = (EOEnterpriseObject) e.nextElement(); NSMutableArray objectsForEntity = (NSMutableArray) objectsByEntityName.valueForKey(eo.entityName()); if (objectsForEntity == null) { objectsForEntity = new NSMutableArray(); objectsByEntityName.takeValueForKey(objectsForEntity, eo.entityName()); } objectsForEntity.addObject(eo); } return objectsByEntityName; } private static String directPathFromKeyPath(String keypath) { String path = keypath; int indexOfDot = keypath.indexOf("."); if (indexOfDot >= 0) { path = keypath.substring(0, indexOfDot); } return path; } private static String indirectPathFromKeyPath(String keypath) { String indirectPath = null; int indexOfDot = keypath.indexOf("."); if (indexOfDot >= 0) { indirectPath = keypath.substring(indexOfDot + 1); } return indirectPath; } } }