Compare commits

...

35 Commits

Author SHA1 Message Date
Mark Paluch
bd8bd4f568 DATAMONGO-2121 - Release version 2.1.3 (Lovelace SR3). 2018-11-27 13:43:24 +01:00
Mark Paluch
c75f29dc42 DATAMONGO-2121 - Prepare 2.1.3 (Lovelace SR3). 2018-11-27 13:42:17 +01:00
Mark Paluch
e493af7266 DATAMONGO-2121 - Updated changelog. 2018-11-27 13:42:07 +01:00
Mark Paluch
8d892e5924 DATAMONGO-2109 - Updated changelog. 2018-11-27 12:36:47 +01:00
Mark Paluch
053299f243 DATAMONGO-2110 - Updated changelog. 2018-11-27 11:27:21 +01:00
Mark Paluch
872659cc00 DATAMONGO-2119 - Polishing.
Convert anonymous JSON callback class into a private static one. Use an expressive Pattern constant.

Original pull request: #621.
2018-11-23 09:48:26 +01:00
Christoph Strobl
96978a6194 DATAMONGO-2119 - Allow SpEL usage for annotated $regex query.
Original pull request: #621.
2018-11-23 09:48:26 +01:00
Oliver Drotbohm
2253d3e301 DATAMONGO-2108 - Fixed broken auditing for entities using optimistic locking.
The previous implementation of MongoTemplate.doSaveVersioned(…) prematurely initialized the version property so that the entity wasn't considered new by the auditing subsystem. Even worse, for primitive version properties, the initialization kept the property at a value of 0, so that the just persisted entity was still considered new. This mean that via the repository route, inserts are triggered even for subsequent attempts to save an entity which caused duplicate key exceptions.

We now make sure we fire the BeforeConvertEvent before the version property is initialized or updated. Also, the initialization of the property now sets primitive properties to 1 initially.

Added integration tests for the auditing via MongoOperations and repositories.
2018-11-22 15:05:31 +01:00
Mark Paluch
5982ee84f7 DATAMONGO-2130 - Polishing.
Replace duplicate checks to ClientSession.hasActiveTransaction() with MongoResourceHolder.hasActiveTransaction(). Introduce MongoResourceHolder.getRequiredSession() to avoid nullability warnings.

Original pull request: #618.
2018-11-16 12:58:40 +01:00
Christoph Strobl
dd2af6462d DATAMONGO-2130 - Polishing.
Set timeout for InetAdress host lookup to reduce test execution time.

Original pull request: #618.
2018-11-16 12:58:40 +01:00
Christoph Strobl
622643bf24 DATAMONGO-2130 - Fix Repository count & exists inside transaction.
We now make sure invocations on repository count and exists methods delegate to countDocuments when inside a transaction.

Original pull request: #618.
2018-11-16 12:58:40 +01:00
Oliver Drotbohm
51cc55baac DATAMONGO-2135 - Default to intermediate List for properties typed to Collection.
We now defensively create a List rather than a LinkedHashSet (which Spring's CollectionFactory.createCollection(…) defaults to) to make sure we're not accidentally dropping values that are considered equal according to their Java class definition.
2018-11-15 15:26:36 +01:00
Mark Paluch
0b106e5649 DATAMONGO-2107 - After release cleanups. 2018-10-29 13:59:17 +01:00
Mark Paluch
8975d93ab3 DATAMONGO-2107 - Prepare next development iteration. 2018-10-29 13:59:15 +01:00
Mark Paluch
e25b6c49f5 DATAMONGO-2107 - Release version 2.1.2 (Lovelace SR2). 2018-10-29 12:53:51 +01:00
Mark Paluch
7a70c205de DATAMONGO-2107 - Prepare 2.1.2 (Lovelace SR2). 2018-10-29 12:52:54 +01:00
Mark Paluch
6045efa450 DATAMONGO-2107 - Updated changelog. 2018-10-29 12:52:45 +01:00
Mark Paluch
7b0816b3ee DATAMONGO-2118 - Polishing.
Fix typo in reactive repositories reference documentation.

Original pull request: #611.
2018-10-26 10:08:03 +02:00
Mona Mohamadinia
14e4ea736d DATAMONGO-2118 - Fix typo in repositories reference documentation.
Original pull request: #611.
2018-10-26 10:08:03 +02:00
Mark Paluch
32e7d9ab7f DATAMONGO-2098 - Polishing.
Annotate methods and parameters with Nullable. Use diamond syntax where appropriate.

Original pull request: #612.
2018-10-25 15:35:26 +02:00
Zied Yaich
7f35ad9e45 DATAMONGO-2098 - Fix typo in MappingMongoConverterParser method.
Original pull request: #612.
2018-10-25 15:35:26 +02:00
Mark Paluch
60228f6e5a DATAMONGO-2113 - Polishing.
Increase subscription await timeout to allow for slow system processing such as on TravisCI.

Original pull request: #615.
2018-10-25 14:33:28 +02:00
Christoph Strobl
7604492b7f DATAMONGO-2113 - Polishing.
Use AssertJ in tests.

Original pull request: #615.
2018-10-25 14:33:28 +02:00
Christoph Strobl
4680fe0e77 DATAMONGO-2113 - Fix resumeTimestamp conversion for change streams.
We now use the first 32 bits of the timestamp to create the instant and ignore the ordinal value.

Original pull request: #615.
2018-10-25 14:33:28 +02:00
Mark Paluch
b4228c88d3 DATAMONGO-2083 - Updated changelog. 2018-10-15 14:19:03 +02:00
Mark Paluch
f6ef8c94c8 DATAMONGO-2084 - Updated changelog. 2018-10-15 12:46:24 +02:00
Mark Paluch
0d0dafa85e DATAMONGO-2094 - After release cleanups. 2018-10-15 11:12:14 +02:00
Mark Paluch
29aa34619f DATAMONGO-2094 - Prepare next development iteration. 2018-10-15 11:12:12 +02:00
Mark Paluch
7f19f769c4 DATAMONGO-2094 - Release version 2.1.1 (Lovelace SR1). 2018-10-15 10:42:04 +02:00
Mark Paluch
a40e89d90a DATAMONGO-2094 - Prepare 2.1.1 (Lovelace SR1). 2018-10-15 10:40:57 +02:00
Mark Paluch
6b2350200a DATAMONGO-2094 - Updated changelog. 2018-10-15 10:40:53 +02:00
Mark Paluch
fb50b0f6e7 DATAMONGO-2096 - Polishing.
Migrate assertions to AssertJ.

Original pull request: #613.
2018-10-05 15:02:38 +02:00
Christoph Strobl
ab568229b5 DATAMONGO-2096 - Fix target field name for GraphLookup aggregation operation.
We now make sure to use the target field name instead of the alias when processing GraphLookupOperation.

Original pull request: #613.
2018-10-05 15:02:38 +02:00
Mark Paluch
7f9c1bd774 DATAMONGO-2061 - After release cleanups. 2018-09-21 07:46:17 -04:00
Mark Paluch
670a0978da DATAMONGO-2061 - Prepare next development iteration. 2018-09-21 07:46:16 -04:00
36 changed files with 622 additions and 157 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
</parent>
<modules>
@@ -27,7 +27,7 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.1.0.RELEASE</springdata.commons>
<springdata.commons>2.1.3.RELEASE</springdata.commons>
<mongo>3.8.2</mongo>
<mongo.reactivestreams>1.9.2</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -50,7 +50,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
</dependency>
<!-- reactive -->

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.RELEASE</version>
<version>2.1.3.RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -110,7 +110,7 @@ public class MongoDatabaseUtils {
ClientSession session = doGetSession(factory, sessionSynchronization);
if(session == null) {
if (session == null) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
@@ -118,6 +118,25 @@ public class MongoDatabaseUtils {
return StringUtils.hasText(dbName) ? factoryToUse.getDb(dbName) : factoryToUse.getDb();
}
/**
* Check if the {@link MongoDbFactory} is actually bound to a {@link ClientSession} that has an active transaction, or
* if a {@link TransactionSynchronization} has been registered for the {@link MongoDbFactory resource} and if the
* associated {@link ClientSession} has an {@link ClientSession#hasActiveTransaction() active transaction}.
*
* @param dbFactory the resource to check transactions for. Must not be {@literal null}.
* @return {@literal true} if the factory has an ongoing transaction.
* @since 2.1.3
*/
public static boolean isTransactionActive(MongoDbFactory dbFactory) {
if (dbFactory.isTransactionActive()) {
return true;
}
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory);
return resourceHolder != null && resourceHolder.hasActiveTransaction();
}
@Nullable
private static ClientSession doGetSession(MongoDbFactory dbFactory, SessionSynchronization sessionSynchronization) {
@@ -140,7 +159,7 @@ public class MongoDatabaseUtils {
// init a non native MongoDB transaction by registering a MongoSessionSynchronization
resourceHolder = new MongoResourceHolder(createClientSession(dbFactory), dbFactory);
resourceHolder.getSession().startTransaction();
resourceHolder.getRequiredSession().startTransaction();
TransactionSynchronizationManager
.registerSynchronization(new MongoSessionSynchronization(resourceHolder, dbFactory));
@@ -187,8 +206,8 @@ public class MongoDatabaseUtils {
@Override
protected void processResourceAfterCommit(MongoResourceHolder resourceHolder) {
if (isTransactionActive(resourceHolder)) {
resourceHolder.getSession().commitTransaction();
if (resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().commitTransaction();
}
}
@@ -199,8 +218,8 @@ public class MongoDatabaseUtils {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) {
resourceHolder.getSession().abortTransaction();
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && this.resourceHolder.hasActiveTransaction()) {
resourceHolder.getRequiredSession().abortTransaction();
}
super.afterCompletion(status);
@@ -214,17 +233,8 @@ public class MongoDatabaseUtils {
protected void releaseResource(MongoResourceHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.getSession().close();
resourceHolder.getRequiredSession().close();
}
}
private boolean isTransactionActive(MongoResourceHolder resourceHolder) {
if (!resourceHolder.hasSession()) {
return false;
}
return resourceHolder.getSession().hasActiveTransaction();
}
}
}

View File

@@ -108,4 +108,15 @@ public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvi
* @since 2.1
*/
MongoDbFactory withSession(ClientSession session);
/**
* Returns if the given {@link MongoDbFactory} is bound to a {@link ClientSession} that has an
* {@link ClientSession#hasActiveTransaction() active transaction}.
*
* @return {@literal true} if there's an active transaction, {@literal false} otherwise.
* @since 2.1.3
*/
default boolean isTransactionActive() {
return false;
}
}

View File

@@ -28,6 +28,7 @@ import com.mongodb.client.ClientSession;
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see MongoTransactionManager
* @see org.springframework.data.mongodb.core.MongoTemplate
@@ -57,6 +58,22 @@ class MongoResourceHolder extends ResourceHolderSupport {
return session;
}
/**
* @return the required associated {@link ClientSession}.
* @throws IllegalStateException if no {@link ClientSession} is associated with this {@link MongoResourceHolder}.
* @since 2.1.3
*/
ClientSession getRequiredSession() {
ClientSession session = getSession();
if (session == null) {
throw new IllegalStateException("No session available!");
}
return session;
}
/**
* @return the associated {@link MongoDbFactory}.
*/
@@ -101,7 +118,21 @@ class MongoResourceHolder extends ResourceHolderSupport {
return false;
}
return hasServerSession() && !getSession().getServerSession().isClosed();
return hasServerSession() && !getRequiredSession().getServerSession().isClosed();
}
/**
* @return {@literal true} if the session has an active transaction.
* @since 2.1.3
* @see #hasActiveSession()
*/
boolean hasActiveTransaction() {
if (!hasActiveSession()) {
return false;
}
return getRequiredSession().hasActiveTransaction();
}
/**
@@ -111,7 +142,7 @@ class MongoResourceHolder extends ResourceHolderSupport {
boolean hasServerSession() {
try {
return getSession().getServerSession() != null;
return getRequiredSession().getServerSession() != null;
} catch (IllegalStateException serverSessionClosed) {
// ignore
}

View File

@@ -60,6 +60,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -75,6 +76,7 @@ import org.w3c.dom.Element;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Zied Yaich
*/
public class MappingMongoConverterParser implements BeanDefinitionParser {
@@ -159,6 +161,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
@Nullable
private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
String disableValidation = element.getAttribute("disable-validation");
@@ -180,6 +183,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
@Nullable
private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) {
if (!JSR_303_PRESENT) {
@@ -197,7 +201,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
}
public static String potentiallyCreateMappingContext(Element element, ParserContext parserContext,
BeanDefinition conversionsDefinition, String converterId) {
@Nullable BeanDefinition conversionsDefinition, @Nullable String converterId) {
String ctxRef = element.getAttribute("mapping-context-ref");
@@ -211,7 +215,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder
.genericBeanDefinition(MongoMappingContext.class);
Set<String> classesToAdd = getInititalEntityClasses(element);
Set<String> classesToAdd = getInitialEntityClasses(element);
if (classesToAdd != null) {
mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd);
@@ -262,6 +266,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
}
}
@Nullable
private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
List<Element> customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters");
@@ -269,7 +274,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
if (customConvertersElements.size() == 1) {
Element customerConvertersElement = customConvertersElements.get(0);
ManagedList<BeanMetadataElement> converterBeans = new ManagedList<BeanMetadataElement>();
ManagedList<BeanMetadataElement> converterBeans = new ManagedList<>();
List<Element> converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter");
if (converterElements != null) {
@@ -285,9 +290,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
provider.addExcludeFilter(new NegatingFilter(new AssignableTypeFilter(Converter.class),
new AssignableTypeFilter(GenericConverter.class)));
for (BeanDefinition candidate : provider.findCandidateComponents(packageToScan)) {
converterBeans.add(candidate);
}
converterBeans.addAll(provider.findCandidateComponents(packageToScan));
}
BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(MongoCustomConversions.class);
@@ -304,7 +307,8 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return null;
}
private static Set<String> getInititalEntityClasses(Element element) {
@Nullable
private static Set<String> getInitialEntityClasses(Element element) {
String basePackage = element.getAttribute(BASE_PACKAGE);
@@ -317,7 +321,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
Set<String> classes = new ManagedSet<String>();
Set<String> classes = new ManagedSet<>();
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
classes.add(candidate.getBeanClassName());
}
@@ -325,6 +329,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
return classes;
}
@Nullable
public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
String converterRef = element.getAttribute("ref");
@@ -375,7 +380,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser {
Assert.notNull(filters, "TypeFilters must not be null");
this.delegates = new HashSet<TypeFilter>(Arrays.asList(filters));
this.delegates = new HashSet<>(Arrays.asList(filters));
}
/*

View File

@@ -83,7 +83,9 @@ public class ChangeStreamEvent<T> {
*/
@Nullable
public Instant getTimestamp() {
return raw != null && raw.getClusterTime() != null ? Instant.ofEpochMilli(raw.getClusterTime().getValue()) : null;
return raw != null && raw.getClusterTime() != null
? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) : null;
}
/**

View File

@@ -242,6 +242,14 @@ class EntityOperations {
* @return
*/
T getBean();
/**
* Returns whether the entity is considered to be new.
*
* @return
* @since 2.1.2
*/
boolean isNew();
}
/**
@@ -387,6 +395,15 @@ class EntityOperations {
public T getBean() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew()
*/
@Override
public boolean isNew() {
return map.get(ID_FIELD) != null;
}
}
private static class SimpleMappedEntity<T extends Map<String, Object>> extends UnmappedEntity<T> {
@@ -549,6 +566,15 @@ class EntityOperations {
public T getBean() {
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#isNew()
*/
@Override
public boolean isNew() {
return entity.isNew(propertyAccessor.getBean());
}
}
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
@@ -631,7 +657,9 @@ class EntityOperations {
return propertyAccessor.getBean();
}
propertyAccessor.setProperty(entity.getRequiredVersionProperty(), 0);
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
propertyAccessor.setProperty(versionProperty, versionProperty.getType().isPrimitive() ? 1 : 0);
return propertyAccessor.getBean();
}

View File

@@ -233,6 +233,15 @@ public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
return delegate.withSession(session);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#isTransactionActive()
*/
@Override
public boolean isTransactionActive() {
return session != null && session.hasActiveTransaction();
}
private MongoDatabase proxyMongoDatabase(MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
@@ -241,7 +250,8 @@ public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session, MongoCollection<?> collection) {
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session,
MongoCollection<?> collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}

View File

@@ -1119,7 +1119,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document document = queryMapper.getMappedObject(query.getQueryObject(),
Optional.ofNullable(entityClass).map(it -> mappingContext.getPersistentEntity(entityClass)));
return execute(collectionName, collection -> collection.count(document, options));
return doCount(collectionName, document, options);
}
protected long doCount(String collectionName, Document filter, CountOptions options) {
if (MongoDatabaseUtils.isTransactionActive(getMongoDbFactory())) {
return execute(collectionName, collection -> collection.countDocuments(filter, options));
}
return execute(collectionName, collection -> collection.count(filter, options));
}
/*
@@ -1203,20 +1212,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
protected <T> T doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
AdaptibleEntity<T> entity = operations.forEntity(objectToSave, mongoConverter.getConversionService());
T toSave = entity.initializeVersionProperty();
BeforeConvertEvent<T> event = new BeforeConvertEvent<>(toSave, collectionName);
toSave = maybeEmitEvent(event).getSource();
BeforeConvertEvent<T> event = new BeforeConvertEvent<>(objectToSave, collectionName);
T toConvert = maybeEmitEvent(event).getSource();
AdaptibleEntity<T> entity = operations.forEntity(toConvert, mongoConverter.getConversionService());
entity.assertUpdateableIdIfNotSet();
T initialized = entity.initializeVersionProperty();
Document dbDoc = entity.toMappedDocument(writer).getDocument();
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
Object id = insertDocument(collectionName, dbDoc, toSave.getClass());
maybeEmitEvent(new BeforeSaveEvent<>(initialized, dbDoc, collectionName));
Object id = insertDocument(collectionName, dbDoc, initialized.getClass());
T saved = populateIdIfNecessary(toSave, id);
T saved = populateIdIfNecessary(initialized, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
@@ -1348,38 +1356,36 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
@SuppressWarnings("unchecked")
private <T> T doSaveVersioned(AdaptibleEntity<T> source, String collectionName) {
Number number = source.getVersion();
if (number != null) {
// Create query for entity with the id and old version
Query query = source.getQueryForVersion();
// Bump version number
T toSave = source.incrementVersion();
toSave = maybeEmitEvent(new BeforeConvertEvent<T>(toSave, collectionName)).getSource();
source.assertUpdateableIdIfNotSet();
MappedDocument mapped = source.toMappedDocument(mongoConverter);
maybeEmitEvent(new BeforeSaveEvent<>(toSave, mapped.getDocument(), collectionName));
Update update = mapped.updateWithoutId();
UpdateResult result = doUpdate(collectionName, query, update, toSave.getClass(), false, false);
if (result.getModifiedCount() == 0) {
throw new OptimisticLockingFailureException(
String.format("Cannot save entity %s with version %s to collection %s. Has it been modified meanwhile?",
source.getId(), number, collectionName));
}
maybeEmitEvent(new AfterSaveEvent<>(toSave, mapped.getDocument(), collectionName));
return toSave;
if (source.isNew()) {
return (T) doInsert(collectionName, source.getBean(), this.mongoConverter);
}
return (T) doInsert(collectionName, source.getBean(), this.mongoConverter);
// Create query for entity with the id and old version
Query query = source.getQueryForVersion();
// Bump version number
T toSave = source.incrementVersion();
toSave = maybeEmitEvent(new BeforeConvertEvent<T>(toSave, collectionName)).getSource();
source.assertUpdateableIdIfNotSet();
MappedDocument mapped = source.toMappedDocument(mongoConverter);
maybeEmitEvent(new BeforeSaveEvent<>(toSave, mapped.getDocument(), collectionName));
Update update = mapped.updateWithoutId();
UpdateResult result = doUpdate(collectionName, query, update, toSave.getClass(), false, false);
if (result.getModifiedCount() == 0) {
throw new OptimisticLockingFailureException(
String.format("Cannot save entity %s with version %s to collection %s. Has it been modified meanwhile?",
source.getId(), source.getVersion(), collectionName));
}
maybeEmitEvent(new AfterSaveEvent<>(toSave, mapped.getDocument(), collectionName));
return toSave;
}
protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
@@ -2820,21 +2826,23 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
/**
* Optimized {@link CollectionCallback} that takes an already mappend query and a nullable
* Optimized {@link CollectionCallback} that takes an already mapped query and a nullable
* {@link com.mongodb.client.model.Collation} to execute a count query limited to one element.
*
* @author Christoph Strobl
* @since 2.0
*/
@RequiredArgsConstructor
private static class ExistsCallback implements CollectionCallback<Boolean> {
private class ExistsCallback implements CollectionCallback<Boolean> {
private final Document mappedQuery;
private final com.mongodb.client.model.Collation collation;
@Override
public Boolean doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
return collection.count(mappedQuery, new CountOptions().limit(1).collation(collation)) > 0;
return doCount(collection.getNamespace().getCollectionName(), mappedQuery,
new CountOptions().limit(1).collation(collation)) > 0;
}
}
@@ -3343,23 +3351,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoTemplate#count(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
* @see org.springframework.data.mongodb.core.MongoTemplate#doCount(java.lang.String, org.bson.Document, com.mongodb.client.model.CountOptions)
*/
@Override
@SuppressWarnings("unchecked")
public long count(Query query, @Nullable Class<?> entityClass, String collectionName) {
protected long doCount(String collectionName, Document filter, CountOptions options) {
if (!session.hasActiveTransaction()) {
return super.count(query, entityClass, collectionName);
return super.doCount(collectionName, filter, options);
}
CountOptions options = new CountOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
Document document = delegate.queryMapper.getMappedObject(query.getQueryObject(),
Optional.ofNullable(entityClass).map(it -> delegate.mappingContext.getPersistentEntity(entityClass)));
return execute(collectionName, collection -> collection.countDocuments(document, options));
return execute(collectionName, collection -> collection.countDocuments(filter, options));
}
}
}

View File

@@ -1915,7 +1915,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
publisher = options.getResumeToken().map(BsonValue::asDocument).map(publisher::resumeAfter).orElse(publisher);
publisher = options.getCollation().map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher);
publisher = options.getResumeTimestamp().map(it -> new BsonTimestamp(it.toEpochMilli()))
publisher = options.getResumeTimestamp().map(it -> new BsonTimestamp((int) it.getEpochSecond(), 0))
.map(publisher::startAtOperationTime).orElse(publisher);
publisher = publisher.fullDocument(options.getFullDocumentLookup().orElse(fullDocument));

View File

@@ -103,8 +103,8 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
graphLookup.put("connectFromField", connectFrom.getName());
graphLookup.put("connectToField", connectTo.getName());
graphLookup.put("connectFromField", connectFrom.getTarget());
graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName());
if (maxDepth != null) {
@@ -112,7 +112,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
}
if (depthField != null) {
graphLookup.put("depthField", depthField.getName());
graphLookup.put("depthField", depthField.getTarget());
}
if (restrictSearchWithMatch != null) {

View File

@@ -248,7 +248,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private ParameterValueProvider<MongoPersistentProperty> getParameterProvider(MongoPersistentEntity<?> entity,
DocumentAccessor source, SpELExpressionEvaluator evaluator, ObjectPath path) {
AssociationAwareMongoDbPropertyValueProvider provider = new AssociationAwareMongoDbPropertyValueProvider(source,
evaluator, path);
PersistentEntityParameterValueProvider<MongoPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
@@ -287,8 +286,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
// Make sure id property is set before all other properties
Object rawId = readAndPopulateIdentifier(accessor, documentAccessor, entity,
path, evaluator);
Object rawId = readAndPopulateIdentifier(accessor, documentAccessor, entity, path, evaluator);
ObjectPath currentPath = path.push(accessor.getBean(), entity, rawId);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(documentAccessor, evaluator,
@@ -620,7 +618,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return;
}
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass())
MongoPersistentEntity<?> entity = isSubTypeOf(obj.getClass(), prop.getType())
? mappingContext.getRequiredPersistentEntity(obj.getClass())
: mappingContext.getRequiredPersistentEntity(type);
@@ -632,10 +630,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
accessor.put(prop, document);
}
private boolean isSubtype(Class<?> left, Class<?> right) {
return left.isAssignableFrom(right) && !left.equals(right);
}
/**
* Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
@@ -1012,7 +1006,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(path, "Object path must not be null!");
Class<?> collectionType = targetType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) //
collectionType = isSubTypeOf(collectionType, Collection.class) //
? collectionType //
: List.class;
@@ -1630,6 +1624,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return true;
}
/**
* Returns whether the given type is a sub type of the given reference, i.e. assignable but not the exact same type.
*
* @param type must not be {@literal null}.
* @param reference must not be {@literal null}.
* @return
*/
private static boolean isSubTypeOf(Class<?> type, Class<?> reference) {
return !type.equals(reference) && reference.isAssignableFrom(type);
}
/**
* Marker class used to indicate we have a non root document object here that might be used within an update - so we
* need to preserve type hints for potential nested elements but need to remove it on top level.

View File

@@ -19,6 +19,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Currency;
@@ -26,6 +27,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.bson.BsonTimestamp;
import org.bson.Document;
import org.bson.types.Binary;
import org.bson.types.Code;
@@ -86,6 +88,7 @@ abstract class MongoConverters {
converters.add(LongToAtomicLongConverter.INSTANCE);
converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(BinaryToByteArrayConverter.INSTANCE);
converters.add(BsonTimestampToInstantConverter.INSTANCE);
return converters;
}
@@ -465,4 +468,22 @@ abstract class MongoConverters {
return source.getData();
}
}
/**
* {@link Converter} implementation converting {@link BsonTimestamp} into {@link Instant}.
*
* @author Christoph Strobl
* @since 2.1.2
*/
@ReadingConverter
enum BsonTimestampToInstantConverter implements Converter<BsonTimestamp, Instant> {
INSTANCE;
@Nullable
@Override
public Instant convert(BsonTimestamp source) {
return Instant.ofEpochSecond(source.getTime(), 0);
}
}
}

View File

@@ -115,7 +115,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
.orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
: FullDocument.UPDATE_LOOKUP);
startAt = changeStreamOptions.getResumeTimestamp().map(Instant::toEpochMilli).map(BsonTimestamp::new)
startAt = changeStreamOptions.getResumeTimestamp().map(it -> new BsonTimestamp((int) it.getEpochSecond(), 0))
.orElse(null);
}

View File

@@ -20,7 +20,9 @@ import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.bson.BSON;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +39,7 @@ import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONCallback;
/**
* Query to use a plain JSON String to create the {@link Query} to actually execute.
@@ -225,7 +228,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
String transformedInput = transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
String parseableInput = makeParameterReferencesParseable(transformedInput);
collectParameterReferencesIntoBindings(bindings, JSON.parse(parseableInput));
collectParameterReferencesIntoBindings(bindings,
JSON.parse(parseableInput, new LenientPatternDecodingCallback()));
return transformedInput;
}
@@ -360,6 +364,43 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
}
}
/**
* {@link JSONCallback} with lenient handling for {@link PatternSyntaxException} falling back to a placeholder
* {@link Pattern} for intermediate query document rendering.
*/
private static class LenientPatternDecodingCallback extends JSONCallback {
private static final Pattern EMPTY_MARKER = Pattern.compile("__Spring_Data_MongoDB_Bind_Marker__");
/*
* (non-Javadoc)
* @see com.mongodb.util.JSONCallback#objectDone()
*/
@Override
public Object objectDone() {
return exceptionSwallowingStackReducingObjectDone();
}
private Object exceptionSwallowingStackReducingObjectDone/*CauseWeJustNeedTheStructureNotTheActualValue*/() {
Object value;
try {
return super.objectDone();
} catch (PatternSyntaxException e) {
value = EMPTY_MARKER;
}
if (!isStackEmpty()) {
_put(curName(), value);
} else {
value = !BSON.hasDecodeHooks() ? value : BSON.applyDecodingHooks(value);
setRoot(value);
}
return value;
}
}
/**
* A generic parameter binding with name or position information.
*

View File

@@ -137,7 +137,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
*/
@Override
public long count() {
return mongoOperations.getCollection(entityInformation.getCollectionName()).count();
return mongoOperations.count(new Query(), entityInformation.getCollectionName());
}
/*

View File

@@ -78,6 +78,39 @@ public class MongoDatabaseUtilsUnitTests {
assertFalse(TransactionSynchronizationManager.isActualTransactionActive());
}
@Test // DATAMONGO-2130
public void isTransactionActiveShouldDetectTxViaFactory() {
when(dbFactory.isTransactionActive()).thenReturn(true);
assertThat(MongoDatabaseUtils.isTransactionActive(dbFactory)).isTrue();
}
@Test // DATAMONGO-2130
public void isTransactionActiveShouldReturnFalseIfNoTxActive() {
when(dbFactory.isTransactionActive()).thenReturn(false);
assertThat(MongoDatabaseUtils.isTransactionActive(dbFactory)).isFalse();
}
@Test // DATAMONGO-2130
public void isTransactionActiveShouldLookupTxForActiveTransactionSynchronizationViaTxManager() {
when(dbFactory.isTransactionActive()).thenReturn(false);
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(MongoDatabaseUtils.isTransactionActive(dbFactory)).isTrue();
}
});
}
@Test // DATAMONGO-1920
public void shouldNotStartSessionWhenNoTransactionOngoing() {

View File

@@ -15,11 +15,13 @@
*/
package org.springframework.data.mongodb.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
import java.util.Optional;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
@@ -28,15 +30,18 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mongodb.core.AuditablePerson;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
/**
@@ -51,6 +56,9 @@ public class AuditingViaJavaConfigRepositoriesTests {
@Autowired AuditablePersonRepository auditablePersonRepository;
@Autowired AuditorAware<AuditablePerson> auditorAware;
@Autowired MongoMappingContext context;
@Autowired MongoOperations operations;
AuditablePerson auditor;
@Configuration
@@ -107,6 +115,61 @@ public class AuditingViaJavaConfigRepositoriesTests {
new AnnotationConfigApplicationContext(SimpleConfig.class);
}
@Test // DATAMONGO-2139
public void auditingWorksForVersionedEntityWithWrapperVersion() {
verifyAuditingViaVersionProperty(new VersionedAuditablePerson(), //
it -> it.version, //
auditablePersonRepository::save, //
null, 0L, 1L);
}
@Test // DATAMONGO-2139
public void auditingWorksForVersionedEntityWithSimpleVersion() {
verifyAuditingViaVersionProperty(new SimpleVersionedAuditablePerson(), //
it -> it.version, //
auditablePersonRepository::save, //
0L, 1L, 2L);
}
@Test // DATAMONGO-2139
public void auditingWorksForVersionedEntityWithWrapperVersionOnTemplate() {
verifyAuditingViaVersionProperty(new VersionedAuditablePerson(), //
it -> it.version, //
operations::save, //
null, 0L, 1L);
}
@Test // DATAMONGO-2139
public void auditingWorksForVersionedEntityWithSimpleVersionOnTemplate() {
verifyAuditingViaVersionProperty(new SimpleVersionedAuditablePerson(), //
it -> it.version, //
operations::save, //
0L, 1L, 2L);
}
private <T extends AuditablePerson> void verifyAuditingViaVersionProperty(T instance,
Function<T, Object> versionExtractor, Function<T, T> persister, Object... expectedValues) {
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(instance.getClass());
assertThat(versionExtractor.apply(instance)).isEqualTo(expectedValues[0]);
assertThat(entity.isNew(instance)).isTrue();
instance = auditablePersonRepository.save(instance);
assertThat(versionExtractor.apply(instance)).isEqualTo(expectedValues[1]);
assertThat(entity.isNew(instance)).isFalse();
instance = auditablePersonRepository.save(instance);
assertThat(versionExtractor.apply(instance)).isEqualTo(expectedValues[2]);
assertThat(entity.isNew(instance)).isFalse();
}
@Repository
static interface AuditablePersonRepository extends MongoRepository<AuditablePerson, String> {}
@@ -128,4 +191,12 @@ public class AuditingViaJavaConfigRepositoriesTests {
return "database";
}
}
static class VersionedAuditablePerson extends AuditablePerson {
@Version Long version;
}
static class SimpleVersionedAuditablePerson extends AuditablePerson {
@Version long version;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.config;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
@@ -124,7 +125,8 @@ public class ServerAddressPropertyEditorUnitTests {
* We can't tell whether the last part of the hostAddress represents a port or not.
*/
@Test // DATAMONGO-808
public void shouldFailToHandleAmbiguousIPv6HostaddressLongWithoutPortAndWithoutBrackets() throws UnknownHostException {
public void shouldFailToHandleAmbiguousIPv6HostaddressLongWithoutPortAndWithoutBrackets()
throws UnknownHostException {
expectedException.expect(IllegalArgumentException.class);
@@ -173,9 +175,9 @@ public class ServerAddressPropertyEditorUnitTests {
for (String hostname : hostnames) {
try {
InetAddress.getByName(hostname);
InetAddress.getByName(hostname).isReachable(1500);
Assert.fail("Supposedly unresolveable hostname '" + hostname + "' can be resolved.");
} catch (UnknownHostException expected) {
} catch (IOException expected) {
// ok
}
}

View File

@@ -22,6 +22,7 @@ import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import com.mongodb.MongoNamespace;
import lombok.Data;
import java.math.BigInteger;
@@ -135,6 +136,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(collection.find(any(org.bson.Document.class), any(Class.class))).thenReturn(findIterable);
when(collection.mapReduce(any(), any(), eq(Document.class))).thenReturn(mapReduceIterable);
when(collection.count(any(Bson.class), any(CountOptions.class))).thenReturn(1L);
when(collection.getNamespace()).thenReturn(new MongoNamespace("db.mock-collection"));
when(collection.aggregate(any(List.class), any())).thenReturn(aggregateIterable);
when(collection.withReadPreference(any())).thenReturn(collection);
when(findIterable.projection(any())).thenReturn(findIterable);

View File

@@ -29,6 +29,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
@@ -1355,7 +1356,7 @@ public class ReactiveMongoTemplateTests {
}
}
@Test // DATAMONGO-2012
@Test // DATAMONGO-2012, DATAMONGO-2113
public void resumesAtTimestampCorrectly() throws InterruptedException {
Assumptions.assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
@@ -1372,7 +1373,7 @@ public class ReactiveMongoTemplateTests {
Person person2 = new Person("Data", 37);
Person person3 = new Person("MongoDB", 39);
StepVerifier.create(template.save(person1)).expectNextCount(1).verifyComplete();
StepVerifier.create(template.save(person1).delayElement(Duration.ofSeconds(1))).expectNextCount(1).verifyComplete();
StepVerifier.create(template.save(person2)).expectNextCount(1).verifyComplete();
Thread.sleep(500); // just give it some time to link receive all events

View File

@@ -15,19 +15,15 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import static org.hamcrest.core.Is.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.util.Arrays;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.query.Criteria;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;
/**
* Unit tests for {@link GraphLookupOperation}.
*
@@ -54,8 +50,7 @@ public class GraphLookupOperationUnitTests {
.as("reportingHierarchy");
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document,
isBsonObject().containing("$graphLookup.depthField", "depth").containing("$graphLookup.maxDepth", 42L));
assertThat(document).containsEntry("$graphLookup.depthField", "depth").containsEntry("$graphLookup.maxDepth", 42L);
}
@Test // DATAMONGO-1551
@@ -70,8 +65,7 @@ public class GraphLookupOperationUnitTests {
.as("reportingHierarchy");
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document,
isBsonObject().containing("$graphLookup.restrictSearchWithMatch", new Document("key", "value")));
assertThat(document).containsEntry("$graphLookup.restrictSearchWithMatch", new Document("key", "value"));
}
@Test // DATAMONGO-1551
@@ -86,9 +80,9 @@ public class GraphLookupOperationUnitTests {
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document,
is(Document.parse("{ $graphLookup : { from: \"employees\", startWith: [\"$reportsTo\", \"$boss\"], "
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }")));
assertThat(document)
.isEqualTo(Document.parse("{ $graphLookup : { from: \"employees\", startWith: [\"$reportsTo\", \"$boss\"], "
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }"));
}
@Test // DATAMONGO-1551
@@ -103,9 +97,8 @@ public class GraphLookupOperationUnitTests {
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document,
is(Document.parse("{ $graphLookup : { from: \"employees\", startWith: [\"$reportsTo\", { $literal: \"$boss\"}], "
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }")));
assertThat(document).containsEntry("$graphLookup.startWith",
Arrays.asList("$reportsTo", new Document("$literal", "$boss")));
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1551
@@ -131,7 +124,39 @@ public class GraphLookupOperationUnitTests {
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document, is(Document.parse("{ $graphLookup : { from: \"employees\", startWith: { $literal: \"hello\"}, "
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }")));
assertThat(document).containsEntry("$graphLookup.startWith", new Document("$literal", "hello"));
}
@Test // DATAMONGO-2096
public void connectFromShouldUseTargetFieldInsteadOfAlias() {
AggregationOperation graphLookupOperation = Aggregation.graphLookup("user").startWith("contacts.userId")
.connectFrom("contacts.userId").connectTo("_id").depthField("numConnections").as("connections");
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document).containsEntry("$graphLookup.startWith", "$contacts.userId");
}
@Test // DATAMONGO-2096
public void connectToShouldUseTargetFieldInsteadOfAlias() {
AggregationOperation graphLookupOperation = Aggregation.graphLookup("user").startWith("contacts.userId")
.connectFrom("userId").connectTo("connectto.field").depthField("numConnections").as("connections");
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document).containsEntry("$graphLookup.connectToField", "connectto.field");
}
@Test // DATAMONGO-2096
public void depthFieldShouldUseTargetFieldInsteadOfAlias() {
AggregationOperation graphLookupOperation = Aggregation.graphLookup("user").startWith("contacts.userId")
.connectFrom("contacts.userId").connectTo("_id").depthField("foo.bar").as("connections");
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
assertThat(document).containsEntry("$graphLookup.depthField", "foo.bar");
}
}

View File

@@ -26,6 +26,7 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.math.BigDecimal;
@@ -1913,6 +1914,18 @@ public class MappingMongoConverterUnitTests {
assertThat(target).doesNotContainKeys("_class");
}
@Test // DATAMONGO-2135
public void addsEqualObjectsToCollection() {
org.bson.Document itemDocument = new org.bson.Document("itemKey", "123");
org.bson.Document orderDocument = new org.bson.Document("items",
Arrays.asList(itemDocument, itemDocument, itemDocument));
Order order = converter.read(Order.class, orderDocument);
assertThat(order.items).hasSize(3);
}
static class GenericType<T> {
T content;
}
@@ -2341,4 +2354,15 @@ public class MappingMongoConverterUnitTests {
final @Id String id;
String value;
}
// DATAMONGO-2135
@EqualsAndHashCode // equality check by fields
static class SomeItem {
String itemKey;
}
static class Order {
Collection<SomeItem> items = new ArrayList<>();
}
}

View File

@@ -15,14 +15,18 @@
*/
package org.springframework.data.mongodb.core.convert;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.assertj.core.api.Assertions.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Currency;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.assertj.core.data.TemporalUnitLessThanOffset;
import org.bson.BsonTimestamp;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
@@ -32,6 +36,7 @@ import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicIntegerToIntegerConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicLongToLongConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.BsonTimestampToInstantConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.CurrencyToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.IntegerToAtomicIntegerConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.LongToAtomicLongConverter;
@@ -39,8 +44,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBig
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToCurrencyConverter;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.bson.Document;
/**
* Unit tests for {@link MongoConverters}.
*
@@ -55,10 +58,10 @@ public class MongoConvertersUnitTests {
BigDecimal bigDecimal = BigDecimal.valueOf(254, 1);
String value = BigDecimalToStringConverter.INSTANCE.convert(bigDecimal);
assertThat(value, is("25.4"));
assertThat(value).isEqualTo("25.4");
BigDecimal reference = StringToBigDecimalConverter.INSTANCE.convert(value);
assertThat(reference, is(bigDecimal));
assertThat(reference).isEqualTo(bigDecimal);
}
@Test // DATAMONGO-858
@@ -69,7 +72,7 @@ public class MongoConvertersUnitTests {
Document document = GeoConverters.BoxToDocumentConverter.INSTANCE.convert(box);
Shape shape = GeoConverters.DocumentToBoxConverter.INSTANCE.convert(document);
assertThat(shape, is((org.springframework.data.geo.Shape) box));
assertThat(shape).isEqualTo(box);
}
@Test // DATAMONGO-858
@@ -80,7 +83,7 @@ public class MongoConvertersUnitTests {
Document document = GeoConverters.CircleToDocumentConverter.INSTANCE.convert(circle);
Shape shape = GeoConverters.DocumentToCircleConverter.INSTANCE.convert(document);
assertThat(shape, is((org.springframework.data.geo.Shape) circle));
assertThat(shape).isEqualTo(circle);
}
@Test // DATAMONGO-858
@@ -91,7 +94,7 @@ public class MongoConvertersUnitTests {
Document document = GeoConverters.PolygonToDocumentConverter.INSTANCE.convert(polygon);
Shape shape = GeoConverters.DocumentToPolygonConverter.INSTANCE.convert(document);
assertThat(shape, is((org.springframework.data.geo.Shape) polygon));
assertThat(shape).isEqualTo(polygon);
}
@Test // DATAMONGO-858
@@ -102,7 +105,7 @@ public class MongoConvertersUnitTests {
Document document = GeoConverters.SphereToDocumentConverter.INSTANCE.convert(sphere);
org.springframework.data.geo.Shape shape = GeoConverters.DocumentToSphereConverter.INSTANCE.convert(document);
assertThat(shape, is((org.springframework.data.geo.Shape) sphere));
assertThat(shape).isEqualTo(sphere);
}
@Test // DATAMONGO-858
@@ -113,36 +116,44 @@ public class MongoConvertersUnitTests {
Document document = GeoConverters.PointToDocumentConverter.INSTANCE.convert(point);
org.springframework.data.geo.Point converted = GeoConverters.DocumentToPointConverter.INSTANCE.convert(document);
assertThat(converted, is((org.springframework.data.geo.Point) point));
assertThat(converted).isEqualTo(point);
}
@Test // DATAMONGO-1372
public void convertsCurrencyToStringCorrectly() {
assertThat(CurrencyToStringConverter.INSTANCE.convert(Currency.getInstance("USD")), is("USD"));
assertThat(CurrencyToStringConverter.INSTANCE.convert(Currency.getInstance("USD"))).isEqualTo("USD");
}
@Test // DATAMONGO-1372
public void convertsStringToCurrencyCorrectly() {
assertThat(StringToCurrencyConverter.INSTANCE.convert("USD"), is(Currency.getInstance("USD")));
assertThat(StringToCurrencyConverter.INSTANCE.convert("USD")).isEqualTo(Currency.getInstance("USD"));
}
@Test // DATAMONGO-1416
public void convertsAtomicLongToLongCorrectly() {
assertThat(AtomicLongToLongConverter.INSTANCE.convert(new AtomicLong(100L)), is(100L));
assertThat(AtomicLongToLongConverter.INSTANCE.convert(new AtomicLong(100L))).isEqualTo(100L);
}
@Test // DATAMONGO-1416
public void convertsAtomicIntegerToIntegerCorrectly() {
assertThat(AtomicIntegerToIntegerConverter.INSTANCE.convert(new AtomicInteger(100)), is(100));
assertThat(AtomicIntegerToIntegerConverter.INSTANCE.convert(new AtomicInteger(100))).isEqualTo(100);
}
@Test // DATAMONGO-1416
public void convertsLongToAtomicLongCorrectly() {
assertThat(LongToAtomicLongConverter.INSTANCE.convert(100L), is(instanceOf(AtomicLong.class)));
assertThat(LongToAtomicLongConverter.INSTANCE.convert(100L)).isInstanceOf(AtomicLong.class);
}
@Test // DATAMONGO-1416
public void convertsIntegerToAtomicIntegerCorrectly() {
assertThat(IntegerToAtomicIntegerConverter.INSTANCE.convert(100), is(instanceOf(AtomicInteger.class)));
assertThat(IntegerToAtomicIntegerConverter.INSTANCE.convert(100)).isInstanceOf(AtomicInteger.class);
}
@Test // DATAMONGO-2113
public void convertsBsonTimestampToInstantCorrectly() {
assertThat(BsonTimestampToInstantConverter.INSTANCE.convert(new BsonTimestamp(6615900307735969796L)))
.isCloseTo(Instant.ofEpochSecond(1540384327), new TemporalUnitLessThanOffset(100, ChronoUnit.MILLIS));
}
}

View File

@@ -405,7 +405,7 @@ public class ChangeStreamTests {
.append("user_name", "jellyBelly").append("age", 8).append("_class", User.class.getName()));
}
@Test // DATAMONGO-2012
@Test // DATAMONGO-2012, DATAMONGO-2113
public void resumeAtTimestampCorrectly() throws InterruptedException {
CollectingMessageListener<ChangeStreamDocument<Document>, User> messageListener1 = new CollectingMessageListener<>();
@@ -415,6 +415,9 @@ public class ChangeStreamTests {
awaitSubscription(subscription1);
template.save(jellyBelly);
Thread.sleep(1000); // cluster timestamp is in seconds, so we need to wait at least one.
template.save(sugarSplashy);
awaitMessages(messageListener1, 12);

View File

@@ -25,10 +25,11 @@ import java.util.List;
* Utilities for testing long running asnyc message retrieval.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class SubscriptionUtils {
static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(1);
static final Duration DEFAULT_TIMEOUT = Duration.ofMillis(1500);
/**
* Wait for {@link Subscription#isActive() to become active} but not longer than {@link #DEFAULT_TIMEOUT}.

View File

@@ -590,6 +590,19 @@ public class StringBasedMongoQueryUnitTests {
assertThat(query.getQueryObject(), is(new Document("arg0", null)));
}
@Test // DATAMONGO-2119
public void spelShouldIgnoreJsonParseErrorsForRegex() {
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByPersonLastnameRegex", Person.class);
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter,
new Person("Molly", "Chandler"));
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);
assertThat(query.getQueryObject().toJson(),
is(new BasicQuery("{lastname: {$regex: 'Chandler'}}").getQueryObject().toJson()));
}
private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) {
try {
@@ -697,10 +710,14 @@ public class StringBasedMongoQueryUnitTests {
@Query("{ 'arg0' : '?0', 'arg1' : '?1s' }")
List<Person> findByWhenQuotedAndSomeStuffAppended(String arg0, String arg1);
@Query("{ 'lastname' : { '$regex' : '^(?0|John ?1|?1)'} }") // use spel or some regex string this is fucking bad
@Query("{ 'lastname' : { '$regex' : '^(?0|John ?1|?1)'} }") // use spel or some regex string this is bad
Person findByLastnameRegex(String lastname, String alternative);
@Query("{ arg0 : ?#{[0]} }")
List<Person> findByUsingSpel(Object arg0);
@Query("{ 'lastname' : { '$regex' : ?#{[0].lastname} } }")
Person findByPersonLastnameRegex(Person key);
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.springframework.data.domain.ExampleMatcher.*;
import java.util.ArrayList;
@@ -28,14 +29,16 @@ import java.util.Set;
import java.util.UUID;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.ExampleMatcher.*;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.mapping.Document;
@@ -44,9 +47,13 @@ import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Person.Sex;
import org.springframework.data.mongodb.repository.User;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.test.util.MongoVersion;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.mongodb.test.util.ReplicaSet;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionTemplate;
/**
* @author A. B. M. Kowser
@@ -59,6 +66,7 @@ import org.springframework.test.util.ReflectionTestUtils;
public class SimpleMongoRepositoryTests {
@Autowired private MongoTemplate template;
public @Rule MongoVersionRule mongoVersion = MongoVersionRule.any();
private Person oliver, dave, carter, boyd, stefan, leroi, alicia;
private List<Person> all;
@@ -383,6 +391,54 @@ public class SimpleMongoRepositoryTests {
assertThat(repository.findAll()).containsExactlyInAnyOrder(first, second);
}
@Test // DATAMONGO-2130
@MongoVersion(asOf = "4.0")
public void countShouldBePossibleInTransaction() {
assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
MongoTransactionManager txmgr = new MongoTransactionManager(template.getMongoDbFactory());
TransactionTemplate tt = new TransactionTemplate(txmgr);
tt.afterPropertiesSet();
long countPreTx = repository.count();
long count = tt.execute(status -> {
Person sample = new Person();
sample.setLastname("Matthews");
repository.save(sample);
return repository.count();
});
assertThat(count).isEqualTo(countPreTx + 1);
}
@Test // DATAMONGO-2130
@MongoVersion(asOf = "4.0")
public void existsShouldBePossibleInTransaction() {
assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
MongoTransactionManager txmgr = new MongoTransactionManager(template.getMongoDbFactory());
TransactionTemplate tt = new TransactionTemplate(txmgr);
tt.afterPropertiesSet();
boolean exists = tt.execute(status -> {
Person sample = new Person();
sample.setLastname("Matthews");
repository.save(sample);
return repository.existsById(sample.getId());
});
assertThat(exists).isTrue();
}
private void assertThatAllReferencePersonsWereStoredCorrectly(Map<String, Person> references, List<Person> saved) {
for (Person person : saved) {

View File

@@ -34,7 +34,7 @@ Note that the domain type shown in the preceding example has a property named `i
====
[source]
----
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {
// additional custom query methods go here
}

View File

@@ -47,7 +47,7 @@ Note that the entity defined in the preceding example has a property named `id`
====
[source]
----
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {
Flux<Person> findByFirstname(String firstname); <1>

View File

@@ -1,6 +1,60 @@
Spring Data MongoDB Changelog
=============================
Changes in version 2.1.3.RELEASE (2018-11-27)
---------------------------------------------
* DATAMONGO-2139 - Auditing broken for entities using optimistic locking via version properties.
* DATAMONGO-2135 - Use List instead of Collection when reading data with MappingMongoConverter.
* DATAMONGO-2130 - Repository count errors in ClientSession / Transaction.
* DATAMONGO-2121 - Release 2.1.3 (Lovelace SR3).
* DATAMONGO-2119 - Combining a SpEL expression and a $regex filter triggers a PatternSyntaxException.
Changes in version 2.0.12.RELEASE (2018-11-27)
----------------------------------------------
* DATAMONGO-2135 - Use List instead of Collection when reading data with MappingMongoConverter.
* DATAMONGO-2119 - Combining a SpEL expression and a $regex filter triggers a PatternSyntaxException.
* DATAMONGO-2118 - Fix typo in repositories reference documentation.
* DATAMONGO-2109 - Release 2.0.12 (Kay SR12).
* DATAMONGO-2098 - Typo in MappingMongoConverterParser.
Changes in version 1.10.17.RELEASE (2018-11-27)
-----------------------------------------------
* DATAMONGO-2135 - Use List instead of Collection when reading data with MappingMongoConverter.
* DATAMONGO-2118 - Fix typo in repositories reference documentation.
* DATAMONGO-2110 - Release 1.10.17 (Ingalls SR17).
Changes in version 2.1.2.RELEASE (2018-10-29)
---------------------------------------------
* DATAMONGO-2118 - Fix typo in repositories reference documentation.
* DATAMONGO-2113 - ChangeStreamTasks incorrectly converts resumeAt Instants into BsonTimestamp.
* DATAMONGO-2107 - Release 2.1.2 (Lovelace SR2).
* DATAMONGO-2098 - Typo in MappingMongoConverterParser.
Changes in version 1.10.16.RELEASE (2018-10-15)
-----------------------------------------------
* DATAMONGO-2096 - Aggregation.graphLookup.**.connectFrom(String) does not handle nested field.
* DATAMONGO-2083 - Release 1.10.16 (Ingalls SR16).
Changes in version 2.0.11.RELEASE (2018-10-15)
----------------------------------------------
* DATAMONGO-2101 - NoSuchMethodException: org.springframework.data.mongodb.core.geo.GeoJsonPoint.<init>().
* DATAMONGO-2096 - Aggregation.graphLookup.**.connectFrom(String) does not handle nested field.
* DATAMONGO-2087 - Typo in MongoRepository.
* DATAMONGO-2086 - Kotlin Fluent API extensions do not allow projections with find queries.
* DATAMONGO-2084 - Release 2.0.11 (Kay SR11).
Changes in version 2.1.1.RELEASE (2018-10-15)
---------------------------------------------
* DATAMONGO-2096 - Aggregation.graphLookup.**.connectFrom(String) does not handle nested field.
* DATAMONGO-2094 - Release 2.1.1 (Lovelace SR1).
Changes in version 2.1.0.RELEASE (2018-09-21)
---------------------------------------------
* DATAMONGO-2091 - Upgrade to MongoDB Java Driver 3.8.2 and Reactive Streams Driver 1.9.2.

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 2.1 GA
Spring Data MongoDB 2.1.3
Copyright (c) [2010-2015] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").