All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.otto.edison.mongo.AbstractMongoRepository Maven / Gradle / Ivy

The newest version!
package de.otto.edison.mongo;

import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.BulkWriteOptions;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.UpdateOptions;
import de.otto.edison.mongo.configuration.MongoProperties;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.conversions.Bson;

import jakarta.annotation.PostConstruct;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.mongodb.client.model.Filters.and;
import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.ReturnDocument.AFTER;
import static de.otto.edison.mongo.UpdateIfMatchResult.CONCURRENTLY_MODIFIED;
import static de.otto.edison.mongo.UpdateIfMatchResult.NOT_FOUND;
import static de.otto.edison.mongo.UpdateIfMatchResult.OK;
import static java.util.Objects.isNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

public abstract class AbstractMongoRepository {

    public static final String ID = "_id";
    public static final String ETAG = "etag";

    protected final MongoProperties mongoProperties;

    private static final boolean DISABLE_PARALLEL_STREAM_PROCESSING = false;
    private static final ReplaceOptions BULK_UPSERT_OPERATION = new ReplaceOptions().upsert(true);
    private static final BulkWriteOptions BULK_WRITE_OPTIONS = new BulkWriteOptions().ordered(false);

    public AbstractMongoRepository(final MongoProperties mongoProperties) {
        this.mongoProperties = mongoProperties;
    }

    @PostConstruct
    public void postConstruct() {
        ensureIndexes();
    }

    /**
     * Find a single value with the specified key, if existing.
     *
     * @param key the key to search for
     * @return an Optional containing the requested value, or {@code Optional.empty()} if no value with this key exists
     */
    public Optional findOne(final K key) {
        return findOne(key, mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS);
    }

    /**
     * Find a single value with the specified key, if existing.
     *
     * @param key      the key to search for
     * @param maxTime  the maximum time for this request
     * @param timeUnit the time unit in which {@code maxTime} is specified
     * @return an Optional containing the requested value, or {@code Optional.empty()} if no value with this key exists
     */
    public Optional findOne(final K key, final long maxTime, final TimeUnit timeUnit) {
        return ofNullable(collection()
                .find(byId(key))
                .maxTime(maxTime, timeUnit)
                .map(this::decode)
                .first());
    }

