package org.molgenis.data.cache.l2; import com.google.common.collect.Iterators; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import org.molgenis.data.*; import org.molgenis.data.transaction.TransactionInformation; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Stream; import static com.google.common.collect.Iterators.partition; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.uniqueIndex; import static java.util.Objects.requireNonNull; import static java.util.Spliterator.ORDERED; import static java.util.Spliterator.SORTED; import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static java.util.stream.StreamSupport.stream; import static org.molgenis.data.RepositoryCapability.CACHEABLE; /** * Adds, removes and retrieves entities from the {@link L2Cache} when a {@link Repository} is * {@link RepositoryCapability#CACHEABLE}. * <p> * Delegates to the underlying repository when an action is not supported by the cache or when the cache doesn't contain * the needed entity. */ public class L2CacheRepositoryDecorator extends AbstractRepositoryDecorator<Entity> { private static final int ID_BATCH_SIZE = 1000; private final L2Cache l2Cache; private final boolean cacheable; private final Repository<Entity> decoratedRepository; private final TransactionInformation transactionInformation; public L2CacheRepositoryDecorator(Repository<Entity> decoratedRepository, L2Cache l2Cache, TransactionInformation transactionInformation) { this.decoratedRepository = requireNonNull(decoratedRepository); this.l2Cache = requireNonNull(l2Cache); this.cacheable = decoratedRepository.getCapabilities().containsAll(newArrayList(CACHEABLE)); this.transactionInformation = transactionInformation; } @Override protected Repository<Entity> delegate() { return decoratedRepository; } /** * Retrieves a single entity by id. * * @param id the entity's ID value * @return the retrieved Entity, or null if not present. */ @Override public Entity findOneById(Object id) { if (cacheable && !transactionInformation.isEntireRepositoryDirty(getName()) && !transactionInformation .isEntityDirty(EntityKey.create(getEntityType(), id))) { return l2Cache.get(delegate(), id); } return delegate().findOneById(id); } /** * Retrieves multiple entities by id. * <p> * If the repository is cacheable and the current transaction hasn't completely dirtied it, will split the stream * into batches and load the batches through {@link #findAllBatch(List)}. * <p> * Otherwise, will delegate this call to the decorated repository. * * @param ids {@link Stream} of ids to retrieve * @return {@link Stream} of retrieved {@link Entity}s, missing ones excluded */ @Override public Stream<Entity> findAll(Stream<Object> ids) { if (cacheable && !transactionInformation.isEntireRepositoryDirty(getName())) { Iterator<List<Object>> idBatches = partition(ids.iterator(), ID_BATCH_SIZE); Iterator<List<Entity>> entityBatches = Iterators.transform(idBatches, this::findAllBatch); return stream(spliteratorUnknownSize(entityBatches, SORTED | ORDERED), false).flatMap(List::stream); } return delegate().findAll(ids); } @Override public Stream<Entity> findAll(Stream<Object> ids, Fetch fetch) { return findAll(ids); } @Override public Entity findOneById(Object id, Fetch fetch) { return findOneById(id); } /** * Retrieves a batch of Entity IDs. * <p> * If currently in transaction, splits the ids into those that have been dirtied in the current transaction * and those that have been left untouched. The untouched ids are loaded through the cache, the dirtied ids * are loaded from the decorated repository directly. * * @param ids list of entity IDs to retrieve * @return List of {@link Entity}s, missing ones excluded. */ private List<Entity> findAllBatch(List<Object> ids) { String entityName = getEntityType().getName(); Multimap<Boolean, Object> partitionedIds = Multimaps .index(ids, id -> transactionInformation.isEntityDirty(EntityKey.create(entityName, id))); Collection<Object> cleanIds = partitionedIds.get(false); Collection<Object> dirtyIds = partitionedIds.get(true); Map<Object, Entity> result = newHashMap( uniqueIndex(l2Cache.getBatch(delegate(), cleanIds), Entity::getIdValue)); result.putAll(delegate().findAll(dirtyIds.stream()).collect(toMap(Entity::getIdValue, e -> e))); return ids.stream().filter(result::containsKey).map(result::get).collect(toList()); } }