    /**
     * Convert given {@link Iterable} to a standard Java8-{@link Stream}.
     * The {@link Stream} requests elements from the iterable in a lazy fashion as they will usually,
     * so p.e. passing collection().find() as parameter will not result in the
     * whole collection being read into memory.
     * 

* Parallel processing of the iterable is not used. * * @param iterable any {@link Iterable} * @param the type of elements returned by the iterator * @return a {@link Stream} wrapping the given {@link Iterable} */ protected static Stream toStream(final Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), DISABLE_PARALLEL_STREAM_PROCESSING); } public Stream findAllAsStream() { return findAllAsStream(mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS); } public Stream findAllAsStream(final long maxTime, final TimeUnit timeUnit) { return toStream( collection().find() .maxTime(maxTime, timeUnit)) .map(this::decode); } public List findAll() { return findAll(mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS); } public List findAll(final long maxTime, final TimeUnit timeUnit) { return findAllAsStream(maxTime, timeUnit).collect(toList()); } public Stream findAllAsStream(final int skip, final int limit) { return findAllAsStream(skip, limit, mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS); } public Stream findAllAsStream(final int skip, final int limit, final long maxTime, final TimeUnit timeUnit) { return toStream( getFindIterable(skip, limit) .maxTime(maxTime, timeUnit)) .map(this::decode); } private FindIterable getFindIterable(int skip, int limit) { return collection() .find() .skip(skip) .limit(limit); } public List findAll(final int skip, final int limit) { return findAll(skip, limit, mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS); } public List findAll(final int skip, final int limit, final long maxTime, final TimeUnit timeUnit) { return findAllAsStream(skip, limit, maxTime, timeUnit).collect(toList()); } public V createOrUpdate(final V value) { return createOrUpdate(value, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } public V createOrUpdate(final V value, final long maxTime, final TimeUnit timeUnit) { final Document doc = encode(value); collectionWithWriteTimeout(maxTime, timeUnit) .replaceOne(byId(keyOf(value)), doc, new ReplaceOptions().upsert(true)); return decode(doc); } public void createOrUpdateBulk(final Collection values) { createOrUpdateBulk(values, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } public void createOrUpdateBulk(final Collection values, final long maxTime, final TimeUnit timeUnit) { if (values.isEmpty()) { return; } final List> bulkOperations = values.stream() .map(value -> new ReplaceOneModel<>( eq(ID, keyOf(value)), encode(value), BULK_UPSERT_OPERATION)) .collect(toList()); collectionWithWriteTimeout(maxTime, timeUnit) .bulkWrite(bulkOperations, BULK_WRITE_OPTIONS); } protected MongoCollection collectionWithWriteTimeout(long maxTime, TimeUnit timeUnit) { return collection() .withWriteConcern(collection().getWriteConcern().withWTimeout(maxTime, timeUnit)); } public V create(final V value) { return create(value, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } public V create(final V value, final long maxTime, final TimeUnit timeUnit) { final K key = keyOf(value); if (key != null) { final Document doc = encode(value); collectionWithWriteTimeout(maxTime, timeUnit).insertOne(doc); return decode(doc); } else { throw new NullPointerException("Key must not be null"); } } /** * Updates the document if it is already present in the repository. * * @param value the new value * @return true, if the document was updated, false otherwise. */ public boolean update(final V value) { return update(value, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } /** * Updates the document if it is already present in the repository. * * @param value the new value * @param maxTime max time for the update * @param timeUnit the time unit for the maxTime value * @return true, if the document was updated, false otherwise. */ public boolean update(final V value, final long maxTime, final TimeUnit timeUnit) { final K key = keyOf(value); if (key != null) { return collectionWithWriteTimeout(maxTime, timeUnit) .replaceOne(byId(key), encode(value)) .getModifiedCount() == 1; } else { throw new IllegalArgumentException("Key must not be null"); } } /** * Updates the document if the document's ETAG is matching the given etag (conditional put). *

* Using this method requires that the document contains an "etag" field that is updated if * the document is changed. *

* * @param value the new value * @param eTag the etag used for conditional update * @return {@link UpdateIfMatchResult} */ public UpdateIfMatchResult updateIfMatch(final V value, final String eTag) { return updateIfMatch(value, eTag, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } /** * Updates the document if the document's ETAG is matching the given etag (conditional put). *

* Using this method requires that the document contains an "etag" field that is updated if * the document is changed. *

* * @param value the new value * @param eTag the etag used for conditional update * @param maxTime max time for the update * @param timeUnit the time unit for the maxTime value * @return {@link UpdateIfMatchResult} */ public UpdateIfMatchResult updateIfMatch(final V value, final String eTag, final long maxTime, final TimeUnit timeUnit) { final K key = keyOf(value); if (key == null) { throw new IllegalArgumentException("Key must not be null"); } final Bson query = and(eq(AbstractMongoRepository.ID, key), eq(ETAG, eTag)); final Document updatedETaggable = collectionWithWriteTimeout(maxTime, timeUnit).findOneAndReplace(query, encode(value), new FindOneAndReplaceOptions().returnDocument(AFTER)); if (isNull(updatedETaggable)) { final boolean documentExists = collection() .countDocuments(eq(AbstractMongoRepository.ID, key), new CountOptions().maxTime(maxTime, timeUnit)) != 0; if (documentExists) { return CONCURRENTLY_MODIFIED; } return NOT_FOUND; } return OK; } public long size() { return size(mongoProperties.getDefaultReadTimeout(), TimeUnit.MILLISECONDS); } public long size(final long maxTime, final TimeUnit timeUnit) { return collection().countDocuments(new BsonDocument(), new CountOptions().maxTime(maxTime, timeUnit)); } /** * Deletes the document identified by key. * * @param key the identifier of the deleted document */ public void delete(final K key) { delete(key, mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } /** * Deletes the document identified by key. * * @param key the identifier of the deleted document * @param maxTime max time for the operation * @param timeUnit the time unit for the maxTime value */ public void delete(final K key, final long maxTime, final TimeUnit timeUnit) { collectionWithWriteTimeout(maxTime, timeUnit).deleteOne(byId(key)); } /** * Deletes all documents from this repository. */ public void deleteAll() { deleteAll(mongoProperties.getDefaultWriteTimeout(), TimeUnit.MILLISECONDS); } /** * Deletes all documents from this repository. * * @param maxTime max time for the operation * @param timeUnit the time unit for the maxTime value */ public void deleteAll(final long maxTime, final TimeUnit timeUnit) { collectionWithWriteTimeout(maxTime, timeUnit).deleteMany(matchAll()); } /** * Returns a query that is selecting documents by ID. * * @param key the document's key * @return query Document */ protected Document byId(final K key) { if (key != null) { return new Document(ID, key.toString()); } else { throw new NullPointerException("Key must not be null"); } } /** * Returns a query that is selecting all documents. * * @return query Document */ protected Document matchAll() { return new Document(); } /** * @return the MongoCollection used by this repository to store {@link Document documents} */ protected abstract MongoCollection collection(); /** * Returns the key / identifier from the given value. *

* The key of a document must never be null. *

* * @param value the value * @return key */ protected abstract K keyOf(final V value); /** * Encode a value into a MongoDB {@link Document}. * * @param value the value * @return Document */ protected abstract Document encode(final V value); /** * Decode a MongoDB {@link Document} into a value. * * @param document the Document * @return V */ protected abstract V decode(final Document document); /** * Ensure that the MongoDB indexes required by the repository do exist. *

* This method is called once after startup of the application. *

*/ protected abstract void ensureIndexes(); }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy