Compare commits

..

100 Commits

Author SHA1 Message Date
Christoph Strobl
24417c4c99 DATAMONGO-1927 - Release version 2.1 M3 (Lovelace). 2018-05-17 09:51:42 +02:00
Christoph Strobl
eec36b791a DATAMONGO-1927 - Prepare 2.1 M3 (Lovelace). 2018-05-17 09:50:48 +02:00
Christoph Strobl
512fa036bb DATAMONGO-1927 - Updated changelog. 2018-05-17 09:50:42 +02:00
Sébastien Deleuze
ea8df26eee DATAMONGO-1980 - Fix a typo in CriteriaExtensions.kt.
Original pull request: #563.
2018-05-16 09:43:53 +02:00
Oliver Gierke
43d821aab0 DATAMONGO-1874 - Polishing.
Cleanups in test case. Moved assertions to AssertJ.
2018-05-15 14:39:35 +02:00
Oliver Gierke
9bb8211ed7 DATAMONGO-1874 - @Document's collection attribute now supports EvaluationContextExtensions.
We now use the API newly introduced with DATACMNS-1260 to expose EvaluationContextExtensions to the SpEL evaluation in case the collection attribute of @Document uses SpEL.

Related tickets: DATACMNS-1260.
2018-05-15 14:39:35 +02:00
Victor
5a24e04226 DATAMONGO-1978 - Fix minor typo in Field.positionKey field name.
Original pull request: #558.
2018-05-15 12:30:22 +02:00
Mark Paluch
521d28ff3f DATAMONGO-1466 - Polishing.
Switch conditionals to Map-based Function registry to pick the appropriate converter. Fix typos in method names.

Original pull request: #561.
2018-05-15 11:27:04 +02:00
Christoph Strobl
e38e7d89f4 DATAMONGO-1466 - Polishing.
Just some minor code style improvements.

Original pull request: #561.
2018-05-15 11:27:04 +02:00
Christoph Strobl
fff69b9ed7 DATAMONGO-1466 - Add embedded typeinformation-based reading GeoJSON converter.
Original pull request: #561.
2018-05-15 11:27:04 +02:00
Oliver Gierke
e2e9e92563 DATAMONGO-1880 - Improve test execution in SessionBoundMongotTemplateTests.
We now also require a replica set for the test execution.
2018-05-15 10:39:36 +02:00
Oliver Gierke
3e040d283b DATAMONGO-1976 - Adapt to SpEL extension API changes in Spring Data Commons.
Related tickets: DATACMNS-1260.
2018-05-15 10:36:18 +02:00
Mark Paluch
a66b87118e DATAMONGO-1970 - Polishing.
ReactiveMongoOperations.withSession(…) no longer commits transactions if a transaction is active. ReactiveSessionScoped obtained through inTransaction() solely manages transactions and participates in ongoing transactions if a given ClientSession has already an active transaction. Remove ReactiveSessionScoped.executeSingle methods to align with ReactiveMongoOperations.

Add tests. Switch reactive tests to .as(StepVerifier:create) form. Extend documentation.

Original pull request: #560.
2018-05-14 13:18:15 +02:00
Christoph Strobl
f296a499e5 DATAMONGO-1970 - Add support for MongoDB 4.0 transactions (reactive).
We now support Mongo Transactions through the reactive Template API. However, there's no reactive repository transaction support yet.

Mono<DeleteResult> result = template.inTransaction()
                              .execute(action -> action.remove(query(where("id").is("step-1")), Step.class));

Original pull request: #560.
2018-05-14 13:16:22 +02:00
Mark Paluch
1acf00b039 DATAMONGO-1974 - Polishing.
Fix typos, links, and code fences.

Original pull request: #559.
2018-05-11 15:30:05 +02:00
Jay Bryant
e23c861a39 DATAMONGO-1974 - Full editing pass for Spring Data MongoDB.
Full editing pass of the Spring Data MongoDB reference guide. I also adjusted index.adoc to work with the changes I made to the build project, so that we get Epub and PDF as well as HTML.

Original pull request: #559.
2018-05-11 15:30:05 +02:00
Mark Paluch
85aef4836d DATAMONGO-1968 - Polishing.
Rename MongoDbFactoryBase to MongoDbFactorySupport. Add constructor to MongoTemplate accepting the new MongoClient type. Extend Javadoc. Switch tests to use the new MongoTemplate constructor.

Original pull request: #557.
2018-05-11 10:29:58 +02:00
Christoph Strobl
5470486d8d DATAMONGO-1968 - Add configuration support for com.mongodb.client.MongoClient.
We now accept MongoDB's new com.mongodb.client.MongoClient object to setup Spring Data MongoDB infrastructure through AbstractMongoClientConfiguration. The new MongoClient does not support DBObject anymore hence it cannot be used with Querydsl.

@Configuration
public class MongoClientConfiguration extends AbstractMongoClientConfiguration {

	@Override
	protected String getDatabaseName() {
		return "database";
	}

	@Override
	public MongoClient mongoClient() {
		return MongoClients.create("mongodb://localhost:27017/?replicaSet=rs0&w=majority");
	}
}

Original pull request: #557.
2018-05-11 10:27:30 +02:00
Mark Paluch
42c02c9b70 DATAMONGO-1971 - Polishing.
Remove outdated profiles.

Original pull request: #554.
2018-05-09 16:35:21 +02:00
Mark Paluch
ee9bca4856 DATAMONGO-1971 - Install MongoDB 3.7.9 on TravisCI.
We now download and unpack MongoDB directly instead of using TravisCI's outdated MongoDB version.

Original pull request: #554.
2018-05-09 16:35:18 +02:00
Mark Paluch
0d823df7f3 DATAMONGO-1920 - Polishing.
Slightly tweak method names. Document MongoDatabaseUtils usage in the context of MongoTransactionManager. Rename SessionSynchronization constants to align with AbstractPlatformTransactionManager. Slightly tweak Javadoc and reference docs for typos.

Original pull request: #554.
2018-05-09 16:31:58 +02:00
Christoph Strobl
4cd2935087 DATAMONGO-1920 - Add support for MongoDB 4.0 transactions (synchronous driver).
MongoTransactionManager is the gateway to the well known Spring transaction support. It allows applications to use managed transaction features of Spring.
The MongoTransactionManager binds a ClientSession to the thread. MongoTemplate automatically detects those and operates on them accordingly.

static class Config extends AbstractMongoConfiguration {

	// ...

	@Bean
	MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
		return new MongoTransactionManager(dbFactory);
	}
}

@Component
public class StateService {

	@Transactional
	void someBusinessFunction(Step step) {

		template.insert(step);

		process(step);

		template.update(Step.class).apply(update.set("state", // ...
	};
});

Original pull request: #554.
2018-05-09 16:31:06 +02:00
Mark Paluch
6fbb7cec22 DATAMONGO-1918 - Updated changelog. 2018-05-08 15:27:17 +02:00
Mark Paluch
44ea579b69 DATAMONGO-1917 - Updated changelog. 2018-05-08 12:22:51 +02:00
Mark Paluch
30af34f80a DATAMONGO-1943 - Polishing.
Reduce visibility. Use List interface instead of concrete type.

Original pull request: #556.
2018-05-07 16:20:41 +02:00
Christoph Strobl
247f30143b DATAMONGO-1943 - Fix ClassCastException caused by SpringDataMongodbSerializer.
We now convert List-typed predicates to List to BasicDBList to meet MongodbSerializer's expectations for top-level lists used for the $and operator.

Original pull request: #556.
2018-05-07 16:18:52 +02:00
Mark Paluch
364f266a3a DATAMONGO-1914 - Polishing.
Throw FileNotFoundException on inherited methods throwing IOException if resource is absent. Retain filename for absent resources to provide context through GridFsResource.getFilename(). Switch exists() to determine presence/absence based on GridFSFile presence. Extend tests.

Original pull request: #555.
2018-05-07 14:53:27 +02:00
Christoph Strobl
f92bd20384 DATAMONGO-1914 - Return an empty GridFsResource instead of null when resource does not exist.
Original pull request: #555.
2018-05-07 14:53:21 +02:00
Mark Paluch
304e1c607f DATAMONGO-1929 - Polishing.
Add missing Nullable annotations and missing diamond operators. Slightly reorder null guards.

Use Lombok to generate required constructors.

Original pull request: #551.
2018-04-23 10:34:37 +02:00
Christoph Strobl
449780573e DATAMONGO-1929 - Add Kotlin extensions for Executable and ReactiveMapReduceOperation.
Original pull request: #551.
2018-04-23 10:34:31 +02:00
Christoph Strobl
e424573f0d DATAMONGO-1929 - Add fluent mapReduce template API.
Original pull request: #551.
2018-04-23 10:34:10 +02:00
Christoph Strobl
31630c0dcc DATAMONGO-1928 - Polishing.
Use native driver operations to avoid potential unwanted template index interaction.

Original Pull Request: #550
2018-04-20 13:17:38 +02:00
Mark Paluch
7cdc3d00c1 DATAMONGO-1928 - Polishing.
Migrate test to AssertJ and as-style for StepVerifier.

Original Pull Request: #550
2018-04-20 13:02:00 +02:00
Mark Paluch
a9f5d7bd3d DATAMONGO-1928 - Use non-blocking index creation through ReactiveMongoTemplate.
We now use ReactiveIndexOperationsProvider to inspect and create indexes for MongoDB collections without using blocking methods. Indexes are created for initial entities and whenever a MongoPersistentEntity is registered in MongoMappingContext.

Index creation is now decoupled from the actual ReactiveMongoTemplate call causing indexes to be created asynchronously. Mongo commands no longer depend on the completion of index creation commands. Decoupling also comes with the aspect that ReactiveMongoTemplate creation/command invocation no longer fails if the actual index creation fails. Previous usage of blocking index creation caused the actual ReactiveMongoTemplate call to fail.

ReactiveMongoTemplate objects can be created with a Consumer<Throwable> callback that is notified if an index creation fails.

Original Pull Request: #550
2018-04-20 13:00:54 +02:00
Christoph Strobl
b5f18468db DATAMONGO-1808 - Polishing.
Move individual methods under BitwiseCriteriaOperators, update Javadoc and split tests.

Original Pull Request: #507
2018-04-18 13:15:05 +02:00
Andreas Zink
ef872d2527 DATAMONGO-1808 - Add support for bitwise query operators.
Add support for $bitsAllClear, $bitsAllSet, $bitsAnyClear and $bitsAnySet.

Original Pull Request: #507
2018-04-18 13:14:45 +02:00
Mark Paluch
c2516946e9 DATAMONGO-1890 - Polishing.
Remove mapReduce default methods in favor of adding variants through a fluent API at a later stage. Assert mapReduce arguments and remove subsequent null guards. Adapt tests.

Original pull request: #548.
2018-04-17 10:12:46 +02:00
Christoph Strobl
857add7349 DATAMONGO-1890 - Add support for mapReduce to ReactiveMongoOperations.
We now support mapReduce via ReactiveMongoTemplate returning a Flux<T> as the operations result.

Original pull request: #548.
2018-04-17 10:11:29 +02:00
Mark Paluch
2ec3f219c8 DATAMONGO-1869 - After release cleanups. 2018-04-13 15:08:33 +02:00
Mark Paluch
5c8701f79c DATAMONGO-1869 - Prepare next development iteration. 2018-04-13 15:08:32 +02:00
Mark Paluch
77819a5d37 DATAMONGO-1869 - Release version 2.1 M2 (Lovelace). 2018-04-13 14:30:57 +02:00
Mark Paluch
a7684e808b DATAMONGO-1869 - Prepare 2.1 M2 (Lovelace). 2018-04-13 14:30:01 +02:00
Mark Paluch
4114bffead DATAMONGO-1869 - Updated changelog. 2018-04-13 14:29:52 +02:00
Oliver Gierke
06662b6889 DATAMONGO-1923 - Polishing. 2018-04-09 14:34:42 +02:00
Oliver Gierke
a6eb8d69d4 DATAMONGO-1923 - Adapted to API changes in Spring Data Commons.
Related tickets: DATACMNS-1275.
2018-04-09 14:34:42 +02:00
Mark Paluch
f94afab567 DATAMONGO-1893 - Polishing.
Inherit fields from previous operation if at least one field is excluded. Extend FieldsExposingAggregationOperation to conditionally inherit fields.

Original pull request: #538.
2018-04-06 10:45:57 +02:00
Christoph Strobl
f0f6185808 DATAMONGO-1893 - Allow exclusion of other fields than _id in aggregation $project.
As of MongoDB 3.4 exclusion of fields other than _id is allowed so we removed the limitation in our code.

Original pull request: #538.
2018-04-06 10:45:57 +02:00
Mark Paluch
7f69e43856 DATAMONGO-1888 - Updated changelog. 2018-04-04 17:12:51 +02:00
Mark Paluch
1348127702 DATAMONGO-1857 - Updated changelog. 2018-04-04 15:16:18 +02:00
Christoph Strobl
90078aa8c5 DATAMONGO-1912 - Polishing.
Organize imports.

Original Pull Request: #545
2018-04-03 14:11:40 +02:00
Mark Paluch
18c5ecd36f DATAMONGO-1912 - Propagate autogenerated Id to persistent top-level Maps.
We now set autogenerated Ids in Maps that are used as top-level entities. This allows transparent and persistent Map usage without requiring to use Document in application code. Previously, we only set autogenerated Ids in Document and persistent entity types.

Original Pull Request: #545
2018-04-03 14:11:40 +02:00
Mark Paluch
25dc56a840 DATAMONGO-1903 - Polishing.
Remove client side operating system check as operating system-dependant constraints depend on the server. Add check on whitespaces. Add author tags. Extend tests.

Adapt check in SimpleReactiveMongoDatabaseFactory accordingly. Remove superfluous UnknownHostException declaration in reactive database factory. Replace references to legacy types in Javadoc with references to current ones.

Original pull request: #546.
2018-04-03 13:44:05 +02:00
George Moraitis
919a07a7c5 DATAMONGO-1903 - Align database name check in SimpleMongoDbFactory with MongoDB limitations.
We now test database names against the current (3.6) MongoDB specifications for database names.

Original pull request: #546.
2018-04-03 13:44:01 +02:00
Mark Paluch
239304160b DATAMONGO-1916 - Polishing.
Remove unused final keywords from method parameters and unused variables. Add nullable annotations to parameters that can be null. Fix generics.

Original pull request: #547.
2018-04-03 11:37:06 +02:00
Christoph Strobl
a824bad2fd DATAMONGO-1916 - Fix potential ClassCastException in MappingMongoConverter#writeInternal when writing collections.
Original pull request: #547.
2018-04-03 11:37:06 +02:00
Mark Paluch
525eeccb9a DATAMONGO-1834 - Polishing.
Increase visibility of Timezone factory methods. Add missing nullable annotation. Tweaked Javadoc. Add tests for Timezone using expressions/field references.

Original Pull Request: #539
2018-03-28 11:25:58 +02:00
Christoph Strobl
bc257aa260 DATAMONGO-1834 - Polishing.
Remove DateFactory and split up tests.
Introduce dedicated Timezone abstraction and update existing factories to apply the timezone if appropriate. Update builders and align code style.

Original Pull Request: #539
2018-03-28 11:25:17 +02:00
Matt Morrissette
10737cd819 DATAMONGO-1834 - Add support for MongoDB 3.6 DateOperators $dateFromString, $dateFromParts and $dateToParts including timezones.
Original Pull Request: #539
2018-03-28 11:25:03 +02:00
Oliver Gierke
982cf84f70 DATAMONGO-1915 - Removed explicit declaration of Jackson library versions. 2018-03-27 19:35:18 +02:00
Christoph Strobl
8f1beff541 DATAMONGO-1911 - Polishing.
Use native MongoDB Codec facilities to render binary and uuid.

Original Pull Request: #544
2018-03-27 13:53:14 +02:00
Mark Paluch
2658af1ac5 DATAMONGO-1911 - Fix UUID serialization in String-based queries.
We now render to the correct UUID representation in String-based queries. Unquoted values render to $binary representation, quoted UUIDs are rendered with their toString() value.

Previously we used JSON.serialize() to encode values to JSON. The com.mongodb.util.JSON serializer does not produce JSON that is compatible with Document.parse. It uses an older JSON format that preceded the MongoDB Extended JSON specification.

Original Pull Request: #544
2018-03-27 13:52:35 +02:00
Mark Paluch
f59cd7e489 DATAMONGO-1913 - Add missing nullable annotations to GridFsTemplate. 2018-03-26 14:25:31 +02:00
Mark Paluch
88805d0743 DATAMONGO-1813 - Polishing.
Add since tag. Add non-null guard. Refactor conditional resource mapping to Optional. Apply code formatter.

Optimize array construction from List.

Original pull request: #543.
2018-03-26 14:25:31 +02:00
Hartmut Lang
aab86d23c9 DATAMONGO-1813 - Add GridFsOperations.getResource(GridFSFile).
We now provide GridFsOperations.getResource(GridFSFile) to create GridFsResource without a database lookup. This allows direct creation of GridFsResource for GridFSFile.

Original pull request: #543.
2018-03-26 14:23:45 +02:00
Mark Paluch
1d7cc2eb97 DATAMONGO-1906 - Polishing.
Tiny rewording of Javadoc.

Original pull request: #540.
2018-03-22 11:55:17 +01:00
Christoph Strobl
4d8f5d63c7 DATAMONGO-1906 - Add SystemVariable $$REMOVE for aggregation $project stage.
Add, document and make sure conditional projection in aggregation is treated correctly.

Original pull request: #540.
2018-03-22 11:55:07 +01:00
Felipe Zanardo Affonso
f7d65cf8d4 DATAMONGO-1909 - Fix typo on return statement.
Original pull request: #523.
2018-03-21 16:04:21 +01:00
Mark Paluch
309148dd64 DATAMONGO-1891 - Polishing.
Tweaked wording.

Original pull request: #542.
2018-03-21 16:01:41 +01:00
Christoph Strobl
ee8436880b DATAMONGO-1891 - Improve $jsonSchema documentation.
Original pull request: #542.
2018-03-21 16:01:24 +01:00
Mark Paluch
1ad975de0a DATAMONGO-1907 - Polishing.
Rename test method to reflect test subject.

Switch from flatMap(…) to map(…) to avoid overhead of Mono creation.

Original pull request: #541.
2018-03-21 09:54:29 +01:00
Ruben J Garcia
f6314a321a DATAMONGO-1907 - Adjust SimpleReactiveMongoRepository.findOne(…) to complete without exception on empty result
We now no longer emit an exception via SimpleReactiveMongoRepository.findOne(Example) if the query completes without yielding a result. Previously findOne(Example) emitted a NoSuchElementException if the query returned no result.

Original pull request: #541.
2018-03-21 09:48:01 +01:00
Mark Paluch
91717e5566 DATAMONGO-1880 - Polishing.
Turn instance methods into static ones where applicable. Avoid parameter type array cloning where possible.
Add reference to rework stack-trace inspection in order to throw ClientSessionException. Migrate MongoPersistentEntityIndexCreatorUnitTests to AssertJ. Add tests to verify simple session proxy wrapping on subsequent MongoDbFactory.withSession(…) calls.

Guard ClientSession tests with replica set rule. Remove unused code. Add non-null guards. Add missing Nullable annotations. Slightly tweak Javadoc and reference documentation.

Original pull request: #536.
2018-03-20 10:43:52 +01:00
Christoph Strobl
b9f7f23b8f DATAMONGO-1880 - Add support for ClientSession.
We now support ClientSession via MongoOperations and ReactiveMongoOperations. Client sessions introduce causal consistency and retryable writes. A client Session can be either provided by application code or managed by specifying ClientSessionOptions. Binding a ClientSession via MongoOperations.withSession(…) provides access to a Session-bound MongoOperations instance that associates the session with each MongoDB operation.

ClientSession support applies only to MongoOperations and ReactiveMongoOperations and is not yet available via repositories.

ClientSession session = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build());

Person person = template.withSession(() -> session)
        .execute(action -> {

          action.insert(new Person("wohoo"));
          return action.findOne(query(where("id").is("wohoo")), Person.class);
        });

session.close();

Original pull request: #536.
2018-03-20 10:42:16 +01:00
Oliver Gierke
caab310cf8 DATAMONGO-1904 - Optimizations in MappingMongoConverter.readCollectionOrArray(…).
Switched to ClassUtils.isAssignableValue(…) in getPotentiallyConvertedSimpleRead(…) as it transparently handles primitives and their wrapper types so that we can avoid the superfluous invocation of the converter infrastructure.
2018-03-15 15:03:37 +01:00
Oliver Gierke
6b116df70b DATAMONGO-1904 - Fixed handling of nested arrays on reads in MappingMongoConverter.
We now properly forward the component type information into recursive calls to MappingMongoConverter.readCollectionOrArray(…).
2018-03-15 15:03:28 +01:00
Oliver Gierke
12d86f30a9 DATAMONGO-1901 - Added project.root configuration to make JavaDoc generation work again.
Related ticket: https://github.com/spring-projects/spring-data-build/issues/527.
2018-03-14 09:44:58 +01:00
Mark Paluch
297ff1587a DATAMONGO-1899 - Export composable repositories via CDI.
We now export composable repositories through our CDI extension. Repositories can now be customized either by a single custom implementation (as it was before) and by providing fragment interfaces along their fragment implementation.

This change aligns CDI support with the existing RepositoryFactory support we provide within a Spring application context.
2018-03-12 16:25:04 +01:00
Oliver Gierke
c2a91aa7b4 DATAMONGO-1898 - Added unit tests for the conversion handling of enums implementing interfaces.
Related tickets: DATACMNS-1278.
2018-03-12 11:07:33 +01:00
Oliver Gierke
5b7b69026b DATAMONGO-1896 - SimpleMongoRepository.saveAll(…) now consistently uses aggregate collection for inserts.
We previously used MongoTemplate.insertAll(…) which determines the collection to insert the individual elements based on the type, which - in cases of entity inheritance - will use dedicated collections for sub-types of the aggregate root. Subsequent lookups of the entities will then fail, as those are executed against the collection the aggregate root is mapped to.

We now rather use ….insert(Collection, String) handing the collection of the aggregate root explicitly.
2018-03-09 00:03:32 +01:00
Mark Paluch
22cb17486c DATAMONGO-1877 - Polishing.
Add JsonSchemaProperty#timestamp(). Fix JavaDoc.

Original pull request: #537.
2018-03-02 15:28:24 +01:00
Christoph Strobl
448df55ff7 DATAMONGO-1877 - Add JsonSchemaProperty for date type.
We now provide factory methods and schema objects for date and timestamp types.

Original pull request: #537.
2018-03-02 15:27:52 +01:00
Mark Paluch
a71e4bb313 DATAMONGO-1882 - Updated changelog. 2018-02-28 11:17:39 +01:00
Mark Paluch
7c3e80a1bc DATAMONGO-1859 - Updated changelog. 2018-02-19 20:30:06 +01:00
Christoph Strobl
5f7e252f87 DATAMONGO-1881 - Upgrade MongoDB sync & reactive streams Java driver to 3.6.3 and 1.7.1.
Drivers work fine against MongoDB server 3.4.9, 3.6.0 and 3.7.1.
2018-02-19 10:00:04 +01:00
Mark Paluch
b44c6cb59f DATAMONGO-1870 - Polishing.
Extend copyright license years. Slightly reword documentation. Use IntStream and insertAll to create test fixture.

Original pull request: #532.
Related pull request: #531.
2018-02-15 10:56:28 +01:00
Christoph Strobl
14467cb1f6 DATAMONGO-1870 - Consider skip/limit on MongoOperations.remove(Query, Class).
We now use _id lookup for remove operations that query with limit or skip parameters. This allows more fine grained control over documents removed.

Original pull request: #532.
Related pull request: #531.
2018-02-15 10:56:25 +01:00
Christoph Strobl
50715cd7c8 DATAMONGO-1860 - Polishing.
Fix references to QuerydslPredicateExecutor.

Original Pull Request: #529
2018-02-14 13:43:35 +01:00
Mark Paluch
4bba7b4406 DATAMONGO-1860 - Polishing.
Fix type references in Javadoc. Change lambdas to method references where applicable.

Original Pull Request: #529
2018-02-14 13:43:35 +01:00
Mark Paluch
0d05e4b35d DATAMONGO-1860 - Retrieve result count via QuerydslMongoPredicateExecutor only for paging.
We now use AbstractMongodbQuery.fetch() instead of AbstractMongodbQuery.fetchResults() to execute MongoDB queries. fetchResults() executes a find(…) and a count(…) query. Retrieving the record count is an expensive operation in MongoDB and the count is not always required. For regular find(…) method, the count is ignored, for paging the count(…) is only required in certain result/request scenarios.

Original Pull Request: #529
2018-02-14 13:43:35 +01:00
Mark Paluch
ab7069187e DATAMONGO-1865 - Polishing.
Adapt to collection name retrieval during query execution. Slightly reword documentation and JavaDoc.

Original pull request: #530.
2018-02-14 12:01:38 +01:00
Christoph Strobl
0ec840e34a DATAMONGO-1865 - Avoid IncorrectResultSizeDataAccessException for derived findFirst/findTop queries.
We now return the first result when executing findFirst/findTop queries. This fixes a glitch introduced in the Kay release throwing IncorrectResultSizeDataAccessException for single entity executions returning more than one result, which is explicitly not the desired behavior in this case.

Original pull request: #530.
2018-02-14 12:01:28 +01:00
Mark Paluch
5a783ba21f DATAMONGO-1871 - Polishing.
Migrate test to AssertJ.

Original pull request: #533.
2018-02-14 11:05:30 +01:00
Christoph Strobl
0edaf8799b DATAMONGO-1871 - Fix AggregationExpression aliasing.
We now make sure to allow a nested property alias by setting the target.

Original pull request: #533.
2018-02-14 11:05:26 +01:00
Oliver Gierke
9efc0970b7 DATAMONGO-1872 - Polishing.
Fixed @since tag for newly introduced method in MongoEntityMetadata.
2018-02-13 12:23:56 +01:00
Oliver Gierke
ce237436d3 DATAMONGO-1873 - Added value() as alias for @Document(collection = "…").
This allows to avoid having to use the explicit collection attribute in case the collection name is supposed to be customized.
2018-02-13 12:12:45 +01:00
Oliver Gierke
99824a498e DATAMONGO-1872 - Polishing.
Fixed imports in MongoTemplate.
2018-02-13 12:01:34 +01:00
Oliver Gierke
2bfeb9cf20 DATAMONGO-1872 - Repository query execution doesn't prematurely fix collection to be queried.
We now avoid calling ….inCollection(…) with a fixed, one-time calculated collection name to make sure we dynamically resolve the collections. That's necessary to make sure SpEL expressions in @Document are evaluated for every query execution.
2018-02-13 12:00:42 +01:00
Christoph Strobl
bb889c7672 DATAMONGO-1866 - Update travis build to use single node replica set. 2018-02-07 13:57:29 +01:00
Christoph Strobl
c2e43bd938 DATAMONGO-1794 - After release cleanups. 2018-02-06 10:11:16 +01:00
Christoph Strobl
c74f91392a DATAMONGO-1794 - Prepare next development iteration. 2018-02-06 10:11:15 +01:00
197 changed files with 14523 additions and 2505 deletions

View File

@@ -3,26 +3,26 @@ language: java
jdk:
- oraclejdk8
before_script:
- mongod --version
services:
- mongodb
before_install:
- mkdir -p downloads
- mkdir -p var/db var/log
- if [[ ! -d downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION} ]] ; then cd downloads && wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}.tgz && tar xzf mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}.tgz && cd ..; fi
- downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongod --version
- downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongod --dbpath var/db --replSet rs0 --fork --logpath var/log/mongod.log
- sleep 10
- |-
downloads/mongodb-linux-x86_64-ubuntu1604-${MONGO_VERSION}/bin/mongo --eval "rs.initiate({_id: 'rs0', members:[{_id: 0, host: '127.0.0.1:27017'}]});"
sleep 15
env:
matrix:
- PROFILE=ci
- PROFILE=mongo36-next
global:
- MONGO_VERSION=3.7.9
# Current MongoDB version is 2.4.2 as of 2016-04, see https://github.com/travis-ci/travis-ci/issues/3694
# apt-get starts a MongoDB instance so it's not started using before_script
addons:
apt:
sources:
- mongodb-3.4-precise
packages:
- mongodb-org-server
- mongodb-org-shell
- oracle-java8-installer
sudo: false
@@ -30,9 +30,6 @@ sudo: false
cache:
directories:
- $HOME/.m2
install:
- |-
mongo admin --eval "db.adminCommand({setFeatureCompatibilityVersion: '3.4'});"
- downloads
script: "mvn clean dependency:list test -P${PROFILE} -Dsort"

View File

@@ -138,6 +138,42 @@ public class MyService {
}
```
### MongoDB 4.0 Transactions
As of version 4 MongoDB supports [Transactions](https://www.mongodb.com/transactions). Transactions are built on top of
`ClientSessions` and therefore require an active session.
`MongoTransactionManager` is the gateway to the well known Spring transaction support. It allows applications to use
[managed transaction features of Spring](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html).
The `MongoTransactionManager` binds a `ClientSession` to the thread. `MongoTemplate` automatically detects those and operates on them accordingly.
```java
@Configuration
static class Config extends AbstractMongoConfiguration {
@Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
// ...
}
@Component
public class StateService {
@Transactional
void someBusinessFunction(Step step) {
template.insert(step);
process(step);
template.update(Step.class).apply(Update.set("state", // ...
};
});
```
## Contributing to Spring Data
Here are some ways for you to get involved in the community:

42
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M1</version>
<version>2.1.0.M3</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.M1</version>
<version>2.1.0.M3</version>
</parent>
<modules>
@@ -27,9 +27,9 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.1.0.M1</springdata.commons>
<mongo>3.6.2</mongo>
<mongo.reactivestreams>1.7.0</mongo.reactivestreams>
<springdata.commons>2.1.0.M3</springdata.commons>
<mongo>3.8.0-beta2</mongo>
<mongo.reactivestreams>1.9.0-beta1</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
</properties>
@@ -115,38 +115,6 @@
<profiles>
<!-- not-yet available profile>
<id>mongo35-next</id>
<properties>
<mongo>3.5.1-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile -->
<profile>
<id>mongo36-next</id>
<properties>
<mongo>3.6.0-SNAPSHOT</mongo>
</properties>
<repositories>
<repository>
<id>mongo-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
</profile>
<profile>
<id>release</id>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.1.0.M1</version>
<version>2.1.0.M3</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.M1</version>
<version>2.1.0.M3</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -17,6 +17,7 @@
<jpa>2.1.1</jpa>
<hibernate>5.2.1.Final</hibernate>
<java-module-name>spring.data.mongodb.cross.store</java-module-name>
<project.root>${basedir}/..</project.root>
</properties>
<dependencies>
@@ -49,7 +50,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>2.1.0.M1</version>
<version>2.1.0.M3</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.M1</version>
<version>2.1.0.M3</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.M1</version>
<version>2.1.0.M3</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -19,6 +19,7 @@
<objenesis>1.3</objenesis>
<equalsverifier>1.7.8</equalsverifier>
<java-module-name>spring.data.mongodb</java-module-name>
<project.root>${basedir}/..</project.root>
<multithreadedtc>1.01</multithreadedtc>
</properties>
@@ -215,7 +216,6 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson}</version>
<optional>true</optional>
</dependency>
@@ -253,6 +253,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<!-- Kotlin extension -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.lang.Nullable;
/**
* {@link NonTransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
* access failures such as reading data using an already closed session.
*
* @author Christoph Strobl
* @since 2.1
*/
public class ClientSessionException extends NonTransientDataAccessException {
/**
* Constructor for {@link ClientSessionException}.
*
* @param msg the detail message. Must not be {@literal null}.
*/
public ClientSessionException(String msg) {
super(msg);
}
/**
* Constructor for {@link ClientSessionException}.
*
* @param msg the detail message. Can be {@literal null}.
* @param cause the root cause. Can be {@literal null}.
*/
public ClientSessionException(@Nullable String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSynchronization;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase;
/**
* Helper class for managing a {@link MongoDatabase} instances via {@link MongoDbFactory}. Used for obtaining
* {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
* {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
* <p />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
* @author Mark Paluch
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
*/
public class MongoDatabaseUtils {
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(MongoDbFactory factory) {
return doGetMongoDatabase(null, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
}
/**
* Obtain the default {@link MongoDatabase database} form the given {@link MongoDbFactory factory}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(MongoDbFactory factory, SessionSynchronization sessionSynchronization) {
return doGetMongoDatabase(null, factory, sessionSynchronization);
}
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory} using
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param dbName the name of the {@link MongoDatabase} to get.
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory) {
return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
}
/**
* Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDbFactory factory}.
* <p />
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current
* {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
*
* @param dbName the name of the {@link MongoDatabase} to get.
* @param factory the {@link MongoDbFactory} to get the {@link MongoDatabase} from.
* @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
* @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
*/
public static MongoDatabase getDatabase(String dbName, MongoDbFactory factory,
SessionSynchronization sessionSynchronization) {
return doGetMongoDatabase(dbName, factory, sessionSynchronization);
}
private static MongoDatabase doGetMongoDatabase(@Nullable String dbName, MongoDbFactory factory,
SessionSynchronization sessionSynchronization) {
Assert.notNull(factory, "Factory must not be null!");
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
ClientSession session = doGetSession(factory, sessionSynchronization);
if(session == null) {
return StringUtils.hasText(dbName) ? factory.getDb(dbName) : factory.getDb();
}
MongoDbFactory factoryToUse = factory.withSession(session);
return StringUtils.hasText(dbName) ? factoryToUse.getDb(dbName) : factoryToUse.getDb();
}
@Nullable
private static ClientSession doGetSession(MongoDbFactory dbFactory, SessionSynchronization sessionSynchronization) {
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory);
// check for native MongoDB transaction
if (resourceHolder != null && (resourceHolder.hasSession() || resourceHolder.isSynchronizedWithTransaction())) {
if (!resourceHolder.hasSession()) {
resourceHolder.setSession(createClientSession(dbFactory));
}
return resourceHolder.getSession();
}
if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) {
return null;
}
// init a non native MongoDB transaction by registering a MongoSessionSynchronization
resourceHolder = new MongoResourceHolder(createClientSession(dbFactory), dbFactory);
resourceHolder.getSession().startTransaction();
TransactionSynchronizationManager
.registerSynchronization(new MongoSessionSynchronization(resourceHolder, dbFactory));
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(dbFactory, resourceHolder);
return resourceHolder.getSession();
}
private static ClientSession createClientSession(MongoDbFactory dbFactory) {
return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build());
}
/**
* MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when
* participating in a non-native MongoDB transaction, such as a Jta or JDBC transaction.
*
* @author Christoph Strobl
* @since 2.1
*/
private static class MongoSessionSynchronization extends ResourceHolderSynchronization<MongoResourceHolder, Object> {
private final MongoResourceHolder resourceHolder;
MongoSessionSynchronization(MongoResourceHolder resourceHolder, MongoDbFactory dbFactory) {
super(resourceHolder, dbFactory);
this.resourceHolder = resourceHolder;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion()
*/
@Override
protected boolean shouldReleaseBeforeCompletion() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object)
*/
@Override
protected void processResourceAfterCommit(MongoResourceHolder resourceHolder) {
if (isTransactionActive(resourceHolder)) {
resourceHolder.getSession().commitTransaction();
}
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int)
*/
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) {
resourceHolder.getSession().abortTransaction();
}
super.afterCompletion(status);
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object)
*/
@Override
protected void releaseResource(MongoResourceHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.getSession().close();
}
}
private boolean isTransactionActive(MongoResourceHolder resourceHolder) {
if (!resourceHolder.hasSession()) {
return false;
}
return resourceHolder.getSession().hasActiveTransaction();
}
}
}

View File

@@ -20,20 +20,22 @@ import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import com.mongodb.ClientSessionOptions;
import com.mongodb.DB;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase;
/**
* Interface for factories creating {@link DB} instances.
* Interface for factories creating {@link MongoDatabase} instances.
*
* @author Mark Pollack
* @author Thomas Darimont
* @author Christoph Strobl
*/
public interface MongoDbFactory extends CodecRegistryProvider {
public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvider {
/**
* Creates a default {@link DB} instance.
* Creates a default {@link MongoDatabase} instance.
*
* @return
* @throws DataAccessException
@@ -56,6 +58,11 @@ public interface MongoDbFactory extends CodecRegistryProvider {
*/
PersistenceExceptionTranslator getExceptionTranslator();
/**
* Get the legacy database entry point. Please consider {@link #getDb()} instead.
*
* @return
*/
DB getLegacyDb();
/**
@@ -67,4 +74,35 @@ public interface MongoDbFactory extends CodecRegistryProvider {
default CodecRegistry getCodecRegistry() {
return getDb().getCodecRegistry();
}
/**
* Obtain a {@link ClientSession} for given ClientSessionOptions.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
ClientSession getSession(ClientSessionOptions options);
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances
* that are aware and bound to a new session with given {@link ClientSessionOptions options}.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
default MongoDbFactory withSession(ClientSessionOptions options) {
return withSession(getSession(options));
}
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances
* that are aware and bound to the given session.
*
* @param session must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
MongoDbFactory withSession(ClientSession session);
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.ResourceHolderSupport;
import com.mongodb.client.ClientSession;
/**
* MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}.
* {@link MongoTransactionManager} binds instances of this class to the thread.
* <p />
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Christoph Strobl
* @since 2.1
* @see MongoTransactionManager
* @see org.springframework.data.mongodb.core.MongoTemplate
*/
class MongoResourceHolder extends ResourceHolderSupport {
private @Nullable ClientSession session;
private MongoDbFactory dbFactory;
/**
* Create a new {@link MongoResourceHolder} for a given {@link ClientSession session}.
*
* @param session the associated {@link ClientSession}. Can be {@literal null}.
* @param dbFactory the associated {@link MongoDbFactory}. must not be {@literal null}.
*/
MongoResourceHolder(@Nullable ClientSession session, MongoDbFactory dbFactory) {
this.session = session;
this.dbFactory = dbFactory;
}
/**
* @return the associated {@link ClientSession}. Can be {@literal null}.
*/
@Nullable
ClientSession getSession() {
return session;
}
/**
* @return the associated {@link MongoDbFactory}.
*/
public MongoDbFactory getDbFactory() {
return dbFactory;
}
/**
* Set the {@link ClientSession} to guard.
*
* @param session can be {@literal null}.
*/
public void setSession(@Nullable ClientSession session) {
this.session = session;
}
/**
* Only set the timeout if it does not match the {@link TransactionDefinition#TIMEOUT_DEFAULT default timeout}.
*
* @param seconds
*/
void setTimeoutIfNotDefaulted(int seconds) {
if (seconds != TransactionDefinition.TIMEOUT_DEFAULT) {
setTimeoutInSeconds(seconds);
}
}
/**
* @return {@literal true} if session is not {@literal null}.
*/
boolean hasSession() {
return session != null;
}
/**
* @return {@literal true} if the session is active and has not been closed.
*/
boolean hasActiveSession() {
if (!hasSession()) {
return false;
}
return hasServerSession() && !getSession().getServerSession().isClosed();
}
/**
* @return {@literal true} if the {@link ClientSession} has a {@link com.mongodb.session.ServerSession} associated
* that is accessible via {@link ClientSession#getServerSession()}.
*/
boolean hasServerSession() {
try {
return getSession().getServerSession() != null;
} catch (IllegalStateException serverSessionClosed) {
// ignore
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import com.mongodb.ClientSessionOptions;
import com.mongodb.client.ClientSession;
/**
* A simple interface for obtaining a {@link ClientSession} to be consumed by
* {@link org.springframework.data.mongodb.core.MongoOperations} and MongoDB native operations that support causal
* consistency and transactions.
*
* @author Christoph Strobl
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
*/
@FunctionalInterface
public interface MongoSessionProvider {
/**
* Obtain a {@link ClientSession} with with given options.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.DataAccessException
*/
ClientSession getSession(ClientSessionOptions options);
}

View File

@@ -0,0 +1,490 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoException;
import com.mongodb.TransactionOptions;
import com.mongodb.client.ClientSession;
/**
* A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages
* {@link ClientSession} based transactions for a single {@link MongoDbFactory}.
* <p />
* Binds a {@link ClientSession} from the specified {@link MongoDbFactory} to the thread.
* <p />
* {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal
* consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction()
* commit} or {@link ClientSession#abortTransaction() abort} a transaction.
* <p />
* Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via
* {@link MongoDatabaseUtils#getDatabase(MongoDbFactory)} instead of a standard {@link MongoDbFactory#getDb()} call.
* Spring classes such as {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
*
* @author Christoph Strobl
* @author Mark Paluch
* @currentRead Shadow's Edge - Brent Weeks
* @since 2.1
* @see <a href="https://www.mongodb.com/transactions">MongoDB Transaction Documentation</a>
* @see MongoDatabaseUtils#getDatabase(MongoDbFactory, SessionSynchronization)
*/
public class MongoTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
private @Nullable MongoDbFactory dbFactory;
private @Nullable TransactionOptions options;
/**
* Create a new {@link MongoTransactionManager} for bean-style usage.
* <p />
* <strong>Note:</strong>The {@link MongoDbFactory db factory} has to be {@link #setDbFactory(MongoDbFactory) set}
* before using the instance. Use this constructor to prepare a {@link MongoTransactionManager} via a
* {@link org.springframework.beans.factory.BeanFactory}.
* <p />
* Optionally it is possible to set default {@link TransactionOptions transaction options} defining
* {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
*
* @see #setDbFactory(MongoDbFactory)
* @see #setTransactionSynchronization(int)
*/
public MongoTransactionManager() {}
/**
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory}.
*
* @param dbFactory must not be {@literal null}.
*/
public MongoTransactionManager(MongoDbFactory dbFactory) {
this(dbFactory, null);
}
/**
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDbFactory} applying the
* given {@link TransactionOptions options}, if present, when starting a new transaction.
*
* @param dbFactory must not be {@literal null}.
* @param options can be {@literal null}.
*/
public MongoTransactionManager(MongoDbFactory dbFactory, @Nullable TransactionOptions options) {
Assert.notNull(dbFactory, "DbFactory must not be null!");
this.dbFactory = dbFactory;
this.options = options;
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction()
*/
@Override
protected Object doGetTransaction() throws TransactionException {
MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager
.getResource(getRequiredDbFactory());
return new MongoTransactionObject(resourceHolder);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#isExistingTransaction(java.lang.Object)
*/
@Override
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return extractMongoTransaction(transaction).hasResourceHolder();
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition)
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
MongoResourceHolder resourceHolder = newResourceHolder(definition,
ClientSessionOptions.builder().causallyConsistent(true).build());
mongoTransactionObject.setResourceHolder(resourceHolder);
if (logger.isDebugEnabled()) {
logger
.debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession())));
}
try {
mongoTransactionObject.startTransaction(options);
} catch (MongoException ex) {
throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
if (logger.isDebugEnabled()) {
logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession())));
}
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(getRequiredDbFactory(), resourceHolder);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doSuspend(java.lang.Object)
*/
@Override
protected Object doSuspend(Object transaction) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
mongoTransactionObject.setResourceHolder(null);
return TransactionSynchronizationManager.unbindResource(getRequiredDbFactory());
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doResume(java.lang.Object, java.lang.Object)
*/
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
TransactionSynchronizationManager.bindResource(getRequiredDbFactory(), suspendedResources);
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to commit transaction for session %s.",
debugString(mongoTransactionObject.getSession())));
}
try {
mongoTransactionObject.commitTransaction();
} catch (MongoException ex) {
throw new TransactionSystemException(String.format("Could not commit Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to abort transaction for session %s.",
debugString(mongoTransactionObject.getSession())));
}
try {
mongoTransactionObject.abortTransaction();
} catch (MongoException ex) {
throw new TransactionSystemException(String.format("Could not abort Mongo transaction for session %s.",
debugString(mongoTransactionObject.getSession())), ex);
}
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doSetRollbackOnly(org.springframework.transaction.support.DefaultTransactionStatus)
*/
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
MongoTransactionObject transactionObject = extractMongoTransaction(status);
transactionObject.getRequiredResourceHolder().setRollbackOnly();
}
/*
* (non-Javadoc)
* org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object)
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
Assert.isInstanceOf(MongoTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
transaction.getClass()));
MongoTransactionObject mongoTransactionObject = (MongoTransactionObject) transaction;
// Remove the connection holder from the thread.
TransactionSynchronizationManager.unbindResource(getRequiredDbFactory());
mongoTransactionObject.getRequiredResourceHolder().clear();
if (logger.isDebugEnabled()) {
logger.debug(String.format("About to release Session %s after transaction.",
debugString(mongoTransactionObject.getSession())));
}
mongoTransactionObject.closeSession();
}
/**
* Set the {@link MongoDbFactory} that this instance should manage transactions for.
*
* @param dbFactory must not be {@literal null}.
*/
public void setDbFactory(MongoDbFactory dbFactory) {
Assert.notNull(dbFactory, "DbFactory must not be null!");
this.dbFactory = dbFactory;
}
/**
* Set the {@link TransactionOptions} to be applied when starting transactions.
*
* @param options can be {@literal null}.
*/
public void setOptions(@Nullable TransactionOptions options) {
this.options = options;
}
/**
* Get the {@link MongoDbFactory} that this instance manages transactions for.
*
* @return can be {@literal null}.
*/
@Nullable
public MongoDbFactory getDbFactory() {
return dbFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceTransactionManager#getResourceFactory()
*/
@Override
public MongoDbFactory getResourceFactory() {
return getRequiredDbFactory();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
getRequiredDbFactory();
}
private MongoResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options) {
MongoDbFactory dbFactory = getResourceFactory();
MongoResourceHolder resourceHolder = new MongoResourceHolder(dbFactory.getSession(options), dbFactory);
resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition));
return resourceHolder;
}
/**
* @throws IllegalStateException if {@link #dbFactory} is {@literal null}.
*/
private MongoDbFactory getRequiredDbFactory() {
Assert.state(dbFactory != null,
"MongoTransactionManager operates upon a MongoDbFactory. Did you forget to provide one? It's required.");
return dbFactory;
}
private static MongoTransactionObject extractMongoTransaction(Object transaction) {
Assert.isInstanceOf(MongoTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
transaction.getClass()));
return (MongoTransactionObject) transaction;
}
private static MongoTransactionObject extractMongoTransaction(DefaultTransactionStatus status) {
Assert.isInstanceOf(MongoTransactionObject.class, status.getTransaction(),
() -> String.format("Expected to find a %s but it turned out to be %s.", MongoTransactionObject.class,
status.getTransaction().getClass()));
return (MongoTransactionObject) status.getTransaction();
}
private static String debugString(@Nullable ClientSession session) {
if (session == null) {
return "null";
}
String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()),
Integer.toHexString(session.hashCode()));
try {
if (session.getServerSession() != null) {
debugString += String.format("id = %s, ", session.getServerSession().getIdentifier());
debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber());
debugString += String.format("closed = %d, ", session.getServerSession().isClosed());
debugString += String.format("clusterTime = %s", session.getClusterTime());
} else {
debugString += "id = n/a";
debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
debugString += String.format("clusterTime = %s", session.getClusterTime());
}
} catch (RuntimeException e) {
debugString += String.format("error = %s", e.getMessage());
}
debugString += "]";
return debugString;
}
/**
* MongoDB specific transaction object, representing a {@link MongoResourceHolder}. Used as transaction object by
* {@link MongoTransactionManager}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see MongoResourceHolder
*/
static class MongoTransactionObject implements SmartTransactionObject {
private @Nullable MongoResourceHolder resourceHolder;
MongoTransactionObject(@Nullable MongoResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* Set the {@link MongoResourceHolder}.
*
* @param resourceHolder can be {@literal null}.
*/
void setResourceHolder(@Nullable MongoResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* @return {@literal true} if a {@link MongoResourceHolder} is set.
*/
boolean hasResourceHolder() {
return resourceHolder != null;
}
/**
* Start a MongoDB transaction optionally given {@link TransactionOptions}.
*
* @param options can be {@literal null}
*/
void startTransaction(@Nullable TransactionOptions options) {
ClientSession session = getRequiredSession();
if (options != null) {
session.startTransaction(options);
} else {
session.startTransaction();
}
}
/**
* Commit the transaction.
*/
void commitTransaction() {
getRequiredSession().commitTransaction();
}
/**
* Rollback (abort) the transaction.
*/
void abortTransaction() {
getRequiredSession().abortTransaction();
}
/**
* Close a {@link ClientSession} without regard to its transactional state.
*/
void closeSession() {
ClientSession session = getRequiredSession();
if (session.getServerSession() != null && !session.getServerSession().isClosed()) {
session.close();
}
}
@Nullable
ClientSession getSession() {
return resourceHolder != null ? resourceHolder.getSession() : null;
}
private MongoResourceHolder getRequiredResourceHolder() {
Assert.state(resourceHolder != null, "MongoResourceHolder is required but not present. o_O");
return resourceHolder;
}
private ClientSession getRequiredSession() {
ClientSession session = getSession();
Assert.state(session != null, "A Session is required but it turned out to be null.");
return session;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly()
*/
@Override
public boolean isRollbackOnly() {
return this.resourceHolder != null && this.resourceHolder.isRollbackOnly();
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.SmartTransactionObject#flush()
*/
@Override
public void flush() {
TransactionSynchronizationUtils.triggerFlush();
}
}
}

View File

@@ -16,11 +16,15 @@
package org.springframework.data.mongodb;
import reactor.core.publisher.Mono;
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import com.mongodb.ClientSessionOptions;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoDatabase;
/**
@@ -65,4 +69,23 @@ public interface ReactiveMongoDatabaseFactory extends CodecRegistryProvider {
default CodecRegistry getCodecRegistry() {
return getMongoDatabase().getCodecRegistry();
}
/**
* Obtain a {@link Mono} emitting a {@link ClientSession} for given {@link ClientSessionOptions options}.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
Mono<ClientSession> getSession(ClientSessionOptions options);
/**
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoDatabaseFactory} returning
* {@link MongoDatabase} instances that are aware and bound to the given session.
*
* @param session must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
ReactiveMongoDatabaseFactory withSession(ClientSession session);
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Optional;
import java.util.function.BiFunction;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import com.mongodb.WriteConcern;
import com.mongodb.session.ClientSession;
/**
* {@link MethodInterceptor} implementation looking up and invoking an alternative target method having
* {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
* <p />
* The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself
* like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them
* if not already proxied.
*
* @param <D> Type of the actual Mongo Database.
* @param <C> Type of the actual Mongo Collection.
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public class SessionAwareMethodInterceptor<D, C> implements MethodInterceptor {
private static final MethodCache METHOD_CACHE = new MethodCache();
private final ClientSession session;
private final ClientSessionOperator collectionDecorator;
private final ClientSessionOperator databaseDecorator;
private final Object target;
private final Class<?> targetType;
private final Class<?> collectionType;
private final Class<?> databaseType;
private final Class<? extends ClientSession> sessionType;
/**
* Create a new SessionAwareMethodInterceptor for given target.
*
* @param session the {@link ClientSession} to be used on invocation.
* @param target the original target object.
* @param databaseType the MongoDB database type
* @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
* {@code MongoDatabase}.
* @param collectionType the MongoDB collection type.
* @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
* {@code MongoCollection}.
* @param <T> target object type.
*/
public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<? extends ClientSession> sessionType,
Class<D> databaseType, ClientSessionOperator<D> databaseDecorator, Class<C> collectionType,
ClientSessionOperator<C> collectionDecorator) {
Assert.notNull(session, "ClientSession must not be null!");
Assert.notNull(target, "Target must not be null!");
Assert.notNull(sessionType, "SessionType must not be null!");
Assert.notNull(databaseType, "Database type must not be null!");
Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!");
Assert.notNull(collectionType, "Collection type must not be null!");
Assert.notNull(collectionDecorator, "Collection ClientSessionOperator must not be null!");
this.session = session;
this.target = target;
this.databaseType = ClassUtils.getUserClass(databaseType);
this.collectionType = ClassUtils.getUserClass(collectionType);
this.collectionDecorator = collectionDecorator;
this.databaseDecorator = databaseDecorator;
this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType;
this.sessionType = sessionType;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor(org.aopalliance.intercept.MethodInvocation)
*/
@Nullable
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
if (requiresDecoration(methodInvocation.getMethod())) {
Object target = methodInvocation.proceed();
if (target instanceof Proxy) {
return target;
}
return decorate(target);
}
if (!requiresSession(methodInvocation.getMethod())) {
return methodInvocation.proceed();
}
Optional<Method> targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType);
return !targetMethod.isPresent() ? methodInvocation.proceed()
: ReflectionUtils.invokeMethod(targetMethod.get(), target,
prependSessionToArguments(session, methodInvocation));
}
private boolean requiresDecoration(Method method) {
return ClassUtils.isAssignable(databaseType, method.getReturnType())
|| ClassUtils.isAssignable(collectionType, method.getReturnType());
}
@SuppressWarnings("unchecked")
protected Object decorate(Object target) {
return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target)
: collectionDecorator.apply(session, target);
}
private static boolean requiresSession(Method method) {
if (method.getParameterCount() == 0
|| !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0])) {
return true;
}
return false;
}
private static Object[] prependSessionToArguments(ClientSession session, MethodInvocation invocation) {
Object[] args = new Object[invocation.getArguments().length + 1];
args[0] = session;
System.arraycopy(invocation.getArguments(), 0, args, 1, invocation.getArguments().length);
return args;
}
/**
* Simple {@link Method} to {@link Method} caching facility for {@link ClientSession} overloaded targets.
*
* @since 2.1
* @author Christoph Strobl
*/
static class MethodCache {
private final ConcurrentReferenceHashMap<MethodClassKey, Optional<Method>> cache = new ConcurrentReferenceHashMap<>();
/**
* Lookup the target {@link Method}.
*
* @param method
* @param targetClass
* @return
*/
Optional<Method> lookup(Method method, Class<?> targetClass, Class<? extends ClientSession> sessionType) {
return cache.computeIfAbsent(new MethodClassKey(method, targetClass),
val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType)));
}
@Nullable
private Method findTargetWithSession(Method sourceMethod, Class<?> targetType,
Class<? extends ClientSession> sessionType) {
Class<?>[] argTypes = sourceMethod.getParameterTypes();
Class<?>[] args = new Class<?>[argTypes.length + 1];
args[0] = sessionType;
System.arraycopy(argTypes, 0, args, 1, argTypes.length);
return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args);
}
/**
* Check whether the cache contains an entry for {@link Method} and {@link Class}.
*
* @param method
* @param targetClass
* @return
*/
boolean contains(Method method, Class<?> targetClass) {
return cache.containsKey(new MethodClassKey(method, targetClass));
}
}
/**
* Represents an operation upon two operands of the same type, producing a result of the same type as the operands
* accepting {@link ClientSession}. This is a specialization of {@link BiFunction} for the case where the operands and
* the result are all of the same type.
*
* @param <T> the type of the operands and result of the operator
*/
public interface ClientSessionOperator<T> extends BiFunction<ClientSession, T, T> {}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
/**
* {@link SessionSynchronization} is used along with {@link org.springframework.data.mongodb.core.MongoTemplate} to
* define in which type of transactions to participate if any.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public enum SessionSynchronization {
/**
* Synchronize with any transaction even with empty transactions and initiate a MongoDB transaction when doing so by
* registering a MongoDB specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}.
*/
ALWAYS,
/**
* Synchronize with native MongoDB transactions initiated via {@link MongoTransactionManager}.
*/
ON_ACTUAL_TRANSACTION;
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.lang.Nullable;
import com.mongodb.client.MongoClient;
/**
* Base class for Spring Data MongoDB configuration using JavaConfig with {@link com.mongodb.client.MongoClient}.
*
* @author Christoph Strobl
* @since 2.1
* @see MongoConfigurationSupport
* @see AbstractMongoConfiguration
*/
@Configuration
public abstract class AbstractMongoClientConfiguration extends MongoConfigurationSupport {
/**
* Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a
* {@link MongoClient} instance to the {@link org.springframework.context.ApplicationContext}.
*
* @return
*/
public abstract MongoClient mongoClient();
/**
* Creates a {@link MongoTemplate}.
*
* @return
*/
@Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory(), mappingMongoConverter());
}
/**
* Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link MongoClient}
* instance configured in {@link #mongoClient()}.
*
* @see #mongoClient()
* @see #mongoTemplate()
* @return
*/
@Bean
public MongoDbFactory mongoDbFactory() {
return new SimpleMongoClientDbFactory(mongoClient(), getDatabaseName());
}
/**
* Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
* class' (the concrete class, not this one here) by default. So if you have a {@code com.acme.AppConfig} extending
* {@link AbstractMongoClientConfiguration} the base package will be considered {@code com.acme} unless the method is
* overridden to implement alternate behavior.
*
* @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
* entities.
* @deprecated use {@link #getMappingBasePackages()} instead.
*/
@Deprecated
@Nullable
protected String getMappingBasePackage() {
Package mappingBasePackage = getClass().getPackage();
return mappingBasePackage == null ? null : mappingBasePackage.getName();
}
/**
* Creates a {@link MappingMongoConverter} using the configured {@link #mongoDbFactory()} and
* {@link #mongoMappingContext()}. Will get {@link #customConversions()} applied.
*
* @see #customConversions()
* @see #mongoMappingContext()
* @see #mongoDbFactory()
* @return
* @throws Exception
*/
@Bean
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext());
converter.setCustomConversions(customConversions());
return converter;
}
}

View File

@@ -29,7 +29,10 @@ import org.springframework.lang.Nullable;
import com.mongodb.MongoClient;
/**
* Base class for Spring Data MongoDB configuration using JavaConfig.
* Base class for Spring Data MongoDB configuration using JavaConfig with {@link com.mongodb.MongoClient}.
* <p />
* <strong>INFO:</strong>In case you want to use {@link com.mongodb.client.MongoClients} for configuration please refer
* to {@link AbstractMongoClientConfiguration}.
*
* @author Mark Pollack
* @author Oliver Gierke
@@ -38,10 +41,10 @@ import com.mongodb.MongoClient;
* @author Christoph Strobl
* @author Mark Paluch
* @see MongoConfigurationSupport
* @see AbstractMongoClientConfiguration
*/
@Configuration
public abstract class
AbstractMongoConfiguration extends MongoConfigurationSupport {
public abstract class AbstractMongoConfiguration extends MongoConfigurationSupport {
/**
* Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a
@@ -63,7 +66,7 @@ AbstractMongoConfiguration extends MongoConfigurationSupport {
/**
* Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link MongoClient}
* instance configured in {@link #mongo()}.
* instance configured in {@link #mongoClient()}.
*
* @see #mongoClient()
* @see #mongoTemplate()
@@ -111,4 +114,5 @@ AbstractMongoConfiguration extends MongoConfigurationSupport {
return converter;
}
}

View File

@@ -275,18 +275,9 @@ class DefaultBulkOperations implements BulkOperations {
try {
MongoCollection<Document> collection = mongoOperations.getCollection(collectionName);
if (defaultWriteConcern != null) {
collection = collection.withWriteConcern(defaultWriteConcern);
}
return mongoOperations.execute(collectionName, collection -> {
return collection.bulkWrite(models.stream().map(this::mapWriteModel).collect(Collectors.toList()), bulkOptions);
} catch (BulkWriteException o_O) {
DataAccessException toThrow = exceptionTranslator.translateExceptionIfPossible(o_O);
throw toThrow == null ? o_O : toThrow;
});
} finally {
this.bulkOptions = getBulkWriteOptions(bulkOperationContext.getBulkMode());
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.data.mongodb.core;
import static org.springframework.data.mongodb.core.MongoTemplate.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -50,18 +48,22 @@ public class DefaultIndexOperations implements IndexOperations {
private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
private final MongoDbFactory mongoDbFactory;
private final String collectionName;
private final QueryMapper mapper;
private final @Nullable Class<?> type;
private MongoOperations mongoOperations;
/**
* Creates a new {@link DefaultIndexOperations}.
*
* @param mongoDbFactory must not be {@literal null}.
* @param collectionName must not be {@literal null}.
* @param queryMapper must not be {@literal null}.
* @deprecated since 2.1. Please use
* {@link DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}.
*/
@Deprecated
public DefaultIndexOperations(MongoDbFactory mongoDbFactory, String collectionName, QueryMapper queryMapper) {
this(mongoDbFactory, collectionName, queryMapper, null);
}
@@ -74,7 +76,10 @@ public class DefaultIndexOperations implements IndexOperations {
* @param queryMapper must not be {@literal null}.
* @param type Type used for mapping potential partial index filter expression. Can be {@literal null}.
* @since 1.10
* @deprecated since 2.1. Please use
* {@link DefaultIndexOperations#DefaultIndexOperations(MongoOperations, String, Class)}.
*/
@Deprecated
public DefaultIndexOperations(MongoDbFactory mongoDbFactory, String collectionName, QueryMapper queryMapper,
@Nullable Class<?> type) {
@@ -82,10 +87,29 @@ public class DefaultIndexOperations implements IndexOperations {
Assert.notNull(collectionName, "Collection name can not be null!");
Assert.notNull(queryMapper, "QueryMapper must not be null!");
this.mongoDbFactory = mongoDbFactory;
this.collectionName = collectionName;
this.mapper = queryMapper;
this.type = type;
this.mongoOperations = new MongoTemplate(mongoDbFactory);
}
/**
* Creates a new {@link DefaultIndexOperations}.
*
* @param mongoOperations must not be {@literal null}.
* @param collectionName must not be {@literal null} or empty.
* @param type can be {@literal null}.
* @since 2.1
*/
public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
this.mongoOperations = mongoOperations;
this.mapper = new QueryMapper(mongoOperations.getConverter());
this.collectionName = collectionName;
this.type = type;
}
/*
@@ -187,11 +211,10 @@ public class DefaultIndexOperations implements IndexOperations {
Assert.notNull(callback, "CollectionCallback must not be null!");
try {
MongoCollection<Document> collection = mongoDbFactory.getDb().getCollection(collectionName);
return callback.doInCollection(collection);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, mongoDbFactory.getExceptionTranslator());
if (type != null) {
return mongoOperations.execute(type, callback);
}
return mongoOperations.execute(collectionName, callback);
}
}

View File

@@ -35,22 +35,10 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @since 2.0
*/
@RequiredArgsConstructor
class ExecutableAggregationOperationSupport implements ExecutableAggregationOperation {
private final MongoTemplate template;
/**
* Create new instance of {@link ExecutableAggregationOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableAggregationOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
private final @NonNull MongoTemplate template;
/*
* (non-Javadoc)

View File

@@ -45,24 +45,12 @@ import com.mongodb.client.FindIterable;
* @author Mark Paluch
* @since 2.0
*/
@RequiredArgsConstructor
class ExecutableFindOperationSupport implements ExecutableFindOperation {
private static final Query ALL_QUERY = new Query();
private final MongoTemplate template;
/**
* Create new {@link ExecutableFindOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableFindOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
private final @NonNull MongoTemplate template;
/*
* (non-Javadoc)

View File

@@ -37,22 +37,10 @@ import com.mongodb.bulk.BulkWriteResult;
* @author Mark Paluch
* @since 2.0
*/
@RequiredArgsConstructor
class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
private final MongoTemplate template;
/**
* Create new {@link ExecutableInsertOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableInsertOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
private final @NonNull MongoTemplate template;
/*
* (non-Javadoc)

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.util.List;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ExecutableMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ExecutableMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the mapReduce results.
*
* @return never {@literal null}.
*/
List<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ExecutableMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ExecutableMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ExecutableMapReduce} provides methods for constructing mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ExecutableMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ExecutableMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
@RequiredArgsConstructor
class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final @NonNull MongoTemplate template;
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ExecutableMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ExecutableMapReduceSupport<T>
implements ExecutableMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>,
MapReduceWithProjection<T>, MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final MongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ExecutableMapReduceSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String collection,
Query query, @Nullable String mapFunction, @Nullable String reduceFunction, @Nullable MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public List<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options,
returnType);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ExecutableMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ExecutableMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
}
}
}

View File

@@ -86,6 +86,13 @@ public interface ExecutableRemoveOperation {
*/
DeleteResult all();
/**
* Remove the first matching document.
*
* @return the {@link DeleteResult}. Never {@literal null}.
*/
DeleteResult one();
/**
* Remove and return all matching documents. <br/>
* <strong>NOTE</strong> The entire list of documents will be fetched before sending the actual delete commands.

View File

@@ -36,24 +36,12 @@ import com.mongodb.client.result.DeleteResult;
* @author Mark Paluch
* @since 2.0
*/
@RequiredArgsConstructor
class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
private static final Query ALL_QUERY = new Query();
private final MongoTemplate tempate;
/**
* Create new {@link ExecutableRemoveOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableRemoveOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.tempate = template;
}
private final @NonNull MongoTemplate tempate;
/*
* (non-Javadoc)
@@ -110,10 +98,16 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
*/
@Override
public DeleteResult all() {
return template.doRemove(getCollectionName(), query, domainType, true);
}
String collectionName = getCollectionName();
return template.doRemove(collectionName, query, domainType);
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove#one()
*/
@Override
public DeleteResult one() {
return template.doRemove(getCollectionName(), query, domainType, false);
}
/*

View File

@@ -35,23 +35,12 @@ import com.mongodb.client.result.UpdateResult;
* @author Mark Paluch
* @since 2.0
*/
@RequiredArgsConstructor
class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
private static final Query ALL_QUERY = new Query();
private final MongoTemplate template;
/**
* Creates new {@link ExecutableUpdateOperationSupport}.
*
* @param template must not be {@literal null}.
*/
ExecutableUpdateOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
private final @NonNull MongoTemplate template;
/*
* (non-Javadoc)

View File

@@ -22,4 +22,4 @@ package org.springframework.data.mongodb.core;
* @since 2.0
*/
public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation,
ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation {}
ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation, ExecutableMapReduceOperation {}

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.Value;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.DB;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
/**
* Common base class for usage with both {@link com.mongodb.client.MongoClients} and {@link com.mongodb.MongoClient}
* defining common properties such as database name and exception translator.
* <p/>
* Not intended to be used directly.
*
* @author Christoph Strobl
* @author Mark Paluch
* @param <C> Client type.
* @since 2.1
* @see SimpleMongoDbFactory
* @see SimpleMongoClientDbFactory
*/
public abstract class MongoDbFactorySupport<C> implements MongoDbFactory {
private final C mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
private final PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
/**
* Create a new {@link MongoDbFactorySupport} object given {@code mongoClient}, {@code databaseName},
* {@code mongoInstanceCreated} and {@link PersistenceExceptionTranslator}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @param mongoInstanceCreated {@literal true} if the client instance was created by a subclass of
* {@link MongoDbFactorySupport} to close the client on {@link #destroy()}.
* @param exceptionTranslator must not be {@literal null}.
*/
protected MongoDbFactorySupport(C mongoClient, String databaseName, boolean mongoInstanceCreated,
PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(mongoClient, "MongoClient must not be null!");
Assert.hasText(databaseName, "Database name must not be empty!");
Assert.isTrue(databaseName.matches("[^/\\\\.$\"\\s]+"),
"Database name must not contain slashes, dots, spaces, quotes, or dollar signs!");
this.mongoClient = mongoClient;
this.databaseName = databaseName;
this.mongoInstanceCreated = mongoInstanceCreated;
this.exceptionTranslator = exceptionTranslator;
}
/**
* Configures the {@link WriteConcern} to be used on the {@link MongoDatabase} instance being created.
*
* @param writeConcern the writeConcern to set
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
*/
public MongoDatabase getDb() throws DataAccessException {
return getDb(databaseName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty!");
MongoDatabase db = doGetMongoDatabase(dbName);
if (writeConcern == null) {
return db;
}
return db.withWriteConcern(writeConcern);
}
/**
* Get the actual {@link MongoDatabase} from the client.
*
* @param dbName must not be {@literal null} or empty.
* @return
*/
protected abstract MongoDatabase doGetMongoDatabase(String dbName);
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
if (mongoInstanceCreated) {
closeClient();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
public PersistenceExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.Session)
*/
public MongoDbFactory withSession(ClientSession session) {
return new MongoDbFactorySupport.ClientSessionBoundMongoDbFactory(session, this);
}
/**
* Close the client instance.
*/
protected abstract void closeClient();
/**
* @return the Mongo client object.
*/
protected C getMongoClient() {
return mongoClient;
}
/**
* @return the database name.
*/
protected String getDefaultDatabaseName() {
return databaseName;
}
/**
* {@link ClientSession} bound {@link MongoDbFactory} decorating the database with a
* {@link SessionAwareMethodInterceptor}.
*
* @author Christoph Strobl
* @since 2.1
*/
@Value
static class ClientSessionBoundMongoDbFactory implements MongoDbFactory {
ClientSession session;
MongoDbFactory delegate;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
*/
@Override
public MongoDatabase getDb() throws DataAccessException {
return proxyMongoDatabase(delegate.getDb());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
return proxyMongoDatabase(delegate.getDb(dbName));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return delegate.getExceptionTranslator();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
@Override
public DB getLegacyDb() {
return delegate.getLegacyDb();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return delegate.getSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#withSession(com.mongodb.session.ClientSession)
*/
@Override
public MongoDbFactory withSession(ClientSession session) {
return delegate.withSession(session);
}
private MongoDatabase proxyMongoDatabase(MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection<?> proxyCollection(com.mongodb.session.ClientSession session, MongoCollection<?> collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.setInterfaces(targetType);
factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class,
this::proxyDatabase, MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy());
}
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.BulkOperationException;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.lang.Nullable;
@@ -119,18 +120,28 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
int code = ((MongoException) ex).getCode();
if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
throw new DuplicateKeyException(ex.getMessage(), ex);
return new DuplicateKeyException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isDataAccessResourceFailureCode(code)) {
throw new DataAccessResourceFailureException(ex.getMessage(), ex);
return new DataAccessResourceFailureException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isInvalidDataAccessApiUsageCode(code) || code == 10003 || code == 12001
|| code == 12010 || code == 12011 || code == 12012) {
throw new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
throw new PermissionDeniedDataAccessException(ex.getMessage(), ex);
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
}
return new UncategorizedMongoDbException(ex.getMessage(), ex);
}
// may interfere with OmitStackTraceInFastThrow (enabled by default).
// see https://jira.spring.io/browse/DATAMONGO-1905
if (ex instanceof IllegalStateException) {
for (StackTraceElement elm : ex.getStackTrace()) {
if (elm.getClassName().contains("ClientSession")) {
return new ClientSessionException(ex.getMessage(), ex);
}
}
}
// If we get here, we have an exception that resulted from user code,
// rather than the persistence provider, so we return null to indicate
// that translation should not occur.

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.core;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.bson.Document;
import org.springframework.data.geo.GeoResults;
@@ -39,9 +41,12 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.Cursor;
import com.mongodb.ReadPreference;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
@@ -151,6 +156,64 @@ public interface MongoOperations extends FluentMongoOperations {
@Nullable
<T> T execute(String collectionName, CollectionCallback<T> action);
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB.
*
* @param sessionOptions must not be {@literal null}.
* @return new instance of {@link SessionScoped}. Never {@literal null}.
* @since 2.1
*/
SessionScoped withSession(ClientSessionOptions sessionOptions);
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use the
* {@link SessionScoped#execute(SessionCallback, Consumer)} hook to potentially close the {@link ClientSession}.
*
* @param sessionProvider must not be {@literal null}.
* @since 2.1
*/
default SessionScoped withSession(Supplier<ClientSession> sessionProvider) {
Assert.notNull(sessionProvider, "SessionProvider must not be null!");
return new SessionScoped() {
private final Object lock = new Object();
private @Nullable ClientSession session = null;
@Override
public <T> T execute(SessionCallback<T> action, Consumer<ClientSession> onComplete) {
synchronized (lock) {
if (session == null) {
session = sessionProvider.get();
}
}
try {
return action.doInSession(MongoOperations.this.withSession(session));
} finally {
onComplete.accept(session);
}
}
};
}
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoOperations}.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*
* @param session must not be {@literal null}.
* @return {@link ClientSession} bound instance of {@link MongoOperations}.
* @since 2.1
*/
MongoOperations withSession(ClientSession session);
/**
* Executes the given {@link Query} on the entity collection of the specified {@code entityType} backed by a Mongo DB
* {@link Cursor}.
@@ -769,7 +832,7 @@ public interface MongoOperations extends FluentMongoOperations {
}
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -782,7 +845,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}.
*
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
@@ -796,7 +859,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -813,7 +876,7 @@ public interface MongoOperations extends FluentMongoOperations {
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);
/**
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify<a/>
* Triggers <a href="https://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/">findAndModify <a/>
* to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query} taking
* {@link FindAndModifyOptions} into account.
*
@@ -953,7 +1016,7 @@ public interface MongoOperations extends FluentMongoOperations {
* Insert a mixed Collection of objects into a database collection determining the collection name to use based on the
* class.
*
* @param collectionToSave the list of objects to save. Must not be {@literal null}.
* @param objectsToSave the list of objects to save. Must not be {@literal null}.
*/
void insertAll(Collection<? extends Object> objectsToSave);
@@ -1142,6 +1205,7 @@ public interface MongoOperations extends FluentMongoOperations {
* @param query the query document that specifies the criteria used to remove a record.
* @param entityClass class that determines the collection to use.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query} or {@literal entityClass} is {@literal null}.
*/
DeleteResult remove(Query query, Class<?> entityClass);
@@ -1153,6 +1217,8 @@ public interface MongoOperations extends FluentMongoOperations {
* @param entityClass class of the pojo to be operated on. Can be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query}, {@literal entityClass} or {@literal collectionName} is
* {@literal null}.
*/
DeleteResult remove(Query query, Class<?> entityClass, String collectionName);
@@ -1165,6 +1231,7 @@ public interface MongoOperations extends FluentMongoOperations {
* @param query the query document that specifies the criteria used to remove a record.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws IllegalArgumentException when {@literal query} or {@literal collectionName} is {@literal null}.
*/
DeleteResult remove(Query query, String collectionName);

View File

@@ -61,7 +61,9 @@ import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.MongoDatabaseUtils;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.SessionSynchronization;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -124,6 +126,8 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import com.mongodb.ClientSessionOptions;
import com.mongodb.Cursor;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.Mongo;
@@ -132,6 +136,7 @@ import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.ClientSession;
import com.mongodb.client.DistinctIterable;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MapReduceIterable;
@@ -202,14 +207,27 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private @Nullable ResourceLoader resourceLoader;
private @Nullable MongoPersistentEntityIndexCreator indexCreator;
private SessionSynchronization sessionSynchronization = SessionSynchronization.ON_ACTUAL_TRANSACTION;
/**
* Constructor used for a basic template configuration
* Constructor used for a basic template configuration.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
*/
public MongoTemplate(MongoClient mongoClient, String databaseName) {
this(new SimpleMongoDbFactory(mongoClient, databaseName), null);
this(new SimpleMongoDbFactory(mongoClient, databaseName), (MongoConverter) null);
}
/**
* Constructor used for a basic template configuration.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @since 2.1
*/
public MongoTemplate(com.mongodb.client.MongoClient mongoClient, String databaseName) {
this(new SimpleMongoClientDbFactory(mongoClient, databaseName), (MongoConverter) null);
}
/**
@@ -218,7 +236,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @param mongoDbFactory must not be {@literal null}.
*/
public MongoTemplate(MongoDbFactory mongoDbFactory) {
this(mongoDbFactory, null);
this(mongoDbFactory, (MongoConverter) null);
}
/**
@@ -251,6 +269,20 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
}
private MongoTemplate(MongoDbFactory dbFactory, MongoTemplate that) {
this.mongoDbFactory = dbFactory;
this.exceptionTranslator = that.exceptionTranslator;
this.sessionSynchronization = that.sessionSynchronization;
this.mongoConverter = that.mongoConverter instanceof MappingMongoConverter ? getDefaultMongoConverter(dbFactory)
: that.mongoConverter;
this.queryMapper = that.queryMapper;
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.projectionFactory = that.projectionFactory;
this.mappingContext = that.mappingContext;
}
/**
* Configures the {@link WriteResultChecking} to be used with the template. Setting {@literal null} will reset the
* default of {@link #DEFAULT_WRITE_RESULT_CHECKING}.
@@ -384,7 +416,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity);
FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType)
.prepare(collection.find(mappedQuery).projection(mappedFields));
.prepare(collection.find(mappedQuery, Document.class).projection(mappedFields));
return new CloseableIterableCursorAdapter<T>(cursor, exceptionTranslator,
new ProjectingReadCallback<>(mongoConverter, entityType, returnType, collectionName));
@@ -496,10 +528,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
public <T> T execute(DbCallback<T> action) {
Assert.notNull(action, "DbCallbackmust not be null!");
Assert.notNull(action, "DbCallback must not be null!");
try {
MongoDatabase db = this.getDb();
MongoDatabase db = prepareDatabase(this.doGetDatabase());
return action.doInDB(db);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -526,13 +558,48 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(callback, "CollectionCallback must not be null!");
try {
MongoCollection<Document> collection = getAndPrepareCollection(getDb(), collectionName);
MongoCollection<Document> collection = getAndPrepareCollection(doGetDatabase(), collectionName);
return callback.doInCollection(collection);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#withSession(com.mongodb.ClientSessionOptions)
*/
@Override
public SessionScoped withSession(ClientSessionOptions options) {
Assert.notNull(options, "ClientSessionOptions must not be null!");
return withSession(() -> mongoDbFactory.getSession(options));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#withSession(com.mongodb.session.ClientSession)
*/
@Override
public MongoTemplate withSession(ClientSession session) {
Assert.notNull(session, "ClientSession must not be null!");
return new SessionBoundMongoTemplate(session, MongoTemplate.this);
}
/**
* Define if {@link MongoTemplate} should participate in transactions. Default is set to
* {@link SessionSynchronization#ON_ACTUAL_TRANSACTION}.<br />
* <strong>NOTE:</strong> MongoDB transactions require at least MongoDB 4.0.
*
* @since 2.1
*/
public void setSessionSynchronization(SessionSynchronization sessionSynchronization) {
this.sessionSynchronization = sessionSynchronization;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#createCollection(java.lang.Class)
@@ -607,6 +674,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return execute(new DbCallback<Boolean>() {
public Boolean doInDB(MongoDatabase db) throws MongoException, DataAccessException {
for (String name : db.listCollectionNames()) {
if (name.equals(collectionName)) {
return true;
@@ -637,7 +705,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public Void doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
collection.drop();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Dropped collection [{}]", collection.getNamespace().getCollectionName());
LOGGER.debug("Dropped collection [{}]",
collection.getNamespace() != null ? collection.getNamespace().getCollectionName() : collectionName);
}
return null;
}
@@ -649,7 +718,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.String)
*/
public IndexOperations indexOps(String collectionName) {
return new DefaultIndexOperations(getMongoDbFactory(), collectionName, queryMapper);
return new DefaultIndexOperations(this, collectionName, null);
}
/*
@@ -657,8 +726,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.Class)
*/
public IndexOperations indexOps(Class<?> entityClass) {
return new DefaultIndexOperations(getMongoDbFactory(), determineCollectionName(entityClass), queryMapper,
entityClass);
return new DefaultIndexOperations(this, determineCollectionName(entityClass), entityClass);
}
/*
@@ -837,10 +905,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Class<T> mongoDriverCompatibleType = getMongoDbFactory().getCodecFor(resultClass).map(Codec::getEncoderClass)
.orElse((Class) BsonValue.class);
MongoIterable<?> result = execute((db) -> {
MongoIterable<?> result = execute(collectionName, (collection) -> {
DistinctIterable<T> iterable = db.getCollection(collectionName).distinct(mappedFieldName, mappedQuery,
mongoDriverCompatibleType);
DistinctIterable<T> iterable = collection.distinct(mappedFieldName, mappedQuery, mongoDriverCompatibleType);
return query.getCollation().map(Collation::toMongoCollation).map(iterable::collation).orElse(iterable);
});
@@ -1045,10 +1112,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(query, "Query must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
CountOptions options = new CountOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
Document document = queryMapper.getMappedObject(query.getQueryObject(),
Optional.ofNullable(entityClass).map(it -> mappingContext.getPersistentEntity(entityClass)));
return execute(collectionName, collection -> collection.count(document));
return execute(collectionName, collection -> collection.count(document, options));
}
/*
@@ -1095,8 +1165,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
protected MongoCollection<Document> prepareCollection(MongoCollection<Document> collection) {
if (this.readPreference != null) {
return collection.withReadPreference(readPreference);
collection = collection.withReadPreference(readPreference);
}
return collection;
}
@@ -1106,7 +1177,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* In case of using MongoDB Java driver version 3 the returned {@link WriteConcern} will be defaulted to
* {@link WriteConcern#ACKNOWLEDGED} when {@link WriteResultChecking} is set to {@link WriteResultChecking#EXCEPTION}.
*
* @param writeConcern any WriteConcern already configured or null
* @param mongoAction any MongoAction already configured or null
* @return The prepared WriteConcern or null
*/
@Nullable
@@ -1362,6 +1433,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName,
entityClass, document, null);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
if (writeConcernToUse == null) {
collection.insertOne(document);
} else {
@@ -1421,10 +1493,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collection.withWriteConcern(writeConcernToUse).insertOne(dbDoc);
}
} else if (writeConcernToUse == null) {
collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new UpdateOptions().upsert(true));
collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new ReplaceOptions().upsert(true));
} else {
collection.withWriteConcern(writeConcernToUse).replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc,
new UpdateOptions().upsert(true));
new ReplaceOptions().upsert(true));
}
return dbDoc.get(ID_FIELD);
}
@@ -1530,7 +1602,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection;
if (!UpdateMapper.isUpdateObject(updateObj)) {
return collection.replaceOne(queryObj, updateObj, opts);
ReplaceOptions replaceOptions = new ReplaceOptions();
replaceOptions.collation(opts.getCollation());
replaceOptions.upsert(opts.isUpsert());
return collection.replaceOne(queryObj, updateObj, replaceOptions);
} else {
if (multi) {
return collection.updateMany(queryObj, updateObj, opts);
@@ -1566,7 +1643,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(object, "Object must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
return doRemove(collectionName, getIdQueryFor(object), object.getClass());
return doRemove(collectionName, getIdQueryFor(object), object.getClass(), false);
}
/**
@@ -1655,7 +1732,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
@Override
public DeleteResult remove(Query query, String collectionName) {
return doRemove(collectionName, query, null);
return doRemove(collectionName, query, null, true);
}
@Override
@@ -1667,19 +1744,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public DeleteResult remove(Query query, Class<?> entityClass, String collectionName) {
Assert.notNull(entityClass, "EntityClass must not be null!");
return doRemove(collectionName, query, entityClass);
return doRemove(collectionName, query, entityClass, true);
}
protected <T> DeleteResult doRemove(final String collectionName, final Query query,
@Nullable final Class<T> entityClass) {
@Nullable final Class<T> entityClass, boolean multi) {
Assert.notNull(query, "Query must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
if (query == null) {
throw new InvalidDataAccessApiUsageException("Query passed in to remove can't be null!");
}
final Document queryObject = query.getQueryObject();
final MongoPersistentEntity<?> entity = getPersistentEntity(entityClass);
final Document queryObject = queryMapper.getMappedObject(query.getQueryObject(), entity);
return execute(collectionName, new CollectionCallback<DeleteResult>() {
@@ -1688,7 +1763,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
maybeEmitEvent(new BeforeDeleteEvent<T>(queryObject, entityClass, collectionName));
Document mappedQuery = queryMapper.getMappedObject(queryObject, entity);
Document removeQuery = queryObject;
DeleteOptions options = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
@@ -1698,22 +1773,33 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
DeleteResult dr = null;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Remove using query: {} in collection: {}.",
new Object[] { serializeToJsonSafely(mappedQuery), collectionName });
new Object[] { serializeToJsonSafely(removeQuery), collectionName });
}
if (writeConcernToUse == null) {
if (query.getLimit() > 0 || query.getSkip() > 0) {
dr = collection.deleteMany(mappedQuery, options);
} else {
dr = collection.withWriteConcern(writeConcernToUse).deleteMany(mappedQuery, options);
MongoCursor<Document> cursor = new QueryCursorPreparer(query, entityClass)
.prepare(collection.find(removeQuery).projection(new Document(ID_FIELD, 1))).iterator();
Set<Object> ids = new LinkedHashSet<>();
while (cursor.hasNext()) {
ids.add(cursor.next().get(ID_FIELD));
}
removeQuery = new Document(ID_FIELD, new Document("$in", ids));
}
MongoCollection<Document> collectionToUse = writeConcernToUse != null
? collection.withWriteConcern(writeConcernToUse) : collection;
DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options)
: collection.deleteOne(removeQuery, options);
maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass, collectionName));
return dr;
return result;
}
});
}
@@ -1753,30 +1839,48 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class<T> entityClass) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(inputCollectionName, "InputCollectionName must not be null!");
Assert.notNull(entityClass, "EntityClass must not be null!");
Assert.notNull(reduceFunction, "ReduceFunction must not be null!");
Assert.notNull(mapFunction, "MapFunction must not be null!");
return new MapReduceResults<>(
mapReduce(query, entityClass, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, entityClass),
new Document());
}
/**
* @param query
* @param domainType
* @param inputCollectionName
* @param mapFunction
* @param reduceFunction
* @param mapReduceOptions
* @param resultType
* @return
* @since 2.1
*/
public <T> List<T> mapReduce(Query query, Class<?> domainType, String inputCollectionName, String mapFunction,
String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class<T> resultType) {
Assert.notNull(domainType, "Domain type must not be null!");
Assert.notNull(inputCollectionName, "Input collection name must not be null!");
Assert.notNull(resultType, "Result type must not be null!");
Assert.notNull(mapFunction, "Map function must not be null!");
Assert.notNull(reduceFunction, "Reduce function must not be null!");
String mapFunc = replaceWithResourceIfNecessary(mapFunction);
String reduceFunc = replaceWithResourceIfNecessary(reduceFunction);
MongoCollection<Document> inputCollection = getCollection(inputCollectionName);
MongoCollection<Document> inputCollection = getAndPrepareCollection(doGetDatabase(), inputCollectionName);
// MapReduceOp
MapReduceIterable<Document> result = inputCollection.mapReduce(mapFunc, reduceFunc);
if (query != null && result != null) {
MapReduceIterable<Document> mapReduce = inputCollection.mapReduce(mapFunc, reduceFunc, Document.class);
if (query.getLimit() > 0 && mapReduceOptions.getLimit() == null) {
result = result.limit(query.getLimit());
if (query.getLimit() > 0 && mapReduceOptions != null && mapReduceOptions.getLimit() == null) {
mapReduce = mapReduce.limit(query.getLimit());
}
if (query.getMeta() != null && query.getMeta().getMaxTimeMsec() != null) {
result = result.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
if (query.getMeta().getMaxTimeMsec() != null) {
mapReduce = mapReduce.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
result = result.sort(getMappedSortObject(query, entityClass));
mapReduce = mapReduce.sort(getMappedSortObject(query, domainType));
result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty()));
}
mapReduce = mapReduce
.filter(queryMapper.getMappedObject(query.getQueryObject(), mappingContext.getPersistentEntity(domainType)));
Optional<Collation> collation = query.getCollation();
@@ -1792,32 +1896,32 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
if (!CollectionUtils.isEmpty(mapReduceOptions.getScopeVariables())) {
result = result.scope(new Document(mapReduceOptions.getScopeVariables()));
mapReduce = mapReduce.scope(new Document(mapReduceOptions.getScopeVariables()));
}
if (mapReduceOptions.getLimit() != null && mapReduceOptions.getLimit().intValue() > 0) {
result = result.limit(mapReduceOptions.getLimit());
if (mapReduceOptions.getLimit() != null && mapReduceOptions.getLimit() > 0) {
mapReduce = mapReduce.limit(mapReduceOptions.getLimit());
}
if (mapReduceOptions.getFinalizeFunction().filter(StringUtils::hasText).isPresent()) {
result = result.finalizeFunction(mapReduceOptions.getFinalizeFunction().get());
mapReduce = mapReduce.finalizeFunction(mapReduceOptions.getFinalizeFunction().get());
}
if (mapReduceOptions.getJavaScriptMode() != null) {
result = result.jsMode(mapReduceOptions.getJavaScriptMode());
mapReduce = mapReduce.jsMode(mapReduceOptions.getJavaScriptMode());
}
if (mapReduceOptions.getOutputSharded().isPresent()) {
result = result.sharded(mapReduceOptions.getOutputSharded().get());
mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get());
}
}
result = collation.map(Collation::toMongoCollation).map(result::collation).orElse(result);
mapReduce = collation.map(Collation::toMongoCollation).map(mapReduce::collation).orElse(mapReduce);
List<T> mappedResults = new ArrayList<T>();
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, entityClass, inputCollectionName);
List<T> mappedResults = new ArrayList<>();
DocumentCallback<T> callback = new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName);
for (Document document : result) {
for (Document document : mapReduce) {
mappedResults.add(callback.doWith(document));
}
return new MapReduceResults<T>(mappedResults, new Document());
return mappedResults;
}
public <T> GroupByResults<T> group(String inputCollectionName, GroupBy groupBy, Class<T> entityClass) {
@@ -1847,13 +1951,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
if (document.containsKey("$reduce")) {
document.put("$reduce", replaceWithResourceIfNecessary(document.get("$reduce").toString()));
document.put("$reduce", replaceWithResourceIfNecessary(ObjectUtils.nullSafeToString(document.get("$reduce"))));
}
if (document.containsKey("$keyf")) {
document.put("$keyf", replaceWithResourceIfNecessary(document.get("$keyf").toString()));
document.put("$keyf", replaceWithResourceIfNecessary(ObjectUtils.nullSafeToString(document.get("$keyf"))));
}
if (document.containsKey("finalize")) {
document.put("finalize", replaceWithResourceIfNecessary(document.get("finalize").toString()));
document.put("finalize", replaceWithResourceIfNecessary(ObjectUtils.nullSafeToString(document.get("finalize"))));
}
Document commandObject = new Document("group", document);
@@ -1862,7 +1966,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
LOGGER.debug("Executing Group with Document [{}]", serializeToJsonSafely(commandObject));
}
Document commandResult = executeCommand(commandObject);
Document commandResult = executeCommand(commandObject, this.readPreference);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Group command result = [{}]", commandResult);
@@ -2087,7 +2191,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return execute(collectionName, (CollectionCallback<CloseableIterator<O>>) collection -> {
AggregateIterable<Document> cursor = collection.aggregate(pipeline) //
AggregateIterable<Document> cursor = collection.aggregate(pipeline, Document.class) //
.allowDiskUse(options.isAllowDiskUse()) //
.useCursor(true);
@@ -2139,6 +2243,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return new ExecutableAggregationOperationSupport(this).aggregateAndReturn(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableAggregationOperation#aggregateAndReturn(java.lang.Class)
*/
@Override
public <T> ExecutableMapReduce<T> mapReduce(Class<T> domainType) {
return new ExecutableMapReduceOperationSupport(this).mapReduce(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#insert(java.lang.Class)
@@ -2194,7 +2307,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
public MongoDatabase getDb() {
return mongoDbFactory.getDb();
return doGetDatabase();
}
protected MongoDatabase doGetDatabase() {
return MongoDatabaseUtils.getDatabase(mongoDbFactory, sessionSynchronization);
}
protected MongoDatabase prepareDatabase(MongoDatabase database) {
return database;
}
protected <T> void maybeEmitEvent(MongoMappingEvent<T> event) {
@@ -2252,7 +2373,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
// TODO: Emit a collection created event
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Created collection [{}]", coll.getNamespace().getCollectionName());
LOGGER.debug("Created collection [{}]",
coll.getNamespace() != null ? coll.getNamespace().getCollectionName() : collectionName);
}
return coll;
}
@@ -2483,15 +2605,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @param savedObject
* @param id
*/
@SuppressWarnings("unchecked")
protected void populateIdIfNecessary(Object savedObject, Object id) {
if (id == null) {
return;
}
if (savedObject instanceof Document) {
Document document = (Document) savedObject;
document.put(ID_FIELD, id);
if (savedObject instanceof Map) {
Map<String, Object> map = (Map<String, Object>) savedObject;
map.put(ID_FIELD, id);
return;
}
@@ -2539,7 +2664,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
T result = objectCallback
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), collectionName)));
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)));
return result;
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -2574,7 +2699,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDb(), collectionName));
.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
@@ -2610,7 +2735,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
try {
FindIterable<Document> iterable = collectionCallback
.doInCollection(getAndPrepareCollection(getDb(), collectionName));
.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));
if (preparer != null) {
iterable = preparer.prepare(iterable);
@@ -2755,12 +2880,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
FindIterable<Document> iterable = collection.find(query);
FindIterable<Document> iterable = collection.find(query, Document.class);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query),
serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName());
serializeToJsonSafely(fields.orElseGet(Document::new)),
collection.getNamespace() != null ? collection.getNamespace().getFullName() : "n/a");
}
if (fields.isPresent()) {
@@ -2800,7 +2926,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public FindIterable<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
return collection.find(query).projection(fields);
return collection.find(query, Document.class).projection(fields);
}
}
@@ -2903,7 +3029,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
/**
* Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given
* {@link MongoReader}.
* {@link EntityReader}.
*
* @author Oliver Gierke
* @author Christoph Strobl
@@ -3211,4 +3337,195 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public MongoDbFactory getMongoDbFactory() {
return mongoDbFactory;
}
/**
* {@link BatchAggregationLoader} is a little helper that can process cursor results returned by an aggregation
* command execution. On presence of a {@literal nextBatch} indicated by presence of an {@code id} field in the
* {@code cursor} another {@code getMore} command gets executed reading the next batch of documents until all results
* are loaded.
*
* @author Christoph Strobl
* @since 1.10
*/
static class BatchAggregationLoader {
private static final String CURSOR_FIELD = "cursor";
private static final String RESULT_FIELD = "result";
private static final String BATCH_SIZE_FIELD = "batchSize";
private static final String FIRST_BATCH = "firstBatch";
private static final String NEXT_BATCH = "nextBatch";
private static final String SERVER_USED = "serverUsed";
private static final String OK = "ok";
private final MongoTemplate template;
private final ReadPreference readPreference;
private final int batchSize;
BatchAggregationLoader(MongoTemplate template, ReadPreference readPreference, int batchSize) {
this.template = template;
this.readPreference = readPreference;
this.batchSize = batchSize;
}
/**
* Run aggregation command and fetch all results.
*/
Document aggregate(String collectionName, Aggregation aggregation, AggregationOperationContext context) {
Document command = prepareAggregationCommand(collectionName, aggregation, context, batchSize);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing aggregation: {}", serializeToJsonSafely(command));
}
return mergeAggregationResults(aggregateBatched(command, collectionName, batchSize));
}
/**
* Pre process the aggregation command sent to the server by adding {@code cursor} options to match execution on
* different server versions.
*/
private static Document prepareAggregationCommand(String collectionName, Aggregation aggregation,
@Nullable AggregationOperationContext context, int batchSize) {
AggregationOperationContext rootContext = context == null ? Aggregation.DEFAULT_CONTEXT : context;
Document command = aggregation.toDocument(collectionName, rootContext);
if (!aggregation.getOptions().isExplain()) {
command.put(CURSOR_FIELD, new Document(BATCH_SIZE_FIELD, batchSize));
}
return command;
}
private List<Document> aggregateBatched(Document command, String collectionName, int batchSize) {
List<Document> results = new ArrayList<>();
Document commandResult = template.executeCommand(command, readPreference);
results.add(postProcessResult(commandResult));
while (hasNext(commandResult)) {
Document getMore = new Document("getMore", getNextBatchId(commandResult)) //
.append("collection", collectionName) //
.append(BATCH_SIZE_FIELD, batchSize);
commandResult = template.executeCommand(getMore, this.readPreference);
results.add(postProcessResult(commandResult));
}
return results;
}
private static Document postProcessResult(Document commandResult) {
if (!commandResult.containsKey(CURSOR_FIELD)) {
return commandResult;
}
Document resultObject = new Document(SERVER_USED, commandResult.get(SERVER_USED));
resultObject.put(OK, commandResult.get(OK));
Document cursor = (Document) commandResult.get(CURSOR_FIELD);
if (cursor.containsKey(FIRST_BATCH)) {
resultObject.put(RESULT_FIELD, cursor.get(FIRST_BATCH));
} else {
resultObject.put(RESULT_FIELD, cursor.get(NEXT_BATCH));
}
return resultObject;
}
private static Document mergeAggregationResults(List<Document> batchResults) {
if (batchResults.size() == 1) {
return batchResults.iterator().next();
}
Document commandResult = new Document();
List<Object> allResults = new ArrayList<>();
for (Document batchResult : batchResults) {
Collection documents = (Collection<?>) batchResult.get(RESULT_FIELD);
if (!CollectionUtils.isEmpty(documents)) {
allResults.addAll(documents);
}
}
// take general info from first batch
commandResult.put(SERVER_USED, batchResults.iterator().next().get(SERVER_USED));
commandResult.put(OK, batchResults.iterator().next().get(OK));
// and append the merged batchResults
commandResult.put(RESULT_FIELD, allResults);
return commandResult;
}
private static boolean hasNext(Document commandResult) {
if (!commandResult.containsKey(CURSOR_FIELD)) {
return false;
}
Object next = getNextBatchId(commandResult);
return next != null && ((Number) next).longValue() != 0L;
}
@Nullable
private static Object getNextBatchId(Document commandResult) {
return ((Document) commandResult.get(CURSOR_FIELD)).get("id");
}
}
/**
* {@link MongoTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with the
* server through the driver API.
* <p />
* The prepare steps for {@link MongoDatabase} and {@link MongoCollection} proxy the target and invoke the desired
* target method matching the actual arguments plus a {@link ClientSession}.
*
* @author Christoph Strobl
* @since 2.1
*/
static class SessionBoundMongoTemplate extends MongoTemplate {
private final MongoTemplate delegate;
/**
* @param session must not be {@literal null}.
* @param that must not be {@literal null}.
*/
SessionBoundMongoTemplate(ClientSession session, MongoTemplate that) {
super(that.getMongoDbFactory().withSession(session), that);
this.delegate = that;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoTemplate#getCollection(java.lang.String)
*/
@Override
public MongoCollection<Document> getCollection(String collectionName) {
// native MongoDB objects that offer methods with ClientSession must not be proxied.
return delegate.getCollection(collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoTemplate#getDb()
*/
@Override
public MongoDatabase getDb() {
// native MongoDB objects that offer methods with ClientSession must not be proxied.
return delegate.getDb();
}
}
}

View File

@@ -19,7 +19,8 @@ package org.springframework.data.mongodb.core;
* Stripped down interface providing access to a fluent API that specifies a basic set of reactive MongoDB operations.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
*/
public interface ReactiveFluentMongoOperations extends ReactiveFindOperation, ReactiveInsertOperation,
ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation {}
ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation, ReactiveMapReduceOperation {}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ReactiveMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ReactiveMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the {@link Flux} emitting mapReduce results.
*
* @return a {@link Flux} emitting the already mapped operation results.
*/
Flux<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ReactiveMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ReactiveMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ReactiveMapReduce} provides methods for constructing reactive mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ReactiveMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ReactiveMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
@RequiredArgsConstructor
class ReactiveMapReduceOperationSupport implements ReactiveMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final @NonNull ReactiveMongoTemplate template;
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ReactiveMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ReactiveMapReduceSupport<T>
implements ReactiveMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>, MapReduceWithProjection<T>,
MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final ReactiveMongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ReactiveMapReduceSupport(ReactiveMongoTemplate template, Class<?> domainType, Class<T> returnType,
@Nullable String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction,
@Nullable MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public Flux<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction,
options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ReactiveMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ReactiveMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.reactivestreams.Publisher;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import com.mongodb.reactivestreams.client.ClientSession;
/**
* {@link ReactiveMongoContext} utilizes and enriches the Reactor {@link Context} with information potentially required
* for e.g. {@link ClientSession} handling and transactions.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
* @see Mono#subscriberContext()
* @see Context
*/
public class ReactiveMongoContext {
private static final Class<?> SESSION_KEY = ClientSession.class;
/**
* Gets the {@code Mono<ClientSession>} from Reactor {@link reactor.util.context.Context}. The resulting {@link Mono}
* emits the {@link ClientSession} if a session is associated with the current {@link reactor.util.context.Context
* subscriber context}. If the context does not contain a session, the resulting {@link Mono} terminates empty (i.e.
* without emitting a value).
*
* @return the {@link Mono} emitting the client session if present; otherwise the {@link Mono} terminates empty.
*/
public static Mono<ClientSession> getSession() {
return Mono.subscriberContext().filter(ctx -> ctx.hasKey(SESSION_KEY))
.flatMap(ctx -> ctx.<Mono<ClientSession>> get(SESSION_KEY));
}
/**
* Sets the {@link ClientSession} into the Reactor {@link reactor.util.context.Context}.
*
* @param context must not be {@literal null}.
* @param session must not be {@literal null}.
* @return a new {@link Context}.
* @see Context#put(Object, Object)
*/
public static Context setSession(Context context, Publisher<ClientSession> session) {
Assert.notNull(context, "Context must not be null!");
Assert.notNull(session, "Session publisher must not be null!");
return context.put(SESSION_KEY, Mono.from(session));
}
}

View File

@@ -20,6 +20,8 @@ import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.bson.Document;
import org.reactivestreams.Publisher;
@@ -31,16 +33,20 @@ import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.index.ReactiveIndexOperations;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.ReadPreference;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoCollection;
/**
@@ -141,6 +147,92 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
*/
<T> Flux<T> execute(String collectionName, ReactiveCollectionCallback<T> action);
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession}
* provided by the given {@link Supplier} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1
*/
default ReactiveSessionScoped withSession(Supplier<ClientSession> sessionProvider) {
Assert.notNull(sessionProvider, "SessionProvider must not be null!");
return withSession(Mono.fromSupplier(sessionProvider));
}
/**
* Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession}
* with given {@literal sessionOptions} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
*
* @param sessionOptions must not be {@literal null}.
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1
*/
ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions);
/**
* Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped} binding the
* {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against MongoDB.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle. Use
* {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the
* {@link ClientSession} when done.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1
*/
ReactiveSessionScoped withSession(Publisher<ClientSession> sessionProvider);
/**
* Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
* <p />
* <strong>Note:</strong> It is up to the caller to manage the {@link ClientSession} lifecycle.
*
* @param session must not be {@literal null}.
* @return {@link ClientSession} bound instance of {@link ReactiveMongoOperations}.
* @since 2.1
*/
ReactiveMongoOperations withSession(ClientSession session);
/**
* Initiate a new {@link ClientSession} and obtain a {@link ClientSession session} bound instance of
* {@link ReactiveSessionScoped}. Starts the transaction and adds the {@link ClientSession} to each and every command
* issued against MongoDB.
* <p/>
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
*
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
*/
ReactiveSessionScoped inTransaction();
/**
* Obtain a {@link ClientSession session} bound instance of {@link ReactiveSessionScoped}, start the transaction and
* bind the {@link ClientSession} provided by the given {@link Publisher} to each and every command issued against
* MongoDB.
* <p/>
* Each {@link ReactiveSessionScoped#execute(ReactiveSessionCallback) execution} initiates a new managed transaction
* that is {@link ClientSession#commitTransaction() committed} on success. Transactions are
* {@link ClientSession#abortTransaction() rolled back} upon errors.
*
* @param sessionProvider must not be {@literal null}.
* @return new instance of {@link ReactiveSessionScoped}. Never {@literal null}.
* @since 2.1
*/
ReactiveSessionScoped inTransaction(Publisher<ClientSession> sessionProvider);
/**
* Create an uncapped collection with a name based on the provided entity class.
*
@@ -1153,6 +1245,42 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Flux<ChangeStreamEvent<T>> changeStream(List<Document> filter, Class<T> resultType, ChangeStreamOptions options,
String collectionName);
/**
* Execute a map-reduce operation. Use {@link MapReduceOptions} to optionally specify an output collection and other
* args.
*
* @param filterQuery the selection criteria for the documents going input to the map function. Must not be
* {@literal null}.
* @param domainType source type used to determine the input collection name and map the filter {@link Query} against.
* Must not be {@literal null}.
* @param resultType the mapping target of the operations result documents. Must not be {@literal null}.
* @param mapFunction the JavaScript map function. Must not be {@literal null}.
* @param reduceFunction the JavaScript reduce function. Must not be {@literal null}.
* @param options additional options like output collection. Must not be {@literal null}.
* @return a {@link Flux} emitting the result document sequence. Never {@literal null}.
* @since 2.1
*/
<T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, Class<T> resultType, String mapFunction,
String reduceFunction, MapReduceOptions options);
/**
* Execute a map-reduce operation. Use {@link MapReduceOptions} to optionally specify an output collection and other
* args.
*
* @param filterQuery the selection criteria for the documents going input to the map function. Must not be
* {@literal null}.
* @param domainType source type used to map the filter {@link Query} against. Must not be {@literal null}.
* @param inputCollectionName the input collection.
* @param resultType the mapping target of the operations result documents. Must not be {@literal null}.
* @param mapFunction the JavaScript map function. Must not be {@literal null}.
* @param reduceFunction the JavaScript reduce function. Must not be {@literal null}.
* @param options additional options like output collection. Must not be {@literal null}.
* @return a {@link Flux} emitting the result document sequence. Never {@literal null}.
* @since 2.1
*/
<T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, String inputCollectionName, Class<T> resultType,
String mapFunction, String reduceFunction, MapReduceOptions options);
/**
* Returns the underlying {@link MongoConverter}.
*

View File

@@ -26,6 +26,8 @@ import reactor.util.function.Tuple2;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -37,6 +39,7 @@ import org.bson.codecs.Codec;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@@ -57,10 +60,12 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
@@ -71,10 +76,9 @@ import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggr
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.*;
import org.springframework.data.mongodb.core.index.IndexOperationsAdapter;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.index.ReactiveIndexOperations;
import org.springframework.data.mongodb.core.index.ReactiveMongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -87,6 +91,7 @@ import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
@@ -101,10 +106,13 @@ import org.springframework.data.util.Pair;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.ClientSessionOptions;
import com.mongodb.CursorType;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
@@ -113,10 +121,13 @@ import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationOptions;
@@ -125,8 +136,10 @@ import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.AggregatePublisher;
import com.mongodb.reactivestreams.client.ChangeStreamPublisher;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.DistinctPublisher;
import com.mongodb.reactivestreams.client.FindPublisher;
import com.mongodb.reactivestreams.client.MapReducePublisher;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
@@ -175,13 +188,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private final ApplicationListener<MappingContextEvent<?, ?>> indexCreatorListener;
private @Nullable WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
private WriteResultChecking writeResultChecking = WriteResultChecking.NONE;
private @Nullable ReadPreference readPreference;
private @Nullable ApplicationEventPublisher eventPublisher;
private @Nullable MongoPersistentEntityIndexCreator indexCreator;
private @Nullable ReactiveMongoPersistentEntityIndexCreator indexCreator;
/**
* Constructor used for a basic template configuration.
@@ -190,7 +204,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param databaseName must not be {@literal null} or empty.
*/
public ReactiveMongoTemplate(MongoClient mongoClient, String databaseName) {
this(new SimpleReactiveMongoDatabaseFactory(mongoClient, databaseName), null);
this(new SimpleReactiveMongoDatabaseFactory(mongoClient, databaseName), (MongoConverter) null);
}
/**
@@ -199,7 +213,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param mongoDatabaseFactory must not be {@literal null}.
*/
public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory) {
this(mongoDatabaseFactory, null);
this(mongoDatabaseFactory, (MongoConverter) null);
}
/**
@@ -210,6 +224,21 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory,
@Nullable MongoConverter mongoConverter) {
this(mongoDatabaseFactory, mongoConverter, ReactiveMongoTemplate::handleSubscriptionException);
}
/**
* Constructor used for a basic template configuration.
*
* @param mongoDatabaseFactory must not be {@literal null}.
* @param mongoConverter can be {@literal null}.
* @param subscriptionExceptionHandler exception handler called by {@link Flux#doOnError(Consumer)} on reactive type
* materialization via {@link Publisher#subscribe(Subscriber)}. This callback is used during non-blocking
* subscription of e.g. index creation {@link Publisher}s. Must not be {@literal null}.
* @since 2.1
*/
public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory,
@Nullable MongoConverter mongoConverter, Consumer<Throwable> subscriptionExceptionHandler) {
Assert.notNull(mongoDatabaseFactory, "ReactiveMongoDatabaseFactory must not be null!");
@@ -220,19 +249,47 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory();
this.indexCreatorListener = new IndexCreatorEventListener(subscriptionExceptionHandler);
// We always have a mapping context in the converter, whether it's a simple one or not
mappingContext = this.mongoConverter.getMappingContext();
// We create indexes based on mapping events
this.mappingContext = this.mongoConverter.getMappingContext();
if (mappingContext instanceof MongoMappingContext) {
indexCreator = new MongoPersistentEntityIndexCreator((MongoMappingContext) mappingContext,
(collectionName) -> IndexOperationsAdapter.blocking(indexOps(collectionName)));
eventPublisher = new MongoMappingEventPublisher(indexCreator);
if (mappingContext instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
// We create indexes based on mapping events
if (this.mappingContext instanceof MongoMappingContext) {
MongoMappingContext mongoMappingContext = (MongoMappingContext) this.mappingContext;
this.indexCreator = new ReactiveMongoPersistentEntityIndexCreator(mongoMappingContext, this::indexOps);
this.eventPublisher = new MongoMappingEventPublisher(this.indexCreatorListener);
mongoMappingContext.setApplicationEventPublisher(this.eventPublisher);
this.mappingContext.getPersistentEntities()
.forEach(entity -> onCheckForIndexes(entity, subscriptionExceptionHandler));
}
}
private ReactiveMongoTemplate(ReactiveMongoDatabaseFactory dbFactory, ReactiveMongoTemplate that) {
this.mongoDatabaseFactory = dbFactory;
this.exceptionTranslator = that.exceptionTranslator;
this.mongoConverter = that.mongoConverter;
this.queryMapper = that.queryMapper;
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.projectionFactory = that.projectionFactory;
this.indexCreator = that.indexCreator;
this.indexCreatorListener = that.indexCreatorListener;
this.mappingContext = that.mappingContext;
}
private void onCheckForIndexes(MongoPersistentEntity<?> entity, Consumer<Throwable> subscriptionExceptionHandler) {
if (indexCreator != null) {
indexCreator.checkForIndexes(entity).subscribe(v -> {}, subscriptionExceptionHandler);
}
}
private static void handleSubscriptionException(Throwable t) {
LOGGER.error("Unexpected exception during asynchronous execution", t);
}
/**
@@ -266,7 +323,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
/**
* Used by @{link {@link #prepareCollection(MongoCollection)} to set the {@link ReadPreference} before any operations
* Used by {@link {@link #prepareCollection(MongoCollection)} to set the {@link ReadPreference} before any operations
* are performed.
*
* @param readPreference
@@ -293,26 +350,28 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
/**
* Inspects the given {@link ApplicationContext} for {@link MongoPersistentEntityIndexCreator} and those in turn if
* they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext}
* can be found we manually add the internally created one as {@link ApplicationListener} to make sure indexes get
* created appropriately for entity types persisted through this {@link ReactiveMongoTemplate} instance.
* Inspects the given {@link ApplicationContext} for {@link ReactiveMongoPersistentEntityIndexCreator} and those in
* turn if they were registered for the current {@link MappingContext}. If no creator for the current
* {@link MappingContext} can be found we manually add the internally created one as {@link ApplicationListener} to
* make sure indexes get created appropriately for entity types persisted through this {@link ReactiveMongoTemplate}
* instance.
*
* @param context must not be {@literal null}.
*/
private void prepareIndexCreator(ApplicationContext context) {
String[] indexCreators = context.getBeanNamesForType(MongoPersistentEntityIndexCreator.class);
String[] indexCreators = context.getBeanNamesForType(ReactiveMongoPersistentEntityIndexCreator.class);
for (String creator : indexCreators) {
MongoPersistentEntityIndexCreator creatorBean = context.getBean(creator, MongoPersistentEntityIndexCreator.class);
ReactiveMongoPersistentEntityIndexCreator creatorBean = context.getBean(creator,
ReactiveMongoPersistentEntityIndexCreator.class);
if (creatorBean.isIndexCreatorFor(mappingContext)) {
return;
}
}
if (context instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) context).addApplicationListener(indexCreator);
((ConfigurableApplicationContext) context).addApplicationListener(indexCreatorListener);
}
}
@@ -402,9 +461,111 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public <T> Flux<T> execute(String collectionName, ReactiveCollectionCallback<T> callback) {
Assert.notNull(callback, "ReactiveCollectionCallback must not be null!");
return createFlux(collectionName, callback);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(org.reactivestreams.Publisher, java.util.function.Consumer)
*/
@Override
public ReactiveSessionScoped withSession(Publisher<ClientSession> sessionProvider) {
Mono<ClientSession> cachedSession = Mono.from(sessionProvider).cache();
return new ReactiveSessionScoped() {
@Override
public <T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally) {
return cachedSession.flatMapMany(session -> {
return ReactiveMongoTemplate.this.withSession(action, session) //
.doFinally(signalType -> {
doFinally.accept(session);
});
});
}
};
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#inTransaction()
*/
@Override
public ReactiveSessionScoped inTransaction() {
return inTransaction(mongoDatabaseFactory
.getSession(ClientSessionOptions.builder().causallyConsistent(true).build()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#inTransaction(org.reactivestreams.Publisher)
*/
@Override
public ReactiveSessionScoped inTransaction(Publisher<ClientSession> sessionProvider) {
Mono<ClientSession> cachedSession = Mono.from(sessionProvider).cache();
return new ReactiveSessionScoped() {
@Override
public <T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally) {
return cachedSession.flatMapMany(session -> {
if (!session.hasActiveTransaction()) {
session.startTransaction();
}
return ReactiveMongoTemplate.this.withSession(action, session) //
.materialize() //
.flatMap(signal -> {
if (session.hasActiveTransaction()) {
if (signal.isOnComplete()) {
return Mono.from(session.commitTransaction()).thenReturn(signal);
}
if (signal.isOnError()) {
return Mono.from(session.abortTransaction()).thenReturn(signal);
}
}
return Mono.just(signal);
}) //
.<T> dematerialize() //
.doFinally(signalType -> {
doFinally.accept(session);
});
});
}
};
}
private <T> Flux<T> withSession(ReactiveSessionCallback<T> action, ClientSession session) {
return Flux.from(action.doInSession(new ReactiveSessionBoundMongoTemplate(session, ReactiveMongoTemplate.this))) //
.subscriberContext(ctx -> ReactiveMongoContext.setSession(ctx, Mono.just(session)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.session.ClientSession)
*/
public ReactiveMongoOperations withSession(ClientSession session) {
return new ReactiveSessionBoundMongoTemplate(session, ReactiveMongoTemplate.this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) {
return withSession(mongoDatabaseFactory.getSession(sessionOptions));
}
/**
* Create a reusable Flux for a {@link ReactiveDatabaseCallback}. It's up to the developer to choose to obtain a new
* {@link Flux} or to reuse the {@link Flux}.
@@ -416,7 +577,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
return Flux.defer(() -> callback.doInDB(getMongoDatabase())).onErrorMap(translateException());
return Flux.defer(() -> callback.doInDB(prepareDatabase(doGetDatabase()))).onErrorMap(translateException());
}
/**
@@ -430,7 +591,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
return Mono.defer(() -> Mono.from(callback.doInDB(getMongoDatabase()))).onErrorMap(translateException());
return Mono.defer(() -> Mono.from(callback.doInDB(prepareDatabase(doGetDatabase()))))
.onErrorMap(translateException());
}
/**
@@ -446,7 +608,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!");
Mono<MongoCollection<Document>> collectionPublisher = Mono
.fromCallable(() -> getAndPrepareCollection(getMongoDatabase(), collectionName));
.fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName));
return collectionPublisher.flatMapMany(callback::doInCollection).onErrorMap(translateException());
}
@@ -465,7 +627,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(callback, "ReactiveCollectionCallback must not be null!");
Mono<MongoCollection<Document>> collectionPublisher = Mono
.fromCallable(() -> getAndPrepareCollection(getMongoDatabase(), collectionName));
.fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName));
return collectionPublisher.flatMap(collection -> Mono.from(callback.doInCollection(collection)))
.onErrorMap(translateException());
@@ -547,7 +709,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
public Mono<Void> dropCollection(final String collectionName) {
return createMono(db -> db.getCollection(collectionName).drop()).doOnSuccess(success -> {
return createMono(collectionName, collection -> collection.drop()).doOnSuccess(success -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Dropped collection [" + collectionName + "]");
}
@@ -563,6 +725,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
public MongoDatabase getMongoDatabase() {
return doGetDatabase();
}
protected MongoDatabase doGetDatabase() {
return mongoDatabaseFactory.getMongoDatabase();
}
@@ -618,7 +784,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return createFlux(collectionName, collection -> {
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass));
FindPublisher<Document> findPublisher = collection.find(mappedQuery).projection(new Document("_id", 1));
FindPublisher<Document> findPublisher = collection.find(mappedQuery, Document.class)
.projection(new Document("_id", 1));
findPublisher = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation)
.orElse(findPublisher);
@@ -820,8 +987,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,
AggregationOptions options, ReadDocumentCallback<O> readCallback) {
AggregatePublisher<Document> cursor = collection.aggregate(pipeline).allowDiskUse(options.isAllowDiskUse())
.useCursor(true);
AggregatePublisher<Document> cursor = collection.aggregate(pipeline, Document.class)
.allowDiskUse(options.isAllowDiskUse()).useCursor(true);
if (options.getCollation().isPresent()) {
cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get());
@@ -987,7 +1154,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
: queryMapper.getMappedObject(query.getQueryObject(),
entityClass == null ? null : mappingContext.getPersistentEntity(entityClass));
return collection.count(Document);
CountOptions options = new CountOptions();
if (query != null) {
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
}
return collection.count(Document, options);
});
}
@@ -1361,10 +1533,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
} else if (writeConcernToUse == null) {
publisher = collection.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document,
new UpdateOptions().upsert(true));
new ReplaceOptions().upsert(true));
} else {
publisher = collection.withWriteConcern(writeConcernToUse)
.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new UpdateOptions().upsert(true));
.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new ReplaceOptions().upsert(true));
}
return Mono.from(publisher).map(o -> document.get(ID_FIELD));
@@ -1471,7 +1643,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation);
if (!UpdateMapper.isUpdateObject(updateObj)) {
return collectionToUse.replaceOne(queryObj, updateObj, updateOptions);
ReplaceOptions replaceOptions = new ReplaceOptions();
replaceOptions.upsert(updateOptions.isUpsert());
replaceOptions.collation(updateOptions.getCollation());
return collectionToUse.replaceOne(queryObj, updateObj, replaceOptions);
}
if (multi) {
return collectionToUse.updateMany(queryObj, updateObj, updateOptions);
@@ -1680,27 +1857,39 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return execute(collectionName, collection -> {
maybeEmitEvent(new BeforeDeleteEvent<T>(queryObject, entityClass, collectionName));
Document removeQuey = queryMapper.getMappedObject(queryObject, entity);
Document dboq = queryMapper.getMappedObject(queryObject, entity);
maybeEmitEvent(new BeforeDeleteEvent<T>(removeQuey, entityClass, collectionName));
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass,
null, queryObject);
null, removeQuey);
final DeleteOptions deleteOptions = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
MongoCollection<Document> collectionToUse = prepareCollection(collection, writeConcernToUse);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Remove using query: {} in collection: {}.",
new Object[] { serializeToJsonSafely(dboq), collectionName });
new Object[] { serializeToJsonSafely(removeQuey), collectionName });
}
query.getCollation().ifPresent(val -> {
if (query.getLimit() > 0 || query.getSkip() > 0) {
// TODO: add collation support as soon as it's there! See https://jira.mongodb.org/browse/JAVARS-27
throw new IllegalArgumentException("DeleteMany does currently not accept collation settings.");
FindPublisher<Document> cursor = new QueryFindPublisherPreparer(query, entityClass)
.prepare(collection.find(removeQuey)) //
.projection(new Document(ID_FIELD, 1));
return Flux.from(cursor) //
.map(doc -> doc.get(ID_FIELD)) //
.collectList() //
.flatMapMany(val -> {
return collectionToUse.deleteMany(new Document(ID_FIELD, new Document("$in", val)), deleteOptions);
});
return collectionToUse.deleteMany(dboq);
} else {
return collectionToUse.deleteMany(removeQuey, deleteOptions);
}
}).doOnNext(deleteResult -> maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass, collectionName)))
.next();
@@ -1832,6 +2021,115 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Flux.from(publisher).map(document -> new ChangeStreamEvent<>(document, resultType, getConverter()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#mapReduce(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.Class, java.lang.String, java.lang.String, org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
public <T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, Class<T> resultType, String mapFunction,
String reduceFunction, MapReduceOptions options) {
return mapReduce(filterQuery, domainType, determineCollectionName(domainType), resultType, mapFunction,
reduceFunction, options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#mapReduce(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String, java.lang.Class, java.lang.String, java.lang.String, org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
public <T> Flux<T> mapReduce(Query filterQuery, Class<?> domainType, String inputCollectionName, Class<T> resultType,
String mapFunction, String reduceFunction, MapReduceOptions options) {
Assert.notNull(filterQuery, "Filter query must not be null!");
Assert.notNull(domainType, "Domain type must not be null!");
Assert.hasText(inputCollectionName, "Input collection name must not be null or empty!");
Assert.notNull(resultType, "Result type must not be null!");
Assert.notNull(mapFunction, "Map function must not be null!");
Assert.notNull(reduceFunction, "Reduce function must not be null!");
Assert.notNull(options, "MapReduceOptions must not be null!");
assertLocalFunctionNames(mapFunction, reduceFunction);
return createFlux(inputCollectionName, collection -> {
Document mappedQuery = queryMapper.getMappedObject(filterQuery.getQueryObject(),
mappingContext.getPersistentEntity(domainType));
MapReducePublisher<Document> publisher = collection.mapReduce(mapFunction, reduceFunction, Document.class);
if (StringUtils.hasText(options.getOutputCollection())) {
publisher = publisher.collectionName(options.getOutputCollection());
}
publisher.filter(mappedQuery);
publisher.sort(getMappedSortObject(filterQuery, domainType));
if (filterQuery.getMeta().getMaxTimeMsec() != null) {
publisher.maxTime(filterQuery.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (filterQuery.getLimit() > 0 || (options.getLimit() != null)) {
if (filterQuery.getLimit() > 0 && (options.getLimit() != null)) {
throw new IllegalArgumentException(
"Both Query and MapReduceOptions define a limit. Please provide the limit only via one of the two.");
}
if (filterQuery.getLimit() > 0) {
publisher.limit(filterQuery.getLimit());
}
if (options.getLimit() != null) {
publisher.limit(options.getLimit());
}
}
Optional<Collation> collation = filterQuery.getCollation();
Optionals.ifAllPresent(filterQuery.getCollation(), options.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and MapReduceOptions define a collation. Please provide the collation only via one of the two.");
});
if (options.getCollation().isPresent()) {
collation = options.getCollation();
}
if (!CollectionUtils.isEmpty(options.getScopeVariables())) {
publisher = publisher.scope(new Document(options.getScopeVariables()));
}
if (options.getLimit() != null && options.getLimit() > 0) {
publisher = publisher.limit(options.getLimit());
}
if (options.getFinalizeFunction().filter(StringUtils::hasText).isPresent()) {
publisher = publisher.finalizeFunction(options.getFinalizeFunction().get());
}
if (options.getJavaScriptMode() != null) {
publisher = publisher.jsMode(options.getJavaScriptMode());
}
if (options.getOutputSharded().isPresent()) {
publisher = publisher.sharded(options.getOutputSharded().get());
}
publisher = collation.map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher);
return Flux.from(publisher)
.map(new ReadDocumentCallback<>(mongoConverter, resultType, inputCollectionName)::doWith);
});
}
private static void assertLocalFunctionNames(String... functions) {
for (String function : functions) {
if (ResourceUtils.isUrl(function)) {
throw new IllegalArgumentException(String.format(
"Blocking accessing to resource %s is not allowed using reactive infrastructure. You may load the resource at startup and cache its value.",
function));
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveFindOperation#query(java.lang.Class)
@@ -1877,6 +2175,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return new ReactiveAggregationOperationSupport(this).aggregateAndReturn(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ReactiveMapReduce<T> mapReduce(Class<T> domainType) {
return new ReactiveMapReduceOperationSupport(this).mapReduce(domainType);
}
/**
* Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)}
* and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is
@@ -2152,15 +2459,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param savedObject
* @param id
*/
@SuppressWarnings("unchecked")
private void populateIdIfNecessary(Object savedObject, @Nullable Object id) {
if (id == null) {
return;
}
if (savedObject instanceof Document) {
Document Document = (Document) savedObject;
Document.put(ID_FIELD, id);
if (savedObject instanceof Map) {
Map<String, Object> map = (Map<String, Object>) savedObject;
map.put(ID_FIELD, id);
return;
}
@@ -2184,7 +2494,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private MongoCollection<Document> getAndPrepareCollection(MongoDatabase db, String collectionName) {
try {
MongoCollection<Document> collection = db.getCollection(collectionName);
MongoCollection<Document> collection = db.getCollection(collectionName, Document.class);
return prepareCollection(collection);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -2222,6 +2532,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return collection;
}
/**
* @param database
* @return
* @since 2.1
*/
protected MongoDatabase prepareDatabase(MongoDatabase database) {
return database;
}
/**
* Prepare the WriteConcern before any processing is done using it. This allows a convenient way to apply custom
* settings in sub-classes. <br />
@@ -2310,7 +2629,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(action, "MongoDatabaseCallback must not be null!");
try {
MongoDatabase db = this.getMongoDatabase();
MongoDatabase db = this.doGetDatabase();
return action.doInDatabase(db);
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -2471,7 +2790,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public Publisher<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
FindPublisher<Document> publisher = collection.find(query);
FindPublisher<Document> publisher = collection.find(query, Document.class);
if (LOGGER.isDebugEnabled()) {
@@ -2513,13 +2832,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public FindPublisher<Document> doInCollection(MongoCollection<Document> collection) {
FindPublisher<Document> findPublisher;
if (query == null || query.isEmpty()) {
findPublisher = collection.find();
if (ObjectUtils.isEmpty(query)) {
findPublisher = collection.find(Document.class);
} else {
findPublisher = collection.find(query);
findPublisher = collection.find(query, Document.class);
}
if (fields == null || fields.isEmpty()) {
if (ObjectUtils.isEmpty(fields)) {
return findPublisher;
} else {
return findPublisher.projection(fields);
@@ -2745,7 +3064,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final Metric metric;
/**
* Creates a new {@link GeoNearResultDbObjectCallback} using the given {@link DbObjectCallback} delegate for
* Creates a new {@link GeoNearResultDbObjectCallback} using the given {@link DocumentCallback} delegate for
* {@link GeoResult} content unmarshalling.
*
* @param delegate must not be {@literal null}.
@@ -2856,6 +3175,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
static class NoOpDbRefResolver implements DbRefResolver {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolveDbRef(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolverCallback)
*/
@Override
@Nullable
public Object resolveDbRef(@Nonnull MongoPersistentProperty property, @Nonnull DBRef dbref,
@@ -2863,6 +3186,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#created(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.Object)
*/
@Override
@Nullable
public DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation,
@@ -2870,14 +3197,91 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#fetch(com.mongodb.DBRef)
*/
@Override
public Document fetch(DBRef dbRef) {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#bulkFetch(java.util.List)
*/
@Override
public List<Document> bulkFetch(List<DBRef> dbRefs) {
return Collections.emptyList();
}
}
/**
* {@link MongoTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with the
* server through the driver API.
* <p />
* The prepare steps for {@link MongoDatabase} and {@link MongoCollection} proxy the target and invoke the desired
* target method matching the actual arguments plus a {@link ClientSession}.
*
* @author Christoph Strobl
* @since 2.1
*/
static class ReactiveSessionBoundMongoTemplate extends ReactiveMongoTemplate {
private final ReactiveMongoTemplate delegate;
/**
* @param session must not be {@literal null}.
* @param that must not be {@literal null}.
*/
ReactiveSessionBoundMongoTemplate(ClientSession session, ReactiveMongoTemplate that) {
super(that.mongoDatabaseFactory.withSession(session), that);
this.delegate = that;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getCollection(java.lang.String)
*/
@Override
public MongoCollection<Document> getCollection(String collectionName) {
// native MongoDB objects that offer methods with ClientSession must not be proxied.
return delegate.getCollection(collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getMongoDatabase()
*/
@Override
public MongoDatabase getMongoDatabase() {
// native MongoDB objects that offer methods with ClientSession must not be proxied.
return delegate.getMongoDatabase();
}
}
@RequiredArgsConstructor
class IndexCreatorEventListener implements ApplicationListener<MappingContextEvent<?, ?>> {
final Consumer<Throwable> subscriptionExceptionHandler;
@Override
public void onApplicationEvent(MappingContextEvent<?, ?> event) {
if (!event.wasEmittedBy(mappingContext)) {
return;
}
PersistentEntity<?, ?> entity = event.getPersistentEntity();
// Double check type as Spring infrastructure does not consider nested generics
if (entity instanceof MongoPersistentEntity) {
onCheckForIndexes((MongoPersistentEntity<?>) entity, subscriptionExceptionHandler);
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.reactivestreams.Publisher;
import org.springframework.data.mongodb.core.query.Query;
/**
* Callback interface for executing operations within a {@link com.mongodb.reactivestreams.client.ClientSession} using
* reactive infrastructure.
*
* @author Christoph Strobl
* @since 2.1
* @see com.mongodb.reactivestreams.client.ClientSession
*/
@FunctionalInterface
public interface ReactiveSessionCallback<T> {
/**
* Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is
* inferred directly into the operation so that no further interaction is necessary.
* <p />
* Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and
* others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway
* objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or
* {@link com.mongodb.reactivestreams.client.MongoDatabase} via eg.
* {@link ReactiveMongoOperations#getCollection(String)} we leave responsibility for
* {@link com.mongodb.session.ClientSession} again up to the caller.
*
* @param operations will never be {@literal null}.
* @return never {@literal null}.
*/
Publisher<T> doInSession(ReactiveMongoOperations operations);
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import reactor.core.publisher.Flux;
import java.util.function.Consumer;
import com.mongodb.reactivestreams.client.ClientSession;
/**
* Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a
* {@link ReactiveSessionCallback}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public interface ReactiveSessionScoped {
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param <T> return type.
* @return a result object returned by the action, can be {@link Flux#empty()}.
*/
default <T> Flux<T> execute(ReactiveSessionCallback<T> action) {
return execute(action, (session) -> {});
}
/**
* Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param doFinally callback object that accepts {@link ClientSession} after invoking {@link ReactiveSessionCallback}.
* This {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of
* {@link ReactiveSessionCallback}).
* @param <T> return type.
* @return a result object returned by the action, can be {@link Flux#empty()}.
*/
<T> Flux<T> execute(ReactiveSessionCallback<T> action, Consumer<ClientSession> doFinally);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
/**
* Callback interface for executing operations within a {@link com.mongodb.session.ClientSession}.
*
* @author Christoph Strobl
* @since 2.1
* @see com.mongodb.session.ClientSession
*/
@FunctionalInterface
public interface SessionCallback<T> {
/**
* Execute operations against a MongoDB instance via session bound {@link MongoOperations}. The session is inferred
* directly into the operation so that no further interaction is necessary.
* <p />
* Please note that only Spring Data-specific abstractions like {@link MongoOperations#find(Query, Class)} and others
* are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway objects like
* {@link com.mongodb.client.MongoCollection} or {@link com.mongodb.client.MongoDatabase} via eg.
* {@link MongoOperations#getCollection(String)} we leave responsibility for {@link com.mongodb.session.ClientSession}
* again up to the caller.
*
* @param operations will never be {@literal null}.
* @return can be {@literal null}.
*/
@Nullable
T doInSession(MongoOperations operations);
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import com.mongodb.client.ClientSession;
/**
* Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}.
* <p />
* The very same bound {@link ClientSession} is used for all invocations of {@code execute} on the instance.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.1
*/
public interface SessionScoped {
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}.
*/
@Nullable
default <T> T execute(SessionCallback<T> action) {
return execute(action, session -> {});
}
/**
* Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}.
* <p/>
* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close()
* closed} when done.
*
* @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}.
* @param doFinally callback object that accepts {@link ClientSession} after invoking {@link SessionCallback}. This
* {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of
* {@link SessionCallback}).
* @param <T> return type.
* @return a result object returned by the action. Can be {@literal null}.
*/
@Nullable
<T> T execute(SessionCallback<T> action, Consumer<ClientSession> doFinally);
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.springframework.beans.factory.DisposableBean;
import com.mongodb.ClientSessionOptions;
import com.mongodb.ConnectionString;
import com.mongodb.DB;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
/**
* Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance.
*
* @author Christoph Strobl
* @since 2.1
*/
public class SimpleMongoClientDbFactory extends MongoDbFactorySupport<MongoClient> implements DisposableBean {
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance for the given {@code connectionString}.
*
* @param connectionString connection coordinates for a database connection. Must contain a database name and must not
* be {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/connection-string/">MongoDB Connection String reference</a>
*/
public SimpleMongoClientDbFactory(String connectionString) {
this(new ConnectionString(connectionString));
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param connectionString connection coordinates for a database connection. Must contain also a database name and not
* be {@literal null}.
*/
public SimpleMongoClientDbFactory(ConnectionString connectionString) {
this(MongoClients.create(connectionString), connectionString.getDatabase(), true);
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
*/
public SimpleMongoClientDbFactory(MongoClient mongoClient, String databaseName) {
this(mongoClient, databaseName, false);
}
/**
* Creates a new {@link SimpleMongoClientDbFactory} instance from the given {@link MongoClient}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @param mongoInstanceCreated
*/
private SimpleMongoClientDbFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) {
super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
@Override
public DB getLegacyDb() {
throw new UnsupportedOperationException(String.format(
"%s does not support legacy DBObject API! Please consider using SimpleMongoDbFactory for that purpose.",
MongoClient.class));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return getMongoClient().startSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#closeClient()
*/
@Override
protected void closeClient() {
getMongoClient().close();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#doGetMongoDatabase(java.lang.String)
*/
@Override
protected MongoDatabase doGetMongoDatabase(String dbName) {
return getMongoClient().getDatabase(dbName);
}
}

View File

@@ -15,43 +15,33 @@
*/
package org.springframework.data.mongodb.core;
import java.net.UnknownHostException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.DB;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
/**
* Factory to create {@link DB} instances from a {@link MongoClient} instance.
* Factory to create {@link MongoDatabase} instances from a {@link MongoClient} instance.
*
* @author Mark Pollack
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author George Moraitis
* @author Mark Paluch
*/
public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
private final MongoClient mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
private final PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
public class SimpleMongoDbFactory extends MongoDbFactorySupport<MongoClient> implements DisposableBean {
/**
* Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClientURI}.
*
* @param uri must not be {@literal null}.
* @throws UnknownHostException
* @param uri coordinates for a database connection. Must contain a database name and must not be {@literal null}.
* @since 1.7
*/
public SimpleMongoDbFactory(MongoClientURI uri) {
@@ -62,7 +52,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
* Creates a new {@link SimpleMongoDbFactory} instance from the given {@link MongoClient}.
*
* @param mongoClient must not be {@literal null}.
* @param databaseName must not be {@literal null}.
* @param databaseName must not be {@literal null} or empty.
* @since 1.7
*/
public SimpleMongoDbFactory(MongoClient mongoClient, String databaseName) {
@@ -70,81 +60,48 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory {
}
/**
* @param client
* @param mongoClient
* @param databaseName
* @param mongoInstanceCreated
* @since 1.7
*/
private SimpleMongoDbFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) {
Assert.notNull(mongoClient, "MongoClient must not be null!");
Assert.hasText(databaseName, "Database name must not be empty!");
Assert.isTrue(databaseName.matches("[\\w-]+"),
"Database name must only contain letters, numbers, underscores and dashes!");
this.mongoClient = mongoClient;
this.databaseName = databaseName;
this.mongoInstanceCreated = mongoInstanceCreated;
this.exceptionTranslator = new MongoExceptionTranslator();
}
/**
* Configures the {@link WriteConcern} to be used on the {@link DB} instance being created.
*
* @param writeConcern the writeConcern to set
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb()
* @see org.springframework.data.mongodb.MongoDbFactory#getLegacyDb()
*/
public MongoDatabase getDb() throws DataAccessException {
return getDb(databaseName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getDb(java.lang.String)
*/
public MongoDatabase getDb(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty.");
MongoDatabase db = mongoClient.getDatabase(dbName);
if (writeConcern == null) {
return db;
}
return db.withWriteConcern(writeConcern);
}
/**
* Clean up the Mongo instance if it was created by the factory itself.
*
* @see DisposableBean#destroy()
*/
public void destroy() throws Exception {
if (mongoInstanceCreated) {
mongoClient.close();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
@SuppressWarnings("deprecation")
@Override
public DB getLegacyDb() {
return mongoClient.getDB(databaseName);
return getMongoClient().getDB(getDefaultDatabaseName());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.MongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public ClientSession getSession(ClientSessionOptions options) {
return getMongoClient().startSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#closeClient()
*/
@Override
protected void closeClient() {
getMongoClient().close();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoDbFactoryBase#doGetMongoDatabase(java.lang.String)
*/
@Override
protected MongoDatabase doGetMongoDatabase(String dbName) {
return getMongoClient().getDatabase(dbName);
}
}

View File

@@ -15,19 +15,25 @@
*/
package org.springframework.data.mongodb.core;
import java.net.UnknownHostException;
import lombok.Value;
import reactor.core.publisher.Mono;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.SessionAwareMethodInterceptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.ClientSessionOptions;
import com.mongodb.ConnectionString;
import com.mongodb.WriteConcern;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
/**
@@ -51,9 +57,8 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
* Creates a new {@link SimpleReactiveMongoDatabaseFactory} instance from the given {@link ConnectionString}.
*
* @param connectionString must not be {@literal null}.
* @throws UnknownHostException
*/
public SimpleReactiveMongoDatabaseFactory(ConnectionString connectionString) throws UnknownHostException {
public SimpleReactiveMongoDatabaseFactory(ConnectionString connectionString) {
this(MongoClients.create(connectionString), connectionString.getDatabase(), true);
}
@@ -72,8 +77,8 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
Assert.notNull(client, "MongoClient must not be null!");
Assert.hasText(databaseName, "Database name must not be empty!");
Assert.isTrue(databaseName.matches("[\\w-]+"),
"Database name must only contain letters, numbers, underscores and dashes!");
Assert.isTrue(databaseName.matches("[^/\\\\.$\"\\s]+"),
"Database name must not contain slashes, dots, spaces, quotes, or dollar signs!");
this.mongo = client;
this.databaseName = databaseName;
@@ -129,4 +134,106 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
public PersistenceExceptionTranslator getExceptionTranslator() {
return this.exceptionTranslator;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDbFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public Mono<ClientSession> getSession(ClientSessionOptions options) {
return Mono.from(mongo.startSession(options));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDbFactory#withSession(com.mongodb.session.ClientSession)
*/
@Override
public ReactiveMongoDatabaseFactory withSession(ClientSession session) {
return new ClientSessionBoundMongoDbFactory(session, this);
}
/**
* {@link ClientSession} bound {@link ReactiveMongoDatabaseFactory} decorating the database with a
* {@link SessionAwareMethodInterceptor}.
*
* @author Christoph Strobl
* @since 2.1
*/
@Value
static class ClientSessionBoundMongoDbFactory implements ReactiveMongoDatabaseFactory {
ClientSession session;
ReactiveMongoDatabaseFactory delegate;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase()
*/
@Override
public MongoDatabase getMongoDatabase() throws DataAccessException {
return decorateDatabase(delegate.getMongoDatabase());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase(java.lang.String)
*/
@Override
public MongoDatabase getMongoDatabase(String dbName) throws DataAccessException {
return decorateDatabase(delegate.getMongoDatabase(dbName));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getExceptionTranslator()
*/
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return delegate.getExceptionTranslator();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getSession(com.mongodb.ClientSessionOptions)
*/
@Override
public Mono<ClientSession> getSession(ClientSessionOptions options) {
return delegate.getSession(options);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession)
*/
@Override
public ReactiveMongoDatabaseFactory withSession(ClientSession session) {
return delegate.withSession(session);
}
private MongoDatabase decorateDatabase(MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection proxyCollection(com.mongodb.session.ClientSession session, MongoCollection collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.setInterfaces(targetType);
factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class,
this::proxyDatabase, MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016. the original author or authors.
* Copyright 2016-2018. the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,12 +20,15 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @author Matt Morrissette
* @since 1.10
*/
abstract class AbstractAggregationExpression implements AggregationExpression {
@@ -46,29 +49,7 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
@SuppressWarnings("unchecked")
public Document toDocument(Object value, AggregationOperationContext context) {
Object valueToUse;
if (value instanceof List) {
List<Object> arguments = (List<Object>) value;
List<Object> args = new ArrayList<Object>(arguments.size());
for (Object val : arguments) {
args.add(unpack(val, context));
}
valueToUse = args;
} else if (value instanceof java.util.Map) {
Document dbo = new Document();
for (java.util.Map.Entry<String, Object> entry : ((java.util.Map<String, Object>) value).entrySet()) {
dbo.put(entry.getKey(), unpack(entry.getValue(), context));
}
valueToUse = dbo;
} else {
valueToUse = unpack(value, context);
}
return new Document(getMongoMethod(), valueToUse);
return new Document(getMongoMethod(), unpack(value, context));
}
protected static List<Field> asFields(String... fieldRefs) {
@@ -94,14 +75,23 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
if (value instanceof List) {
List<Object> sourceList = (List<Object>) value;
List<Object> mappedList = new ArrayList<Object>(sourceList.size());
List<Object> mappedList = new ArrayList<>(sourceList.size());
sourceList.stream().map((item) -> unpack(item, context)).forEach(mappedList::add);
for (Object item : sourceList) {
mappedList.add(unpack(item, context));
}
return mappedList;
}
if (value instanceof Map) {
Document targetDocument = new Document();
Map<String, Object> sourceMap = (Map<String, Object>) value;
sourceMap.forEach((k, v) -> targetDocument.append(k, unpack(v, context)));
return targetDocument;
}
return value;
}
@@ -112,9 +102,7 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
List<Object> clone = new ArrayList<Object>((List) this.value);
if (value instanceof List) {
for (Object val : (List) value) {
clone.add(val);
}
clone.addAll((List) value);
} else {
clone.add(value);
}
@@ -127,10 +115,9 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
@SuppressWarnings("unchecked")
protected java.util.Map<String, Object> append(String key, Object value) {
if (!(this.value instanceof java.util.Map)) {
throw new IllegalArgumentException("o_O");
}
java.util.Map<String, Object> clone = new LinkedHashMap<String, Object>((java.util.Map<String, Object>) this.value);
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
java.util.Map<String, Object> clone = new LinkedHashMap<>((java.util.Map) this.value);
clone.put(key, value);
return clone;
@@ -144,7 +131,67 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
if (value instanceof java.util.Map) {
return new ArrayList<Object>(((java.util.Map) value).values());
}
return new ArrayList<Object>(Collections.singletonList(value));
return new ArrayList<>(Collections.singletonList(value));
}
/**
* Get the value at a given index.
*
* @param index
* @param <T>
* @return
* @since 2.1
*/
@SuppressWarnings("unchecked")
protected <T> T get(int index) {
return (T) values().get(index);
}
/**
* Get the value for a given key.
*
* @param key
* @param <T>
* @return
* @since 2.1
*/
@SuppressWarnings("unchecked")
protected <T> T get(Object key) {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
return (T) ((java.util.Map<String, Object>) this.value).get(key);
}
/**
* Get the argument map.
*
* @since 2.1
* @return
*/
@SuppressWarnings("unchecked")
protected java.util.Map<String, Object> argumentMap() {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
return Collections.unmodifiableMap((java.util.Map) value);
}
/**
* Check if the given key is available.
*
* @param key
* @return
* @since 2.1
*/
@SuppressWarnings("unchecked")
protected boolean contains(Object key) {
if (!(this.value instanceof java.util.Map)) {
return false;
}
return ((java.util.Map<String, Object>) this.value).containsKey(key);
}
protected abstract String getMongoMethod();

View File

@@ -64,6 +64,33 @@ public class Aggregation {
*/
public static final String CURRENT = SystemVariable.CURRENT.toString();
/**
* A variable to conditionally exclude a field. In a {@code $projection}, a field set to the variable
* {@literal REMOVE} is excluded from the output.
*
* <pre>
* <code>
*
* db.books.aggregate( [
* {
* $project: {
* title: 1,
* "author.first": 1,
* "author.last" : 1,
* "author.middle": {
* $cond: {
* if: { $eq: [ "", "$author.middle" ] },
* then: "$$REMOVE",
* else: "$author.middle"
* }
* }
* }
* } ] )
* </code>
* </pre>
*/
public static final String REMOVE = SystemVariable.REMOVE.toString();
public static final AggregationOperationContext DEFAULT_CONTEXT = AggregationOperationRenderer.DEFAULT_CONTEXT;
public static final AggregationOptions DEFAULT_OPTIONS = newAggregationOptions().build();
@@ -648,11 +675,12 @@ public class Aggregation {
* Describes the system variables available in MongoDB aggregation framework pipeline expressions.
*
* @author Thomas Darimont
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation-variables">Aggregation Variables</a>
* @author Christoph Strobl
* @see <a href="https://docs.mongodb.com/manual/reference/aggregation-variables">Aggregation Variables</a>.
*/
enum SystemVariable {
ROOT, CURRENT;
ROOT, CURRENT, REMOVE;
private static final String PREFIX = "$$";

View File

@@ -59,7 +59,7 @@ class AggregationOperationRenderer {
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
ExposedFields fields = exposedFieldsOperation.getFields();
if (operation instanceof InheritsFieldsAggregationOperation) {
if (operation instanceof InheritsFieldsAggregationOperation || exposedFieldsOperation.inheritsFields()) {
contextToUse = new InheritingExposedFieldsAggregationOperationContext(fields, contextToUse);
} else {
contextToUse = fields.exposesNoFields() ? DEFAULT_CONTEXT

View File

@@ -33,9 +33,26 @@ public interface FieldsExposingAggregationOperation extends AggregationOperation
*/
ExposedFields getFields();
/**
* @return {@literal true} to conditionally inherit fields from previous operations.
* @since 2.0.6
*/
default boolean inheritsFields() {
return false;
}
/**
* Marker interface for {@link AggregationOperation} that inherits fields from previous operations.
*/
interface InheritsFieldsAggregationOperation extends FieldsExposingAggregationOperation {}
interface InheritsFieldsAggregationOperation extends FieldsExposingAggregationOperation {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#inheritsFields()
*/
@Override
default boolean inheritsFields() {
return true;
}
}
}

View File

@@ -140,11 +140,6 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
*/
public ProjectionOperation andExclude(String... fieldNames) {
for (String fieldName : fieldNames) {
Assert.isTrue(Fields.UNDERSCORE_ID.equals(fieldName),
String.format(EXCLUSION_ERROR, fieldName, Fields.UNDERSCORE_ID));
}
List<FieldProjection> excludeProjections = FieldProjection.from(Fields.fields(fieldNames), false);
return new ProjectionOperation(this.projections, excludeProjections);
}
@@ -188,6 +183,18 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return fields != null ? fields : ExposedFields.empty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#inheritsFields()
*/
@Override
public boolean inheritsFields() {
return projections.stream().filter(FieldProjection.class::isInstance) //
.map(FieldProjection.class::cast) //
.anyMatch(FieldProjection::isExcluded);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
@@ -455,7 +462,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
}
if (value instanceof AggregationExpression) {
return this.operation.and(new ExpressionProjection(Fields.field(alias), (AggregationExpression) value));
return this.operation.and(new ExpressionProjection(Fields.field(alias, alias), (AggregationExpression) value));
}
return this.operation.and(new FieldProjection(Fields.field(alias, getRequiredName()), null));
@@ -1344,6 +1351,13 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return projections;
}
/**
* @return {@literal true} if this field is excluded.
*/
public boolean isExcluded() {
return Boolean.FALSE.equals(value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)

View File

@@ -18,12 +18,12 @@ package org.springframework.data.mongodb.core.aggregation;
import static org.springframework.data.mongodb.core.aggregation.Fields.*;
import org.bson.Document;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -95,8 +95,8 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
private FieldReference getReferenceFor(Field field) {
PersistentPropertyPath<MongoPersistentProperty> propertyPath = mappingContext.getPersistentPropertyPath(
field.getTarget(), type);
PersistentPropertyPath<MongoPersistentProperty> propertyPath = mappingContext
.getPersistentPropertyPath(field.getTarget(), type);
Field mappedField = field(field.getName(),
propertyPath.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE));

View File

@@ -47,7 +47,7 @@ public interface DbRefResolver {
* @return
*/
@Nullable
Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler proxyHandler);
/**
@@ -59,7 +59,8 @@ public interface DbRefResolver {
* @param id will never be {@literal null}.
* @return
*/
DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation, MongoPersistentEntity<?> entity,
DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity,
Object id);
/**

View File

@@ -40,6 +40,7 @@ import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.LazyLoadingException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -51,7 +52,7 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.DBRef;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
/**
@@ -89,7 +90,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolveDbRef(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolverCallback)
*/
@Override
public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback,
public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback,
DbRefProxyHandler handler) {
Assert.notNull(property, "Property must not be null!");
@@ -108,7 +109,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
* @see org.springframework.data.mongodb.core.convert.DbRefResolver#created(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.Object)
*/
@Override
public DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation,
public DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity, Object id) {
if (annotation != null && StringUtils.hasText(annotation.db())) {
@@ -126,9 +127,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
public Document fetch(DBRef dbRef) {
StringUtils.hasText(dbRef.getDatabaseName());
return (StringUtils.hasText(dbRef.getDatabaseName()) ? mongoDbFactory.getDb(dbRef.getDatabaseName())
: mongoDbFactory.getDb()).getCollection(dbRef.getCollectionName(), Document.class)
.find(Filters.eq("_id", dbRef.getId())).first();
return getCollection(dbRef).find(Filters.eq("_id", dbRef.getId())).first();
}
/*
@@ -158,9 +157,7 @@ public class DefaultDbRefResolver implements DbRefResolver {
ids.add(ref.getId());
}
MongoDatabase db = mongoDbFactory.getDb();
List<Document> result = db.getCollection(collection) //
List<Document> result = getCollection(refs.iterator().next()) //
.find(new Document("_id", new Document("$in", ids))) //
.into(new ArrayList<>());
@@ -466,6 +463,11 @@ public class DefaultDbRefResolver implements DbRefResolver {
} catch (RuntimeException ex) {
DataAccessException translatedException = this.exceptionTranslator.translateExceptionIfPossible(ex);
if (translatedException instanceof ClientSessionException) {
throw new LazyLoadingException("Unable to lazily resolve DBRef! Invalid session state.", ex);
}
throw new LazyLoadingException("Unable to lazily resolve DBRef!",
translatedException != null ? translatedException : ex);
}
@@ -474,4 +476,17 @@ public class DefaultDbRefResolver implements DbRefResolver {
return result;
}
}
/**
* Customization hook for obtaining the {@link MongoCollection} for a given {@link DBRef}.
*
* @param dbref must not be {@literal null}.
* @return the {@link MongoCollection} the given {@link DBRef} points to.
* @since 2.1
*/
protected MongoCollection<Document> getCollection(DBRef dbref) {
return (StringUtils.hasText(dbref.getDatabaseName()) ? mongoDbFactory.getDb(dbref.getDatabaseName())
: mongoDbFactory.getDb()).getCollection(dbref.getCollectionName(), Document.class);
}
}

View File

@@ -15,10 +15,13 @@
*/
package org.springframework.data.mongodb.core.convert;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
@@ -41,11 +44,13 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.query.GeoCommand;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.BasicDBList;
import com.mongodb.Function;
/**
* Wrapper class to contain useful geo structure converters for the usage with Mongo.
@@ -58,6 +63,26 @@ import com.mongodb.BasicDBList;
*/
abstract class GeoConverters {
private final static Map<String, Function<Document, GeoJson<?>>> converters;
static {
Collator caseInsensitive = Collator.getInstance();
caseInsensitive.setStrength(Collator.PRIMARY);
Map<String, Function<Document, GeoJson<?>>> geoConverters = new TreeMap<>(caseInsensitive);
geoConverters.put("point", DocumentToGeoJsonPointConverter.INSTANCE::convert);
geoConverters.put("multipoint", DocumentToGeoJsonMultiPointConverter.INSTANCE::convert);
geoConverters.put("linestring", DocumentToGeoJsonLineStringConverter.INSTANCE::convert);
geoConverters.put("multilinestring", DocumentToGeoJsonMultiLineStringConverter.INSTANCE::convert);
geoConverters.put("polygon", DocumentToGeoJsonPolygonConverter.INSTANCE::convert);
geoConverters.put("multipolygon", DocumentToGeoJsonMultiPolygonConverter.INSTANCE::convert);
geoConverters.put("geometrycollection", DocumentToGeoJsonGeometryCollectionConverter.INSTANCE::convert);
converters = geoConverters;
}
/**
* Private constructor to prevent instantiation.
*/
@@ -91,7 +116,8 @@ abstract class GeoConverters {
, DocumentToGeoJsonMultiLineStringConverter.INSTANCE //
, DocumentToGeoJsonMultiPointConverter.INSTANCE //
, DocumentToGeoJsonMultiPolygonConverter.INSTANCE //
, DocumentToGeoJsonGeometryCollectionConverter.INSTANCE);
, DocumentToGeoJsonGeometryCollectionConverter.INSTANCE //
, DocumentToGeoJsonConverter.INSTANCE);
}
/**
@@ -101,7 +127,7 @@ abstract class GeoConverters {
* @since 1.5
*/
@ReadingConverter
static enum DocumentToPointConverter implements Converter<Document, Point> {
enum DocumentToPointConverter implements Converter<Document, Point> {
INSTANCE;
@@ -132,7 +158,7 @@ abstract class GeoConverters {
* @author Thomas Darimont
* @since 1.5
*/
static enum PointToDocumentConverter implements Converter<Point, Document> {
enum PointToDocumentConverter implements Converter<Point, Document> {
INSTANCE;
@@ -147,13 +173,13 @@ abstract class GeoConverters {
}
/**
* Converts a {@link Box} into a {@link BasicDBList}.
* Converts a {@link Box} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
@WritingConverter
static enum BoxToDocumentConverter implements Converter<Box, Document> {
enum BoxToDocumentConverter implements Converter<Box, Document> {
INSTANCE;
@@ -176,13 +202,13 @@ abstract class GeoConverters {
}
/**
* Converts a {@link BasicDBList} into a {@link Box}.
* Converts a {@link Document} into a {@link Box}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
static enum DocumentToBoxConverter implements Converter<Document, Box> {
enum DocumentToBoxConverter implements Converter<Document, Box> {
INSTANCE;
@@ -205,12 +231,12 @@ abstract class GeoConverters {
}
/**
* Converts a {@link Circle} into a {@link BasicDBList}.
* Converts a {@link Circle} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
static enum CircleToDocumentConverter implements Converter<Circle, Document> {
enum CircleToDocumentConverter implements Converter<Circle, Document> {
INSTANCE;
@@ -276,7 +302,7 @@ abstract class GeoConverters {
}
/**
* Converts a {@link Sphere} into a {@link BasicDBList}.
* Converts a {@link Sphere} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
@@ -305,13 +331,13 @@ abstract class GeoConverters {
}
/**
* Converts a {@link BasicDBList} into a {@link Sphere}.
* Converts a {@link Document} into a {@link Sphere}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
static enum DocumentToSphereConverter implements Converter<Document, Sphere> {
enum DocumentToSphereConverter implements Converter<Document, Sphere> {
INSTANCE;
@@ -347,12 +373,12 @@ abstract class GeoConverters {
}
/**
* Converts a {@link Polygon} into a {@link BasicDBList}.
* Converts a {@link Polygon} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
static enum PolygonToDocumentConverter implements Converter<Polygon, Document> {
enum PolygonToDocumentConverter implements Converter<Polygon, Document> {
INSTANCE;
@@ -368,7 +394,7 @@ abstract class GeoConverters {
}
List<Point> points = source.getPoints();
List<Document> pointTuples = new ArrayList<Document>(points.size());
List<Document> pointTuples = new ArrayList<>(points.size());
for (Point point : points) {
pointTuples.add(PointToDocumentConverter.INSTANCE.convert(point));
@@ -381,13 +407,13 @@ abstract class GeoConverters {
}
/**
* Converts a {@link BasicDBList} into a {@link Polygon}.
* Converts a {@link Document} into a {@link Polygon}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
static enum DocumentToPolygonConverter implements Converter<Document, Polygon> {
enum DocumentToPolygonConverter implements Converter<Document, Polygon> {
INSTANCE;
@@ -404,7 +430,7 @@ abstract class GeoConverters {
}
List<Document> points = (List<Document>) source.get("points");
List<Point> newPoints = new ArrayList<Point>(points.size());
List<Point> newPoints = new ArrayList<>(points.size());
for (Document element : points) {
@@ -417,12 +443,12 @@ abstract class GeoConverters {
}
/**
* Converts a {@link Sphere} into a {@link BasicDBList}.
* Converts a {@link Sphere} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
static enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> {
enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> {
INSTANCE;
@@ -482,7 +508,7 @@ abstract class GeoConverters {
* @since 1.7
*/
@SuppressWarnings("rawtypes")
static enum GeoJsonToDocumentConverter implements Converter<GeoJson, Document> {
enum GeoJsonToDocumentConverter implements Converter<GeoJson, Document> {
INSTANCE;
@@ -545,7 +571,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum GeoJsonPointToDocumentConverter implements Converter<GeoJsonPoint, Document> {
enum GeoJsonPointToDocumentConverter implements Converter<GeoJsonPoint, Document> {
INSTANCE;
@@ -563,7 +589,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum GeoJsonPolygonToDocumentConverter implements Converter<GeoJsonPolygon, Document> {
enum GeoJsonPolygonToDocumentConverter implements Converter<GeoJsonPolygon, Document> {
INSTANCE;
@@ -581,7 +607,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonPointConverter implements Converter<Document, GeoJsonPoint> {
enum DocumentToGeoJsonPointConverter implements Converter<Document, GeoJsonPoint> {
INSTANCE;
@@ -609,7 +635,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonPolygonConverter implements Converter<Document, GeoJsonPolygon> {
enum DocumentToGeoJsonPolygonConverter implements Converter<Document, GeoJsonPolygon> {
INSTANCE;
@@ -654,7 +680,7 @@ abstract class GeoConverters {
String.format("Cannot convert type '%s' to MultiPolygon.", source.get("type")));
List dbl = (List) source.get("coordinates");
List<GeoJsonPolygon> polygones = new ArrayList<GeoJsonPolygon>();
List<GeoJsonPolygon> polygones = new ArrayList<>();
for (Object polygon : dbl) {
polygones.add(toGeoJsonPolygon((List) polygon));
@@ -668,7 +694,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonLineStringConverter implements Converter<Document, GeoJsonLineString> {
enum DocumentToGeoJsonLineStringConverter implements Converter<Document, GeoJsonLineString> {
INSTANCE;
@@ -696,7 +722,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, GeoJsonMultiPoint> {
enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, GeoJsonMultiPoint> {
INSTANCE;
@@ -724,7 +750,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, GeoJsonMultiLineString> {
enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, GeoJsonMultiLineString> {
INSTANCE;
@@ -756,7 +782,7 @@ abstract class GeoConverters {
* @author Christoph Strobl
* @since 1.7
*/
static enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document, GeoJsonGeometryCollection> {
enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document, GeoJsonGeometryCollection> {
INSTANCE;
@@ -775,41 +801,12 @@ abstract class GeoConverters {
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"),
String.format("Cannot convert type '%s' to GeometryCollection.", source.get("type")));
List<GeoJson<?>> geometries = new ArrayList<GeoJson<?>>();
List<GeoJson<?>> geometries = new ArrayList<>();
for (Object o : (List) source.get("geometries")) {
geometries.add(convertGeometries((Document) o));
geometries.add(toGenericGeoJson((Document) o));
}
return new GeoJsonGeometryCollection(geometries);
}
private static GeoJson<?> convertGeometries(Document source) {
Object type = source.get("type");
if (ObjectUtils.nullSafeEquals(type, "Point")) {
return DocumentToGeoJsonPointConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiPoint")) {
return DocumentToGeoJsonMultiPointConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "LineString")) {
return DocumentToGeoJsonLineStringConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiLineString")) {
return DocumentToGeoJsonMultiLineStringConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "Polygon")) {
return DocumentToGeoJsonPolygonConverter.INSTANCE.convert(source);
}
if (ObjectUtils.nullSafeEquals(type, "MultiPolygon")) {
return DocumentToGeoJsonMultiPolygonConverter.INSTANCE.convert(source);
}
throw new IllegalArgumentException(String.format("Cannot convert unknown GeoJson type %s", type));
}
}
@@ -827,7 +824,7 @@ abstract class GeoConverters {
@SuppressWarnings("unchecked")
static List<Point> toListOfPoint(List listOfCoordinatePairs) {
List<Point> points = new ArrayList<Point>();
List<Point> points = new ArrayList<>();
for (Object point : listOfCoordinatePairs) {
@@ -852,6 +849,46 @@ abstract class GeoConverters {
return new GeoJsonPolygon(toListOfPoint((List) dbList.get(0)));
}
/**
* Converter implementation transforming a {@link Document} into a concrete {@link GeoJson} based on the embedded
* {@literal type} information.
*
* @since 2.1
* @author Christoph Strobl
*/
@ReadingConverter
enum DocumentToGeoJsonConverter implements Converter<Document, GeoJson> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
public GeoJson convert(Document source) {
return toGenericGeoJson(source);
}
}
private static GeoJson<?> toGenericGeoJson(Document source) {
String type = source.get("type", String.class);
if(type != null) {
Function<Document, GeoJson<?>> converter = converters.get(type);
if(converter != null){
return converter.apply(source);
}
}
throw new IllegalArgumentException(
String.format("No converter found capable of converting GeoJson type %s.", type));
}
private static double toPrimitiveDoubleValue(Object value) {
Assert.isInstanceOf(Number.class, value, "Argument must be a Number.");

View File

@@ -147,8 +147,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) {
this.typeMapper = typeMapper == null
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext)
: typeMapper;
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper;
}
/*
@@ -264,7 +263,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
path);
}
@Nullable
private <S extends Object> S read(final MongoPersistentEntity<S> entity, final Document bson, final ObjectPath path) {
DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
@@ -316,7 +314,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
for (MongoPersistentProperty prop : entity) {
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
readAssociation(prop.getAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
continue;
}
// we skip the id property since it was already set
@@ -329,7 +327,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (prop.isAssociation()) {
readAssociation(prop.getAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
readAssociation(prop.getRequiredAssociation(), accessor, documentAccessor, dbRefProxyHandler, callback);
continue;
}
@@ -357,7 +355,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) {
org.springframework.data.mongodb.core.mapping.DBRef annotation = null;
org.springframework.data.mongodb.core.mapping.DBRef annotation;
if (referringProperty != null) {
annotation = referringProperty.getDBRef();
@@ -378,7 +376,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*
* @see org.springframework.data.mongodb.core.convert.MongoWriter#write(java.lang.Object, com.mongodb.Document)
*/
public void write(final Object obj, final Bson bson) {
public void write(Object obj, Bson bson) {
if (null == obj) {
return;
@@ -405,9 +403,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*
* @param obj
* @param bson
* @param typeHint
*/
@SuppressWarnings("unchecked")
protected void writeInternal(@Nullable Object obj, final Bson bson, final TypeInformation<?> typeHint) {
protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation<?> typeHint) {
if (null == obj) {
return;
@@ -428,7 +427,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (Collection.class.isAssignableFrom(entityType)) {
writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (BasicDBList) bson);
writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection) bson);
return;
}
@@ -437,7 +436,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
addCustomTypeKeyIfNecessary(typeHint, obj, bson);
}
protected void writeInternal(@Nullable Object obj, final Bson bson, MongoPersistentEntity<?> entity) {
protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable MongoPersistentEntity<?> entity) {
if (obj == null) {
return;
@@ -463,7 +462,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
private void writeProperties(Bson bson, MongoPersistentEntity<?> entity, PersistentPropertyAccessor accessor,
DocumentAccessor dbObjectAccessor, MongoPersistentProperty idProperty) {
DocumentAccessor dbObjectAccessor, @Nullable MongoPersistentProperty idProperty) {
// Write the properties
for (MongoPersistentProperty prop : entity) {
@@ -472,7 +471,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
continue;
}
if (prop.isAssociation()) {
writeAssociation(prop.getAssociation(), accessor, dbObjectAccessor);
writeAssociation(prop.getRequiredAssociation(), accessor, dbObjectAccessor);
continue;
}
@@ -499,7 +498,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
@SuppressWarnings({ "unchecked" })
protected void writePropertyInternal(Object obj, DocumentAccessor accessor, MongoPersistentProperty prop) {
protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop) {
if (obj == null) {
return;
@@ -557,8 +556,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass())
? mappingContext.getRequiredPersistentEntity(obj.getClass())
: mappingContext.getRequiredPersistentEntity(type);
? mappingContext.getRequiredPersistentEntity(obj.getClass()) : mappingContext.getRequiredPersistentEntity(type);
Object existingValue = accessor.get(prop);
Document document = existingValue instanceof Document ? (Document) existingValue : new Document();
@@ -654,17 +652,19 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
/**
* Populates the given {@link BasicDBList} with values from the given {@link Collection}.
* Populates the given {@link Collection sink} with converted values from the given {@link Collection source}.
*
* @param source the collection to create a {@link BasicDBList} for, must not be {@literal null}.
* @param source the collection to create a {@link Collection} for, must not be {@literal null}.
* @param type the {@link TypeInformation} to consider or {@literal null} if unknown.
* @param sink the {@link BasicDBList} to write to.
* @param sink the {@link Collection} to write to.
* @return
*/
private BasicDBList writeCollectionInternal(Collection<?> source, TypeInformation<?> type, BasicDBList sink) {
private List<Object> writeCollectionInternal(Collection<?> source, @Nullable TypeInformation<?> type, Collection<?> sink) {
TypeInformation<?> componentType = null;
List<Object> collection = sink instanceof List ? (List) sink : new ArrayList<>(sink);
if (type != null) {
componentType = type.getComponentType();
}
@@ -674,17 +674,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Class<?> elementType = element == null ? null : element.getClass();
if (elementType == null || conversions.isSimpleType(elementType)) {
sink.add(getPotentiallyConvertedSimpleWrite(element));
collection.add(getPotentiallyConvertedSimpleWrite(element));
} else if (element instanceof Collection || elementType.isArray()) {
sink.add(writeCollectionInternal(asCollection(element), componentType, new BasicDBList()));
collection.add(writeCollectionInternal(asCollection(element), componentType, new BasicDBList()));
} else {
Document document = new Document();
writeInternal(element, document, componentType);
sink.add(document);
collection.add(document);
}
}
return sink;
return collection;
}
/**
@@ -777,8 +777,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
return conversions.hasCustomWriteTarget(key.getClass(), String.class)
? (String) getPotentiallyConvertedSimpleWrite(key)
: key.toString();
? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString();
}
/**
@@ -868,9 +867,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
@Nullable
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, Class<?> target) {
private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class<?> target) {
if (value == null || target == null || target.isAssignableFrom(value.getClass())) {
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
return value;
}
@@ -933,58 +932,62 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* Reads the given {@link BasicDBList} into a collection of the given {@link TypeInformation}.
*
* @param targetType must not be {@literal null}.
* @param sourceValue must not be {@literal null}.
* @param source must not be {@literal null}.
* @param path must not be {@literal null}.
* @return the converted {@link Collection} or array, will never be {@literal null}.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object readCollectionOrArray(TypeInformation<?> targetType, List sourceValue, ObjectPath path) {
@SuppressWarnings("unchecked")
private Object readCollectionOrArray(TypeInformation<?> targetType, Collection<?> source, ObjectPath path) {
Assert.notNull(targetType, "Target type must not be null!");
Assert.notNull(path, "Object path must not be null!");
Class<?> collectionType = targetType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) //
? collectionType //
: List.class;
TypeInformation<?> componentType = targetType.getComponentType() != null ? targetType.getComponentType()
TypeInformation<?> componentType = targetType.getComponentType() != null //
? targetType.getComponentType() //
: ClassTypeInformation.OBJECT;
Class<?> rawComponentType = componentType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class;
Collection<Object> items = targetType.getType().isArray() ? new ArrayList<>(sourceValue.size())
: CollectionFactory.createCollection(collectionType, rawComponentType, sourceValue.size());
Collection<Object> items = targetType.getType().isArray() //
? new ArrayList<>(source.size()) //
: CollectionFactory.createCollection(collectionType, rawComponentType, source.size());
if (sourceValue.isEmpty()) {
if (source.isEmpty()) {
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
}
if (!DBRef.class.equals(rawComponentType) && isCollectionOfDbRefWhereBulkFetchIsPossible(sourceValue)) {
if (!DBRef.class.equals(rawComponentType) && isCollectionOfDbRefWhereBulkFetchIsPossible(source)) {
List<Object> objects = bulkReadAndConvertDBRefs((List<DBRef>) sourceValue, componentType, path, rawComponentType);
List<Object> objects = bulkReadAndConvertDBRefs((List<DBRef>) source, componentType, path, rawComponentType);
return getPotentiallyConvertedSimpleRead(objects, targetType.getType());
}
for (Object dbObjItem : sourceValue) {
for (Object element : source) {
if (dbObjItem instanceof DBRef) {
items.add(DBRef.class.equals(rawComponentType) ? dbObjItem
: readAndConvertDBRef((DBRef) dbObjItem, componentType, path, rawComponentType));
} else if (dbObjItem instanceof Document) {
items.add(read(componentType, (Document) dbObjItem, path));
} else if (dbObjItem instanceof BasicDBObject) {
items.add(read(componentType, (BasicDBObject) dbObjItem, path));
if (element instanceof DBRef) {
items.add(DBRef.class.equals(rawComponentType) ? element
: readAndConvertDBRef((DBRef) element, componentType, path, rawComponentType));
} else if (element instanceof Document) {
items.add(read(componentType, (Document) element, path));
} else if (element instanceof BasicDBObject) {
items.add(read(componentType, (BasicDBObject) element, path));
} else {
if (dbObjItem instanceof Collection) {
if (element instanceof Collection) {
if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) {
throw new MappingException(
String.format(INCOMPATIBLE_TYPES, dbObjItem, dbObjItem.getClass(), rawComponentType, path));
String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType, path));
}
}
if (dbObjItem instanceof List) {
items.add(readCollectionOrArray(ClassTypeInformation.OBJECT, (List) dbObjItem, path));
if (element instanceof List) {
items.add(readCollectionOrArray(componentType, (Collection<Object>) element, path));
} else {
items.add(getPotentiallyConvertedSimpleRead(dbObjItem, rawComponentType));
items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType));
}
}
}
@@ -1045,8 +1048,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
map.put(key, DBRef.class.equals(rawValueType) ? value
: readAndConvertDBRef((DBRef) value, defaultedValueType, ObjectPath.ROOT, rawValueType));
} else if (value instanceof List) {
map.put(key,
readCollectionOrArray(valueType != null ? valueType : ClassTypeInformation.LIST, (List) value, path));
map.put(key, readCollectionOrArray(valueType != null ? valueType : ClassTypeInformation.LIST,
(List<Object>) value, path));
} else {
map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType));
}
@@ -1084,8 +1087,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
"Cannot add key/value pair to %s. as map. Given Bson must be a Document or DBObject!", bson.getClass()));
}
@SuppressWarnings("unchecked")
private static void addAllToMap(Bson bson, Map value) {
private static void addAllToMap(Bson bson, Map<String, ?> value) {
if (bson instanceof Document) {
((Document) bson).putAll(value);
@@ -1140,10 +1142,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return getPotentiallyConvertedSimpleWrite(obj);
}
TypeInformation<?> typeHint = typeInformation;
if (obj instanceof List) {
return maybeConvertList((List<Object>) obj, typeHint);
return maybeConvertList((List<Object>) obj, typeInformation);
}
if (obj instanceof Document) {
@@ -1151,7 +1151,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Document newValueDocument = new Document();
for (String vk : ((Document) obj).keySet()) {
Object o = ((Document) obj).get(vk);
newValueDocument.put(vk, convertToMongoType(o, typeHint));
newValueDocument.put(vk, convertToMongoType(o, typeInformation));
}
return newValueDocument;
}
@@ -1162,7 +1162,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
for (String vk : ((DBObject) obj).keySet()) {
Object o = ((DBObject) obj).get(vk);
newValueDbo.put(vk, convertToMongoType(o, typeHint));
newValueDbo.put(vk, convertToMongoType(o, typeInformation));
}
return newValueDbo;
@@ -1173,18 +1173,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Document result = new Document();
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) obj).entrySet()) {
result.put(entry.getKey().toString(), convertToMongoType(entry.getValue(), typeHint));
result.put(entry.getKey().toString(), convertToMongoType(entry.getValue(), typeInformation));
}
return result;
}
if (obj.getClass().isArray()) {
return maybeConvertList(Arrays.asList((Object[]) obj), typeHint);
return maybeConvertList(Arrays.asList((Object[]) obj), typeInformation);
}
if (obj instanceof Collection) {
return maybeConvertList((Collection<?>) obj, typeHint);
return maybeConvertList((Collection<?>) obj, typeInformation);
}
Document newDocument = new Document();
@@ -1219,6 +1219,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param recursively whether to apply the removal recursively
* @return
*/
@SuppressWarnings("unchecked")
private Object removeTypeInfo(Object object, boolean recursively) {
if (!(object instanceof Document)) {
@@ -1239,7 +1240,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
removeTypeInfo(element, recursively);
}
} else if (value instanceof List) {
for (Object element : (List) value) {
for (Object element : (List<Object>) value) {
removeTypeInfo(element, recursively);
}
} else {
@@ -1379,7 +1380,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} else if (value instanceof DBRef) {
return potentiallyReadOrResolveDbRef((DBRef) value, type, path, rawType);
} else if (value instanceof List) {
return (T) readCollectionOrArray(type, (List) value, path);
return (T) readCollectionOrArray(type, (List<Object>) value, path);
} else if (value instanceof Document) {
return (T) read(type, (Document) value, path);
} else if (value instanceof DBObject) {
@@ -1434,8 +1435,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
List<Document> referencedRawDocuments = dbrefs.size() == 1
? Collections.singletonList(readRef(dbrefs.iterator().next()))
: bulkReadRefs(dbrefs);
? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs);
String collectionName = dbrefs.iterator().next().getCollectionName();
List<T> targeList = new ArrayList<>(dbrefs.size());
@@ -1495,7 +1495,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param source must not be {@literal null}.
* @return
*/
private static boolean isCollectionOfDbRefWhereBulkFetchIsPossible(Iterable<Object> source) {
private static boolean isCollectionOfDbRefWhereBulkFetchIsPossible(Iterable<?> source) {
Assert.notNull(source, "Iterable of DBRefs must not be null!");

View File

@@ -69,7 +69,6 @@ public interface MongoConverter
Assert.notNull(targetType, "TargetType must not be null!");
Assert.notNull(dbRefResolver, "DbRefResolver must not be null!");
if (targetType != Object.class && ClassUtils.isAssignable(targetType, source.getClass())) {
return (T) source;
}

View File

@@ -35,11 +35,11 @@ import org.springframework.data.domain.Example;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;

View File

@@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
/**
* TODO: Revisit for a better pattern.
* Provider interface to obtain {@link IndexOperations} by MongoDB collection name.
*
* @author Mark Paluch
* @author Jens Schauder
* @since 2.0
*/
@FunctionalInterface
public interface IndexOperationsProvider {
/**
* Returns the operations that can be performed on indexes
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @return index operations on the named collection
*/
IndexOperations indexOps(String collectionName);

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.index;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -38,7 +39,20 @@ import org.springframework.util.Assert;
*/
public class MongoMappingEventPublisher implements ApplicationEventPublisher {
private final MongoPersistentEntityIndexCreator indexCreator;
private final ApplicationListener<MappingContextEvent<?, ?>> indexCreator;
/**
* Creates a new {@link MongoMappingEventPublisher} for the given {@link ApplicationListener}.
*
* @param indexCreator must not be {@literal null}.
* @since 2.1
*/
public MongoMappingEventPublisher(ApplicationListener<MappingContextEvent<?, ?>> indexCreator) {
Assert.notNull(indexCreator, "ApplicationListener must not be null!");
this.indexCreator = indexCreator;
}
/**
* Creates a new {@link MongoMappingEventPublisher} for the given {@link MongoPersistentEntityIndexCreator}.
@@ -48,6 +62,7 @@ public class MongoMappingEventPublisher implements ApplicationEventPublisher {
public MongoMappingEventPublisher(MongoPersistentEntityIndexCreator indexCreator) {
Assert.notNull(indexCreator, "MongoPersistentEntityIndexCreator must not be null!");
this.indexCreator = indexCreator;
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
/**
* Provider interface to obtain {@link ReactiveIndexOperations} by MongoDB collection name.
*
* @author Mark Paluch
* @since 2.1
*/
@FunctionalInterface
public interface ReactiveIndexOperationsProvider {
/**
* Returns the operations that can be performed on indexes.
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @return index operations on the named collection
*/
ReactiveIndexOperations indexOps(String collectionName);
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import com.mongodb.MongoException;
/**
* Component that inspects {@link MongoPersistentEntity} instances contained in the given {@link MongoMappingContext}
* for indexing metadata and ensures the indexes to be available using reactive infrastructure.
*
* @author Mark Paluch
* @since 2.1
*/
public class ReactiveMongoPersistentEntityIndexCreator {
private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMongoPersistentEntityIndexCreator.class);
private final Map<Class<?>, Boolean> classesSeen = new ConcurrentHashMap<Class<?>, Boolean>();
private final MongoMappingContext mappingContext;
private final ReactiveIndexOperationsProvider operationsProvider;
private final IndexResolver indexResolver;
/**
* Creates a new {@link ReactiveMongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext},
* {@link ReactiveIndexOperationsProvider}.
*
* @param mappingContext must not be {@literal null}.
* @param operationsProvider must not be {@literal null}.
*/
public ReactiveMongoPersistentEntityIndexCreator(MongoMappingContext mappingContext,
ReactiveIndexOperationsProvider operationsProvider) {
this(mappingContext, operationsProvider, new MongoPersistentEntityIndexResolver(mappingContext));
}
/**
* Creates a new {@link ReactiveMongoPersistentEntityIndexCreator} for the given {@link MongoMappingContext},
* {@link ReactiveIndexOperationsProvider}, and {@link IndexResolver}.
*
* @param mappingContext must not be {@literal null}.
* @param operationsProvider must not be {@literal null}.
* @param indexResolver must not be {@literal null}.
*/
public ReactiveMongoPersistentEntityIndexCreator(MongoMappingContext mappingContext,
ReactiveIndexOperationsProvider operationsProvider, IndexResolver indexResolver) {
Assert.notNull(mappingContext, "MongoMappingContext must not be null!");
Assert.notNull(operationsProvider, "ReactiveIndexOperations must not be null!");
Assert.notNull(indexResolver, "IndexResolver must not be null!");
this.mappingContext = mappingContext;
this.operationsProvider = operationsProvider;
this.indexResolver = indexResolver;
}
/**
* Returns whether the current index creator was registered for the given {@link MappingContext}.
*
* @param context
* @return
*/
public boolean isIndexCreatorFor(MappingContext<?, ?> context) {
return this.mappingContext.equals(context);
}
/**
* Inspect entities for index creation.
*
* @return a {@link Mono} that completes without value after indexes were created.
*/
public Mono<Void> checkForIndexes(MongoPersistentEntity<?> entity) {
Class<?> type = entity.getType();
if (!classesSeen.containsKey(type)) {
if (this.classesSeen.put(type, Boolean.TRUE) == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Analyzing class " + type + " for index information.");
}
return checkForAndCreateIndexes(entity);
}
}
return Mono.empty();
}
private Mono<Void> checkForAndCreateIndexes(MongoPersistentEntity<?> entity) {
List<Mono<?>> publishers = new ArrayList<>();
if (entity.isAnnotationPresent(Document.class)) {
for (IndexDefinitionHolder indexToCreate : indexResolver.resolveIndexFor(entity.getTypeInformation())) {
publishers.add(createIndex(indexToCreate));
}
}
return publishers.isEmpty() ? Mono.empty() : Flux.merge(publishers).then();
}
Mono<String> createIndex(IndexDefinitionHolder indexDefinition) {
return operationsProvider.indexOps(indexDefinition.getCollection()).ensureIndex(indexDefinition) //
.onErrorResume(ReactiveMongoPersistentEntityIndexCreator::isDataIntegrityViolation,
e -> translateException(e, indexDefinition));
}
private Mono<? extends String> translateException(Throwable e, IndexDefinitionHolder indexDefinition) {
Mono<IndexInfo> existingIndex = fetchIndexInformation(indexDefinition);
Mono<String> defaultError = Mono.error(new DataIntegrityViolationException(
String.format("Cannot create index for '%s' in collection '%s' with keys '%s' and options '%s'.",
indexDefinition.getPath(), indexDefinition.getCollection(), indexDefinition.getIndexKeys(),
indexDefinition.getIndexOptions()),
e.getCause()));
return existingIndex.flatMap(it -> {
return Mono.<String> error(new DataIntegrityViolationException(
String.format("Index already defined as '%s'.", indexDefinition.getPath()), e.getCause()));
}).switchIfEmpty(defaultError);
}
private Mono<IndexInfo> fetchIndexInformation(IndexDefinitionHolder indexDefinition) {
Object indexNameToLookUp = indexDefinition.getIndexOptions().get("name");
Flux<IndexInfo> existingIndexes = operationsProvider.indexOps(indexDefinition.getCollection()).getIndexInfo();
return existingIndexes //
.filter(indexInfo -> ObjectUtils.nullSafeEquals(indexNameToLookUp, indexInfo.getName())) //
.next() //
.doOnError(e -> {
LOGGER.debug(
String.format("Failed to load index information for collection '%s'.", indexDefinition.getCollection()),
e);
});
}
private static boolean isDataIntegrityViolation(Throwable t) {
if (t instanceof UncategorizedMongoDbException) {
return t.getCause() instanceof MongoException
&& MongoDbErrorCodes.isDataIntegrityViolationCode(((MongoException) t.getCause()).getCode());
}
return false;
}
}

View File

@@ -21,11 +21,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
@@ -38,7 +33,6 @@ import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -55,7 +49,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
*/
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty>
implements MongoPersistentEntity<T>, ApplicationContextAware {
implements MongoPersistentEntity<T> {
private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @Field annotation!";
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@@ -63,7 +57,6 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
private final String collection;
private final String language;
private final StandardEvaluationContext context;
private final @Nullable Expression expression;
/**
@@ -79,8 +72,6 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
Class<?> rawType = typeInformation.getType();
String fallback = MongoCollectionUtils.getPreferredCollectionName(rawType);
this.context = new StandardEvaluationContext();
if (this.isAnnotationPresent(Document.class)) {
Document document = this.getRequiredAnnotation(Document.class);
@@ -95,23 +86,15 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
}
}
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context.addPropertyAccessor(new BeanFactoryAccessor());
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
context.setRootObject(applicationContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getCollection()
*/
public String getCollection() {
return expression == null ? collection : expression.getValue(context, String.class);
return expression == null //
? collection //
: expression.getValue(getEvaluationContext(null), String.class);
}
/*

View File

@@ -21,13 +21,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.annotation.Persistent;
/**
* Identifies a domain object to be persisted to MongoDB.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
* @author Oliver Gierke ogierke@vmware.com
* @author Jon Brisbin
* @author Oliver Gierke
* @author Christoph Strobl
*/
@Persistent
@@ -36,6 +37,24 @@ import org.springframework.data.annotation.Persistent;
@Target({ ElementType.TYPE })
public @interface Document {
/**
* The collection the document representing the entity is supposed to be stored in. If not configured, a default
* collection name will be derived from the type's name. The attribute supports SpEL expressions to dynamically
* calculate the collection to based on a per operation basis.
*
* @return the name of the collection to be used.
*/
@AliasFor("collection")
String value() default "";
/**
* The collection the document representing the entity is supposed to be stored in. If not configured, a default
* collection name will be derived from the type's name. The attribute supports SpEL expressions to dynamically
* calculate the collection to based on a per operation basis.
*
* @return the name of the collection to be used.
*/
@AliasFor("value")
String collection() default "";
/**

View File

@@ -87,14 +87,7 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
*/
@Override
protected <T> BasicMongoPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
BasicMongoPersistentEntity<T> entity = new BasicMongoPersistentEntity<T>(typeInformation);
if (context != null) {
entity.setApplicationContext(context);
}
return entity;
return new BasicMongoPersistentEntity<T>(typeInformation);
}
/*
@@ -103,6 +96,9 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
super.setApplicationContext(applicationContext);
this.context = applicationContext;
}
}

View File

@@ -37,8 +37,8 @@ public class MapReduceOptions {
private Optional<String> outputDatabase = Optional.empty();
private MapReduceCommand.OutputType outputType = MapReduceCommand.OutputType.REPLACE;
private Map<String, Object> scopeVariables = new HashMap<String, Object>();
private Map<String, Object> extraOptions = new HashMap<String, Object>();
private Map<String, Object> scopeVariables = new HashMap<>();
private Map<String, Object> extraOptions = new HashMap<>();
private @Nullable Boolean jsMode;
private Boolean verbose = Boolean.TRUE;
private @Nullable Integer limit;

View File

@@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import org.bson.BSON;
import org.bson.BsonRegularExpression;
import org.bson.Document;
import org.bson.types.Binary;
import org.springframework.data.domain.Example;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point;
@@ -41,6 +42,7 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -56,6 +58,7 @@ import com.mongodb.BasicDBList;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Andreas Zink
*/
public class Criteria implements CriteriaDefinition {
@@ -356,7 +359,7 @@ public class Criteria implements CriteriaDefinition {
/**
* Creates a criterion using the {@literal $type} operator.
*
* @param type must not be {@literal null}.
* @param types must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
@@ -622,6 +625,18 @@ public class Criteria implements CriteriaDefinition {
return registerCriteriaChainElement(schemaCriteria);
}
/**
* Use {@link BitwiseCriteriaOperators} as gateway to create a criterion using one of the
* <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise operators</a> like
* {@code $bitsAllClear}.
*
* @return new instance of {@link BitwiseCriteriaOperators}. Never {@literal null}.
* @since 2.1
*/
public BitwiseCriteriaOperators bits() {
return new BitwiseCriteriaOperatorsImpl(this);
}
/**
* Creates an 'or' criteria using the $or operator for all of the provided criteria
* <p>
@@ -880,4 +895,324 @@ public class Criteria implements CriteriaDefinition {
return value instanceof GeoJson
|| (value instanceof GeoCommand && ((GeoCommand) value).getShape() instanceof GeoJson);
}
/**
* MongoDB specific <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise query
* operators</a> like {@code $bitsAllClear, $bitsAllSet,...} for usage with {@link Criteria#bits()} and {@link Query}.
*
* @author Christoph Strobl
* @since 2.1
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/query-bitwise/">https://docs.mongodb.com/manual/reference/operator/query-bitwise/</a>
* @currentRead Beyond the Shadows - Brent Weeks
*/
public interface BitwiseCriteriaOperators {
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(String bitmask);
/**
* Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(List<Integer> positions);
}
/**
* Default implementation of {@link BitwiseCriteriaOperators}.
*
* @author Christoph Strobl
* @currentRead Beyond the Shadows - Brent Weeks
*/
private static class BitwiseCriteriaOperatorsImpl implements BitwiseCriteriaOperators {
private final Criteria target;
BitwiseCriteriaOperatorsImpl(Criteria target) {
this.target = target;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(int)
*/
@Override
public Criteria allClear(int numericBitmask) {
return numericBitmask("$bitsAllClear", numericBitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(java.lang.String)
*/
@Override
public Criteria allClear(String bitmask) {
return stringBitmask("$bitsAllClear", bitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allClear(java.util.List)
*/
@Override
public Criteria allClear(List<Integer> positions) {
return positions("$bitsAllClear", positions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(int)
*/
@Override
public Criteria allSet(int numericBitmask) {
return numericBitmask("$bitsAllSet", numericBitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(java.lang.String)
*/
@Override
public Criteria allSet(String bitmask) {
return stringBitmask("$bitsAllSet", bitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#allSet(java.util.List)
*/
@Override
public Criteria allSet(List<Integer> positions) {
return positions("$bitsAllSet", positions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(int)
*/
@Override
public Criteria anyClear(int numericBitmask) {
return numericBitmask("$bitsAnyClear", numericBitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(java.lang.String)
*/
@Override
public Criteria anyClear(String bitmask) {
return stringBitmask("$bitsAnyClear", bitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anyClear(java.util.List)
*/
@Override
public Criteria anyClear(List<Integer> positions) {
return positions("$bitsAnyClear", positions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(int)
*/
@Override
public Criteria anySet(int numericBitmask) {
return numericBitmask("$bitsAnySet", numericBitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(java.lang.String)
*/
@Override
public Criteria anySet(String bitmask) {
return stringBitmask("$bitsAnySet", bitmask);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.BitwiseCriteriaOperators#anySet(java.util.Collection)
*/
@Override
public Criteria anySet(List<Integer> positions) {
return positions("$bitsAnySet", positions);
}
private Criteria positions(String operator, List<Integer> positions) {
Assert.notNull(positions, "Positions must not be null!");
Assert.noNullElements(positions.toArray(), "Positions must not contain null values.");
target.criteria.put(operator, positions);
return target;
}
private Criteria stringBitmask(String operator, String bitmask) {
Assert.hasText(bitmask, "Bitmask must not be null!");
target.criteria.put(operator, new Binary(Base64Utils.decodeFromString(bitmask)));
return target;
}
private Criteria numericBitmask(String operator, int bitmask) {
target.criteria.put(operator, bitmask);
return target;
}
}
}

View File

@@ -38,7 +38,7 @@ public class Field {
private final Map<String, Integer> criteria = new HashMap<String, Integer>();
private final Map<String, Object> slices = new HashMap<String, Object>();
private final Map<String, Criteria> elemMatchs = new HashMap<String, Criteria>();
private @Nullable String postionKey;
private @Nullable String positionKey;
private int positionValue;
public Field include(String key) {
@@ -78,7 +78,7 @@ public class Field {
Assert.hasText(field, "DocumentField must not be null or empty!");
postionKey = field;
positionKey = field;
positionValue = value;
return this;
@@ -97,8 +97,8 @@ public class Field {
document.put(entry.getKey(), new Document("$elemMatch", entry.getValue().getCriteriaObject()));
}
if (postionKey != null) {
document.put(postionKey + ".$", positionValue);
if (positionKey != null) {
document.put(positionKey + ".$", positionValue);
}
return document;

View File

@@ -24,10 +24,12 @@ import org.bson.Document;
import org.springframework.data.domain.Range;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NullJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject;
import org.springframework.util.Assert;
/**
@@ -927,4 +929,64 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'date'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class DateJsonSchemaProperty extends IdentifiableJsonSchemaProperty<DateJsonSchemaObject> {
DateJsonSchemaProperty(String identifier, DateJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link DateJsonSchemaProperty}.
* @see DateJsonSchemaProperty#description(String)
*/
public DateJsonSchemaProperty description(String description) {
return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link DateJsonSchemaProperty}.
* @see DateJsonSchemaProperty#generateDescription()
*/
public DateJsonSchemaProperty generatedDescription() {
return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'timestamp'} property.
*
* @author Mark Paluch
* @since 2.1
*/
public static class TimestampJsonSchemaProperty extends IdentifiableJsonSchemaProperty<TimestampJsonSchemaObject> {
TimestampJsonSchemaProperty(String identifier, TimestampJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link TimestampJsonSchemaProperty}.
* @see TimestampJsonSchemaProperty#description(String)
*/
public TimestampJsonSchemaProperty description(String description) {
return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link TimestampJsonSchemaProperty}.
* @see TimestampJsonSchemaProperty#generateDescription()
*/
public TimestampJsonSchemaProperty generatedDescription() {
return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
}

View File

@@ -29,10 +29,12 @@ import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NullJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -125,6 +127,24 @@ public interface JsonSchemaObject {
return new NullJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'date'}.
*
* @return never {@literal null}.
*/
static DateJsonSchemaObject date() {
return new DateJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'timestamp'}.
*
* @return never {@literal null}.
*/
static TimestampJsonSchemaObject timestamp() {
return new TimestampJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of given {@link Type}.
*

View File

@@ -20,10 +20,12 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.TimestampJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
@@ -150,7 +152,7 @@ public interface JsonSchemaProperty extends JsonSchemaObject {
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @return new instance of {@link BooleanJsonSchemaProperty}.
*/
static BooleanJsonSchemaProperty bool(String identifier) {
return new BooleanJsonSchemaProperty(identifier, JsonSchemaObject.bool());
@@ -161,12 +163,34 @@ public interface JsonSchemaProperty extends JsonSchemaObject {
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @return new instance of {@link NullJsonSchemaProperty}.
*/
static NullJsonSchemaProperty nil(String identifier) {
return new NullJsonSchemaProperty(identifier, JsonSchemaObject.nil());
}
/**
* Creates a new {@link DateJsonSchemaProperty} with given {@literal identifier} of {@code type : 'date'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link DateJsonSchemaProperty}.
*/
static DateJsonSchemaProperty date(String identifier) {
return new DateJsonSchemaProperty(identifier, JsonSchemaObject.date());
}
/**
* Creates a new {@link TimestampJsonSchemaProperty} with given {@literal identifier} of {@code type : 'timestamp'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link TimestampJsonSchemaProperty}.
*/
static TimestampJsonSchemaProperty timestamp(String identifier) {
return new TimestampJsonSchemaProperty(identifier, JsonSchemaObject.timestamp());
}
/**
* Obtain a builder to create a {@link JsonSchemaProperty}.
*

View File

@@ -1447,4 +1447,189 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return "Must be null.";
}
}
/**
* {@link JsonSchemaObject} implementation of {@code type : 'date'} schema elements.<br />
* Provides programmatic access to schema specifics via a fluent API producing immutable {@link JsonSchemaObject
* schema objects}.
*
* @author Christoph Strobl
* @since 2.1
*/
static class DateJsonSchemaObject extends TypedJsonSchemaObject {
DateJsonSchemaObject() {
this(null, false, null);
}
private DateJsonSchemaObject(@Nullable String description, boolean generateDescription,
@Nullable Restrictions restrictions) {
super(Type.dateType(), description, generateDescription, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/
@Override
public DateJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new DateJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection)
*/
@Override
public DateJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
return new DateJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection)
*/
@Override
public DateJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
return new DateJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection)
*/
@Override
public DateJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
return new DateJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject)
*/
@Override
public DateJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
return new DateJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String)
*/
@Override
public DateJsonSchemaObject description(String description) {
return new DateJsonSchemaObject(description, generateDescription, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription()
*/
@Override
public DateJsonSchemaObject generatedDescription() {
return new DateJsonSchemaObject(description, true, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override
protected String generateDescription() {
return "Must be a date.";
}
}
/**
* {@link JsonSchemaObject} implementation of {@code type : 'timestamp'} schema elements.<br />
* Provides programmatic access to schema specifics via a fluent API producing immutable {@link JsonSchemaObject
* schema objects}.
*
* @author Mark Paluch
* @since 2.1
*/
static class TimestampJsonSchemaObject extends TypedJsonSchemaObject {
TimestampJsonSchemaObject() {
this(null, false, null);
}
private TimestampJsonSchemaObject(@Nullable String description, boolean generateDescription,
@Nullable Restrictions restrictions) {
super(Type.timestampType(), description, generateDescription, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/
@Override
public TimestampJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new TimestampJsonSchemaObject(description, generateDescription,
restrictions.possibleValues(possibleValues));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection)
*/
@Override
public TimestampJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
return new TimestampJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection)
*/
@Override
public TimestampJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
return new TimestampJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection)
*/
@Override
public TimestampJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
return new TimestampJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject)
*/
@Override
public TimestampJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
return new TimestampJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String)
*/
@Override
public TimestampJsonSchemaObject description(String description) {
return new TimestampJsonSchemaObject(description, generateDescription, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription()
*/
@Override
public TimestampJsonSchemaObject generatedDescription() {
return new TimestampJsonSchemaObject(description, true, restrictions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override
protected String generateDescription() {
return "Must be a timestamp.";
}
}
}

View File

@@ -34,6 +34,7 @@ import com.mongodb.client.gridfs.GridFSFindIterable;
* @author Thomas Darimont
* @author Martin Baumgartner
* @author Christoph Strobl
* @author Hartmut Lang
*/
public interface GridFsOperations extends ResourcePatternResolver {
@@ -152,11 +153,21 @@ public interface GridFsOperations extends ResourcePatternResolver {
* Returns the {@link GridFsResource} with the given file name.
*
* @param filename must not be {@literal null}.
* @return the resource if it exists or {@literal null}.
* @return the resource. Use {@link org.springframework.core.io.Resource#exists()} to check if the returned
* {@link GridFsResource} is actually present.
* @see ResourcePatternResolver#getResource(String)
*/
GridFsResource getResource(String filename);
/**
* Returns the {@link GridFsResource} for a {@link com.mongodb.client.gridfs.model.GridFSFile}.
*
* @param file must not be {@literal null}.
* @return the resource for the file.
* @since 2.1
*/
GridFsResource getResource(com.mongodb.client.gridfs.model.GridFSFile file);
/**
* Returns all {@link GridFsResource}s matching the given file name pattern.
*

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.gridfs;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
@@ -23,6 +24,8 @@ import java.util.Optional;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.MongoGridFSException;
import com.mongodb.client.gridfs.model.GridFSFile;
@@ -38,8 +41,24 @@ import com.mongodb.client.gridfs.model.GridFSFile;
public class GridFsResource extends InputStreamResource {
static final String CONTENT_TYPE_FIELD = "_contentType";
private static final ByteArrayInputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]);
private final GridFSFile file;
private final @Nullable GridFSFile file;
private final String filename;
/**
* Creates a new, absent {@link GridFsResource}.
*
* @param filename filename of the absent resource.
* @since 2.1
*/
private GridFsResource(String filename) {
super(EMPTY_INPUT_STREAM, String.format("GridFs resource [%s]", filename));
this.file = null;
this.filename = filename;
}
/**
* Creates a new {@link GridFsResource} from the given {@link GridFSFile}.
@@ -58,8 +77,35 @@ public class GridFsResource extends InputStreamResource {
*/
public GridFsResource(GridFSFile file, InputStream inputStream) {
super(inputStream);
super(inputStream, String.format("GridFs resource [%s]", file.getFilename()));
this.file = file;
this.filename = file.getFilename();
}
/**
* Obtain an absent {@link GridFsResource}.
*
* @param filename filename of the absent resource, must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
public static GridFsResource absent(String filename) {
Assert.notNull(filename, "Filename must not be null");
return new GridFsResource(filename);
}
/*
* (non-Javadoc)
* @see org.springframework.core.io.InputStreamResource#getInputStream()
*/
@Override
public InputStream getInputStream() throws IOException, IllegalStateException {
verifyExists();
return super.getInputStream();
}
/*
@@ -68,6 +114,8 @@ public class GridFsResource extends InputStreamResource {
*/
@Override
public long contentLength() throws IOException {
verifyExists();
return file.getLength();
}
@@ -77,7 +125,16 @@ public class GridFsResource extends InputStreamResource {
*/
@Override
public String getFilename() throws IllegalStateException {
return file.getFilename();
return filename;
}
/*
* (non-Javadoc)
* @see org.springframework.core.io.AbstractResource#exists()
*/
@Override
public boolean exists() {
return file != null;
}
/*
@@ -86,15 +143,30 @@ public class GridFsResource extends InputStreamResource {
*/
@Override
public long lastModified() throws IOException {
verifyExists();
return file.getUploadDate().getTime();
}
/*
* (non-Javadoc)
* @see org.springframework.core.io.AbstractResource#getDescription()
*/
@Override
public String getDescription() {
return String.format("GridFs resource [%s]", this.getFilename());
}
/**
* Returns the {@link Resource}'s id.
*
* @return never {@literal null}.
* @throws IllegalStateException if the file does not {@link #exists()}.
*/
public Object getId() {
Assert.state(exists(), () -> String.format("%s does not exist.", getDescription()));
return file.getId();
}
@@ -104,14 +176,24 @@ public class GridFsResource extends InputStreamResource {
* @return never {@literal null}.
* @throws com.mongodb.MongoGridFSException in case no content type declared on {@link GridFSFile#getMetadata()} nor
* provided via {@link GridFSFile#getContentType()}.
* @throws IllegalStateException if the file does not {@link #exists()}.
*/
@SuppressWarnings("deprecation")
public String getContentType() {
Assert.state(exists(), () -> String.format("%s does not exist.", getDescription()));
return Optionals
.firstNonEmpty(
() -> Optional.ofNullable(file.getMetadata()).map(it -> it.get(CONTENT_TYPE_FIELD, String.class)),
() -> Optional.ofNullable(file.getContentType()))
.orElseThrow(() -> new MongoGridFSException("No contentType data for this GridFS file"));
}
private void verifyExists() throws FileNotFoundException {
if (!exists()) {
throw new FileNotFoundException(String.format("%s does not exist.", getDescription()));
}
}
}

View File

@@ -51,12 +51,13 @@ import com.mongodb.client.gridfs.model.GridFSUploadOptions;
* @author Martin Baumgartner
* @author Christoph Strobl
* @author Mark Paluch
* @author Hartmut Lang
*/
public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver {
private final MongoDbFactory dbFactory;
private final String bucket;
private final @Nullable String bucket;
private final MongoConverter converter;
private final QueryMapper queryMapper;
@@ -77,7 +78,7 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
* @param converter must not be {@literal null}.
* @param bucket
*/
public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter, String bucket) {
public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter, @Nullable String bucket) {
Assert.notNull(dbFactory, "MongoDbFactory must not be null!");
Assert.notNull(converter, "MongoConverter must not be null!");
@@ -227,8 +228,19 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
*/
public GridFsResource getResource(String location) {
GridFSFile file = findOne(query(whereFilename().is(location)));
return file != null ? new GridFsResource(file, getGridFs().openDownloadStream(location)) : null;
return Optional.ofNullable(findOne(query(whereFilename().is(location)))).map(this::getResource)
.orElseGet(() -> GridFsResource.absent(location));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.gridfs.GridFsOperations#getResource(com.mongodb.client.gridfs.model.GridFSFile)
*/
public GridFsResource getResource(GridFSFile file) {
Assert.notNull(file, "GridFSFile must not be null!");
return new GridFsResource(file, getGridFs().openDownloadStream(file.getFilename()));
}
/*
@@ -246,13 +258,13 @@ public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver
if (path.isPattern()) {
GridFSFindIterable files = find(query(whereFilename().regex(path.toRegex())));
List<GridFsResource> resources = new ArrayList<GridFsResource>();
List<GridFsResource> resources = new ArrayList<>();
for (GridFSFile file : files) {
resources.add(new GridFsResource(file, getGridFs().openDownloadStream(file.getFilename())));
}
return resources.toArray(new GridFsResource[resources.size()]);
return resources.toArray(new GridFsResource[0]);
}
return new GridFsResource[] { getResource(locationPattern) };

View File

@@ -63,11 +63,10 @@ public class MongoRepositoryBean<T> extends CdiRepositoryBean<T> {
* @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class)
*/
@Override
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType, Optional<Object> customImplementation) {
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
MongoOperations mongoOperations = getDependencyInstance(operations, MongoOperations.class);
MongoRepositoryFactory factory = new MongoRepositoryFactory(mongoOperations);
return customImplementation.isPresent() ? factory.getRepository(repositoryType, customImplementation.get()) : factory.getRepository(repositoryType);
return create(() -> new MongoRepositoryFactory(mongoOperations), repositoryType);
}
}

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.data.mongodb.repository.query;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithProjection;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
@@ -27,7 +28,6 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.Sli
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.Assert;
/**
@@ -42,7 +42,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
private final MongoQueryMethod method;
private final MongoOperations operations;
private final FindWithProjection<?> findOperationWithProjection;
private final ExecutableFind<?> executableFind;
/**
* Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}.
@@ -58,11 +58,10 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
this.method = method;
this.operations = operations;
ReturnedType returnedType = method.getResultProcessor().getReturnedType();
MongoEntityMetadata<?> metadata = method.getEntityInformation();
Class<?> type = metadata.getCollectionEntity().getType();
this.findOperationWithProjection = operations//
.query(returnedType.getDomainType())//
.inCollection(method.getEntityInformation().getCollectionName());
this.executableFind = operations.query(type);
}
/*
@@ -90,8 +89,8 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection //
: findOperationWithProjection.as(typeToRead);
? executableFind //
: executableFind.as(typeToRead);
MongoQueryExecution execution = getExecution(accessor, find);
@@ -119,7 +118,11 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
} else if (isExistsQuery()) {
return q -> operation.matching(q).exists();
} else {
return q -> operation.matching(q).oneValue();
return q -> {
TerminatingFind<?> find = operation.matching(q);
return isLimiting() ? find.firstValue() : find.oneValue();
};
}
}
@@ -174,4 +177,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
* @since 1.5
*/
protected abstract boolean isDeleteQuery();
/**
* Return whether the query has an explicit limit set.
*
* @return
* @since 2.0.4
*/
protected abstract boolean isLimiting();
}

View File

@@ -22,18 +22,20 @@ import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection;
import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ReactiveFindOperation.TerminatingFind;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.CollectionExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.DeleteExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingConverter;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.ResultProcessingExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.SingleEntityExecution;
import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.TailExecution;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.Assert;
/**
@@ -48,6 +50,7 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
private final ReactiveMongoQueryMethod method;
private final ReactiveMongoOperations operations;
private final EntityInstantiators instantiators;
private final FindWithProjection<?> findOperationWithProjection;
/**
* Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and
@@ -64,6 +67,11 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
this.method = method;
this.operations = operations;
this.instantiators = new EntityInstantiators();
MongoEntityMetadata<?> metadata = method.getEntityInformation();
Class<?> type = metadata.getCollectionEntity().getType();
this.findOperationWithProjection = operations.query(type);
}
/*
@@ -103,10 +111,16 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
applyQueryMetaAttributesWhenPresent(query);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
Class<?> typeToRead = processor.getReturnedType().getTypeToRead();
FindWithQuery<?> find = typeToRead == null //
? findOperationWithProjection //
: findOperationWithProjection.as(typeToRead);
String collection = method.getEntityInformation().getCollectionName();
ReactiveMongoQueryExecution execution = getExecution(query, parameterAccessor,
new ResultProcessingConverter(processor, operations, instantiators));
new ResultProcessingConverter(processor, operations, instantiators), find);
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
}
@@ -120,11 +134,11 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @return
*/
private ReactiveMongoQueryExecution getExecution(Query query, MongoParameterAccessor accessor,
Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(accessor), resultProcessing);
Converter<Object, Object> resultProcessing, FindWithQuery<?> operation) {
return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing);
}
private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor accessor) {
private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor accessor, FindWithQuery<?> operation) {
if (isDeleteQuery()) {
return new DeleteExecution(operations, method);
@@ -133,9 +147,20 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
} else if (isTailable(method)) {
return new TailExecution(operations, accessor.getPageable());
} else if (method.isCollectionQuery()) {
return new CollectionExecution(operations, accessor.getPageable());
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all();
} else if (isCountQuery()) {
return (q, t, c) -> operation.matching(q).count();
} else {
return new SingleEntityExecution(operations, isCountQuery());
return (q, t, c) -> {
TerminatingFind<?> find = operation.matching(q);
if (isCountQuery()) {
return find.count();
}
return isLimiting() ? find.first() : find.one();
};
}
}
@@ -186,4 +211,12 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery {
* @since 1.5
*/
protected abstract boolean isDeleteQuery();
/**
* Return whether the query has an explicit limit set.
*
* @return
* @since 2.0.4
*/
protected abstract boolean isLimiting();
}

View File

@@ -19,19 +19,27 @@ import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.experimental.UtilityClass;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import org.bson.BSON;
import org.bson.codecs.BinaryCodec;
import org.bson.codecs.Codec;
import org.bson.codecs.UuidCodec;
import org.bson.json.JsonWriter;
import org.bson.types.Binary;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -41,6 +49,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.util.JSON;
/**
@@ -56,7 +65,8 @@ import com.mongodb.util.JSON;
class ExpressionEvaluatingParameterBinder {
private final SpelExpressionParser expressionParser;
private final EvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final CodecRegistryProvider codecRegistryProvider;
/**
* Creates new {@link ExpressionEvaluatingParameterBinder}
@@ -65,13 +75,14 @@ class ExpressionEvaluatingParameterBinder {
* @param evaluationContextProvider must not be {@literal null}.
*/
public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
EvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(expressionParser, "ExpressionParser must not be null!");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.codecRegistryProvider = () -> MongoClient.getDefaultCodecRegistry();
}
/**
@@ -212,18 +223,37 @@ class ExpressionEvaluatingParameterBinder {
if (value instanceof byte[]) {
String base64representation = DatatypeConverter.printBase64Binary((byte[]) value);
if (!binding.isQuoted()) {
return "{ '$binary' : '" + base64representation + "', '$type' : '" + BSON.B_GENERAL + "'}";
if (binding.isQuoted()) {
return DatatypeConverter.printBase64Binary((byte[]) value);
}
return base64representation;
return encode(new Binary((byte[]) value), BinaryCodec::new);
}
if (value instanceof UUID) {
if (binding.isQuoted()) {
return value.toString();
}
return encode((UUID) value, UuidCodec::new);
}
return JSON.serialize(value);
}
private <T> String encode(T value, Supplier<Codec<T>> defaultCodec) {
Codec<T> codec = codecRegistryProvider.getCodecFor((Class<T>) value.getClass()).orElseGet(defaultCodec);
StringWriter writer = new StringWriter();
codec.encode(new JsonWriter(writer), value, null);
writer.flush();
return writer.toString();
}
/**
* Evaluates the given {@code expressionString}.
*

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.repository.core.EntityMetadata;
/**
@@ -30,4 +31,12 @@ public interface MongoEntityMetadata<T> extends EntityMetadata<T> {
* @return
*/
String getCollectionName();
/**
* Returns the {@link MongoPersistentEntity} that supposed to determine the collection to be queried.
*
* @return
* @since 2.0.4
*/
MongoPersistentEntity<?> getCollectionEntity();
}

View File

@@ -30,9 +30,9 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Shape;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -61,10 +61,10 @@ import org.springframework.util.ClassUtils;
class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
private static final Logger LOG = LoggerFactory.getLogger(MongoQueryCreator.class);
private final MongoParameterAccessor accessor;
private final boolean isGeoNearQuery;
private final MongoParameterAccessor accessor;
private final MappingContext<?, MongoPersistentProperty> context;
private final boolean isGeoNearQuery;
/**
* Creates a new {@link MongoQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} and

View File

@@ -160,4 +160,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
protected boolean isDeleteQuery() {
return tree.isDelete();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return tree.isLimiting();
}
}

View File

@@ -44,29 +44,13 @@ import com.mongodb.client.result.DeleteResult;
* various flavors.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
*/
interface ReactiveMongoQueryExecution {
Object execute(Query query, Class<?> type, String collection);
/**
* {@link ReactiveMongoQueryExecution} for collection returning queries.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
final class CollectionExecution implements ReactiveMongoQueryExecution {
private final @NonNull ReactiveMongoOperations operations;
private final Pageable pageable;
@Override
public Object execute(Query query, Class<?> type, String collection) {
return operations.find(query.with(pageable), type, collection);
}
}
/**
* {@link ReactiveMongoQueryExecution} for collection returning queries using tailable cursors.
*
@@ -84,23 +68,6 @@ interface ReactiveMongoQueryExecution {
}
}
/**
* {@link ReactiveMongoQueryExecution} to return a single entity.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
final class SingleEntityExecution implements ReactiveMongoQueryExecution {
private final ReactiveMongoOperations operations;
private final boolean countProjection;
@Override
public Object execute(Query query, Class<?> type, String collection) {
return countProjection ? operations.count(query, type, collection) : operations.findOne(query, type, collection);
}
}
/**
* {@link MongoQueryExecution} to execute geo-near queries.
*

View File

@@ -118,7 +118,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#createCountQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createCountQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
@@ -127,7 +127,7 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery()
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isCountQuery()
*/
@Override
protected boolean isCountQuery() {
@@ -136,10 +136,19 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery()
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isDeleteQuery()
*/
@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return tree.isLimiting();
}
}

View File

@@ -27,7 +27,8 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBindingParser;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -62,13 +63,13 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
* @param evaluationContextProvider must not be {@literal null}.
*/
public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations,
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
}
/**
* Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link EvaluationContextProvider}.
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
@@ -77,7 +78,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
*/
public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method,
ReactiveMongoOperations mongoOperations, SpelExpressionParser expressionParser,
EvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations);
@@ -104,7 +105,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
@@ -125,7 +126,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery()
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isCountQuery()
*/
@Override
protected boolean isCountQuery() {
@@ -134,11 +135,20 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery()
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isDeleteQuery()
*/
@Override
protected boolean isDeleteQuery() {
return this.isDeleteQuery;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return false;
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.repository.query;
import lombok.Getter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.util.Assert;
@@ -26,7 +28,7 @@ import org.springframework.util.Assert;
class SimpleMongoEntityMetadata<T> implements MongoEntityMetadata<T> {
private final Class<T> type;
private final MongoPersistentEntity<?> collectionEntity;
private final @Getter MongoPersistentEntity<?> collectionEntity;
/**
* Creates a new {@link SimpleMongoEntityMetadata} using the given type and {@link MongoPersistentEntity} to use for

View File

@@ -28,7 +28,7 @@ import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -62,7 +62,8 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
private final ExpressionEvaluatingParameterBinder parameterBinder;
/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}.
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations},
* {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
@@ -70,13 +71,13 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
* @param evaluationContextProvider must not be {@literal null}.
*/
public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations,
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider);
}
/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod},
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link EvaluationContextProvider}.
* {@link MongoOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}.
*
* @param query must not be {@literal null}.
* @param method must not be {@literal null}.
@@ -84,7 +85,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
* @param expressionParser must not be {@literal null}.
*/
public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations,
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations);
@@ -174,6 +175,15 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
return countBooleanValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return false;
}
private static int countBooleanValues(boolean... values) {
int count = 0;

View File

@@ -39,9 +39,9 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -131,7 +131,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
EvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
}
@@ -160,10 +160,11 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final MongoOperations operations;
private final EvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
public MongoQueryLookupStrategy(MongoOperations operations, EvaluationContextProvider evaluationContextProvider,
public MongoQueryLookupStrategy(MongoOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.operations = operations;

View File

@@ -59,7 +59,7 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
/**
* Creates a new {@link QuerydslMongoPredicateExecutor} for the given {@link MongoEntityInformation} and
* {@link MongoTemplate}. Uses the {@link SimpleEntityPathResolver} to create an {@link EntityPath} for the given
* {@link MongoOperations}. Uses the {@link SimpleEntityPathResolver} to create an {@link EntityPath} for the given
* domain class.
*
* @param entityInformation must not be {@literal null}.
@@ -72,7 +72,7 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
/**
* Creates a new {@link QuerydslMongoPredicateExecutor} for the given {@link MongoEntityInformation},
* {@link MongoTemplate} and {@link EntityPathResolver}.
* {@link MongoOperations} and {@link EntityPathResolver}.
*
* @param entityInformation must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
@@ -108,19 +108,19 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate)
*/
@Override
public List<T> findAll(Predicate predicate) {
Assert.notNull(predicate, "Predicate must not be null!");
return createQueryFor(predicate).fetchResults().getResults();
return createQueryFor(predicate).fetch();
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, com.mysema.query.types.OrderSpecifier<?>[])
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, com.querydsl.core.types.OrderSpecifier<?>[])
*/
@Override
public List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders) {
@@ -128,12 +128,12 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(orders, "Order specifiers must not be null!");
return createQueryFor(predicate).orderBy(orders).fetchResults().getResults();
return createQueryFor(predicate).orderBy(orders).fetch();
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, org.springframework.data.domain.Sort)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Sort)
*/
@Override
public List<T> findAll(Predicate predicate, Sort sort) {
@@ -141,24 +141,24 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(sort, "Sort must not be null!");
return applySorting(createQueryFor(predicate), sort).fetchResults().getResults();
return applySorting(createQueryFor(predicate), sort).fetch();
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.OrderSpecifier[])
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.OrderSpecifier[])
*/
@Override
public Iterable<T> findAll(OrderSpecifier<?>... orders) {
Assert.notNull(orders, "Order specifiers must not be null!");
return createQuery().orderBy(orders).fetchResults().getResults();
return createQuery().orderBy(orders).fetch();
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, org.springframework.data.domain.Pageable)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Pageable)
*/
@Override
public Page<T> findAll(Predicate predicate, Pageable pageable) {
@@ -168,13 +168,12 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
AbstractMongodbQuery<T, SpringDataMongodbQuery<T>> query = createQueryFor(predicate);
return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetchResults().getResults(), pageable,
() -> createQueryFor(predicate).fetchCount());
return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount);
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#count(com.mysema.query.types.Predicate)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#count(com.querydsl.core.types.Predicate)
*/
@Override
public long count(Predicate predicate) {
@@ -186,7 +185,7 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.QueryDslPredicateExecutor#exists(com.mysema.query.types.Predicate)
* @see org.springframework.data.querydsl.QuerydslPredicateExecutor#exists(com.querydsl.core.types.Predicate)
*/
@Override
public boolean exists(Predicate predicate) {
@@ -197,7 +196,7 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
}
/**
* Creates a {@link MongodbQuery} for the given {@link Predicate}.
* Creates a {@link AbstractMongodbQuery} for the given {@link Predicate}.
*
* @param predicate
* @return
@@ -207,12 +206,12 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
}
/**
* Creates a {@link MongodbQuery}.
* Creates a {@link AbstractMongodbQuery}.
*
* @return
*/
private AbstractMongodbQuery<T, SpringDataMongodbQuery<T>> createQuery() {
return new SpringDataMongodbQuery<T>(mongoOperations, entityInformation.getJavaType());
return new SpringDataMongodbQuery<>(mongoOperations, entityInformation.getJavaType());
}
/**
@@ -248,13 +247,13 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
return query;
}
sort.stream().map(this::toOrder).forEach(it -> query.orderBy(it));
sort.stream().map(this::toOrder).forEach(query::orderBy);
return query;
}
/**
* Transforms a plain {@link Order} into a QueryDsl specific {@link OrderSpecifier}.
* Transforms a plain {@link Order} into a Querydsl specific {@link OrderSpecifier}.
*
* @param order
* @return

View File

@@ -36,9 +36,9 @@ import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
@@ -99,7 +99,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
EvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
}
@@ -130,7 +130,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final ReactiveMongoOperations operations;
private final EvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
/*

View File

@@ -100,7 +100,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
if (allNew) {
List<S> result = source.stream().collect(Collectors.toList());
mongoOperations.insertAll(result);
mongoOperations.insert(result, entityInformation.getCollectionName());
return result;
} else {

View File

@@ -45,6 +45,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @author Oliver Gierke
* @author Christoph Strobl
* @author Ruben J Garcia
* @since 2.0
*/
@RequiredArgsConstructor
@@ -91,13 +92,13 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
q.limit(2);
return mongoOperations.find(q, example.getProbeType(), entityInformation.getCollectionName()).buffer(2)
.flatMap(vals -> {
.map(vals -> {
if (vals.size() > 1) {
return Mono.error(new IncorrectResultSizeDataAccessException(1));
throw new IncorrectResultSizeDataAccessException(1);
}
return Mono.just(vals.iterator().next());
}).single();
return vals.iterator().next();
}).next();
}
/*
@@ -314,8 +315,7 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Assert.notNull(entityStream, "The given Publisher of entities must not be null!");
return Flux.from(entityStream)
.flatMap(entity -> entityInformation.isNew(entity) ? //
return Flux.from(entityStream).flatMap(entity -> entityInformation.isNew(entity) ? //
mongoOperations.insert(entity, entityInformation.getCollectionName()).then(Mono.just(entity)) : //
mongoOperations.save(entity, entityInformation.getCollectionName()).then(Mono.just(entity)));
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository.support;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
@@ -27,10 +28,12 @@ import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
@@ -93,7 +96,7 @@ class SpringDataMongodbSerializer extends MongodbSerializer {
return super.visit(expr, context);
}
return converter.convertToMongoType(expr.getConstant());
return toQuerydslMongoType(expr.getConstant());
}
/*
@@ -128,7 +131,8 @@ class SpringDataMongodbSerializer extends MongodbSerializer {
Document mappedIdValue = mapper.getMappedObject((BasicDBObject) superIdValue, Optional.empty());
return (DBObject) JSON.parse(mappedIdValue.toJson());
}
return super.asDBObject(key, value instanceof Pattern ? value : converter.convertToMongoType(value));
return super.asDBObject(key, value instanceof Pattern ? value : toQuerydslMongoType(value));
}
/*
@@ -231,4 +235,25 @@ class SpringDataMongodbSerializer extends MongodbSerializer {
return property;
}
private Object toQuerydslMongoType(Object source) {
Object target = converter.convertToMongoType(source);
if (target instanceof List) {
List<Object> newList = new BasicDBList();
for (Object item : (List) target) {
if (item instanceof Document) {
newList.add(new BasicDBObject(BsonUtils.asMap((Document) item)));
} else {
newList.add(item);
}
}
return newList;
}
return target;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core
import kotlin.reflect.KClass
/**
* Extension for [ExecutableMapReduceOperation.mapReduce] providing a [KClass] based variant.
*
* @author Christoph Strobl
* @since 2.1
*/
fun <T : Any> ExecutableMapReduceOperation.mapReduce(entityClass: KClass<T>): ExecutableMapReduceOperation.MapReduceWithMapFunction<T> =
mapReduce(entityClass.java)
/**
* Extension for [ExecutableMapReduceOperation.mapReduce] leveraging reified type parameters.
*
* @author Christoph Strobl
* @since 2.1
*/
inline fun <reified T : Any> ExecutableMapReduceOperation.mapReduce(): ExecutableMapReduceOperation.MapReduceWithMapFunction<T> =
mapReduce(T::class.java)
/**
* Extension for [ExecutableMapReduceOperation.MapReduceWithProjection.as] providing a [KClass] based variant.
*
* @author Christoph Strobl
* @since 2.1
*/
fun <T : Any> ExecutableMapReduceOperation.MapReduceWithProjection<T>.asType(resultType: KClass<T>): ExecutableMapReduceOperation.MapReduceWithQuery<T> =
`as`(resultType.java)
/**
* Extension for [ExecutableMapReduceOperation.MapReduceWithProjection.as] leveraging reified type parameters.
*
* @author Christoph Strobl
* @since 2.1
*/
inline fun <reified T : Any> ExecutableMapReduceOperation.MapReduceWithProjection<T>.asType(): ExecutableMapReduceOperation.MapReduceWithQuery<T> =
`as`(T::class.java)

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core
import kotlin.reflect.KClass
/**
* Extension for [ReactiveMapReduceOperation.mapReduce] providing a [KClass] based variant.
*
* @author Christoph Strobl
* @since 2.1
*/
fun <T : Any> ReactiveMapReduceOperation.mapReduce(entityClass: KClass<T>): ReactiveMapReduceOperation.MapReduceWithMapFunction<T> =
mapReduce(entityClass.java)
/**
* Extension for [ReactiveMapReduceOperation.mapReduce] leveraging reified type parameters.
*
* @author Christoph Strobl
* @since 2.1
*/
inline fun <reified T : Any> ReactiveMapReduceOperation.mapReduce(): ReactiveMapReduceOperation.MapReduceWithMapFunction<T> =
mapReduce(T::class.java)
/**
* Extension for [ReactiveMapReduceOperation.MapReduceWithProjection.as] providing a [KClass] based variant.
*
* @author Christoph Strobl
* @since 2.1
*/
fun <T : Any> ReactiveMapReduceOperation.MapReduceWithProjection<T>.asType(resultType: KClass<T>): ReactiveMapReduceOperation.MapReduceWithQuery<T> =
`as`(resultType.java)
/**
* Extension for [ReactiveMapReduceOperation.MapReduceWithProjection.as] leveraging reified type parameters.
*
* @author Christoph Strobl
* @since 2.1
*/
inline fun <reified T : Any> ReactiveMapReduceOperation.MapReduceWithProjection<T>.asType(): ReactiveMapReduceOperation.MapReduceWithQuery<T> =
`as`(T::class.java)

View File

@@ -16,7 +16,7 @@
package org.springframework.data.mongodb.core.query
/**
* Extension for [Criteria.is] providing an `isEqualTo` alias since `in` is a reserved keyword in Kotlin.
* Extension for [Criteria.is] providing an `isEqualTo` alias since `is` is a reserved keyword in Kotlin.
*
* @author Sebastien Deleuze
* @since 2.0

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ServerSession;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoDatabaseUtilsUnitTests {
@Mock ClientSession session;
@Mock ServerSession serverSession;
@Mock MongoDbFactory dbFactory;
@Mock MongoDatabase db;
@Mock UserTransaction userTransaction;
@Before
public void setUp() {
when(dbFactory.getSession(any())).thenReturn(session);
when(dbFactory.withSession(session)).thenReturn(dbFactory);
when(dbFactory.getDb()).thenReturn(db);
when(session.getServerSession()).thenReturn(serverSession);
when(session.hasActiveTransaction()).thenReturn(true);
when(serverSession.isClosed()).thenReturn(false);
}
@After
public void verifyTransactionSynchronizationManagerState() {
assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty());
assertFalse(TransactionSynchronizationManager.isSynchronizationActive());
assertNull(TransactionSynchronizationManager.getCurrentTransactionName());
assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
assertNull(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel());
assertFalse(TransactionSynchronizationManager.isActualTransactionActive());
}
@Test // DATAMONGO-1920
public void shouldNotStartSessionWhenNoTransactionOngoing() {
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
verify(dbFactory, never()).getSession(any());
verify(dbFactory, never()).withSession(any(ClientSession.class));
}
@Test // DATAMONGO-1920
public void shouldParticipateInOngoingJtaTransactionWithCommitWhenSessionSychronizationIsAny() throws Exception {
when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE,
Status.STATUS_ACTIVE);
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse();
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ALWAYS);
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue();
}
});
verify(userTransaction).begin();
verify(session).startTransaction();
verify(session).commitTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void shouldParticipateInOngoingJtaTransactionWithRollbackWhenSessionSychronizationIsAny() throws Exception {
when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE,
Status.STATUS_ACTIVE);
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse();
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ALWAYS);
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue();
transactionStatus.setRollbackOnly();
}
});
verify(userTransaction).rollback();
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void shouldNotParticipateInOngoingJtaTransactionWithRollbackWhenSessionSychronizationIsNative()
throws Exception {
when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE,
Status.STATUS_ACTIVE);
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse();
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isFalse();
transactionStatus.setRollbackOnly();
}
});
verify(userTransaction).rollback();
verify(session, never()).startTransaction();
verify(session, never()).abortTransaction();
verify(session, never()).close();
}
@Test // DATAMONGO-1920
public void shouldParticipateInOngoingMongoTransactionWhenSessionSychronizationIsNative() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue();
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
transactionStatus.setRollbackOnly();
}
});
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void shouldParticipateInOngoingMongoTransactionWhenSessionSychronizationIsAny() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(dbFactory)).isTrue();
MongoDatabaseUtils.getDatabase(dbFactory, SessionSynchronization.ALWAYS);
transactionStatus.setRollbackOnly();
}
});
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
}

View File

@@ -0,0 +1,333 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoDatabase;
import com.mongodb.session.ServerSession;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoTransactionManagerUnitTests {
@Mock ClientSession session;
@Mock ClientSession session2;
@Mock ServerSession serverSession;
@Mock MongoDbFactory dbFactory;
@Mock MongoDbFactory dbFactory2;
@Mock MongoDatabase db;
@Mock MongoDatabase db2;
@Before
public void setUp() {
when(dbFactory.getSession(any())).thenReturn(session, session2);
when(dbFactory.withSession(session)).thenReturn(dbFactory);
when(dbFactory.withSession(session2)).thenReturn(dbFactory2);
when(dbFactory.getDb()).thenReturn(db);
when(dbFactory2.getDb()).thenReturn(db2);
when(session.getServerSession()).thenReturn(serverSession);
when(session2.getServerSession()).thenReturn(serverSession);
when(serverSession.isClosed()).thenReturn(false);
}
@After
public void verifyTransactionSynchronizationManager() {
assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty());
assertFalse(TransactionSynchronizationManager.isSynchronizationActive());
}
@Test // DATAMONGO-1920
public void triggerCommitCorrectly() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
verify(dbFactory).withSession(eq(session));
txManager.commit(txStatus);
verify(session).startTransaction();
verify(session).commitTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void participateInOnGoingTransactionWithCommit() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.execute(db -> {
db.drop();
return null;
});
}
});
verify(dbFactory, times(2)).withSession(eq(session));
txManager.commit(txStatus);
verify(session).startTransaction();
verify(session).commitTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void participateInOnGoingTransactionWithRollbackOnly() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.execute(db -> {
db.drop();
return null;
});
status.setRollbackOnly();
}
});
verify(dbFactory, times(2)).withSession(eq(session));
assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> txManager.commit(txStatus));
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void triggerRollbackCorrectly() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
verify(dbFactory).withSession(eq(session));
txManager.rollback(txStatus);
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void suspendTransactionWhilePropagationNotSupported() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.execute(db -> {
db.drop();
return null;
});
}
});
template.execute(MongoDatabase::listCollections);
txManager.commit(txStatus);
verify(session).startTransaction();
verify(session2, never()).startTransaction();
verify(dbFactory, times(2)).withSession(eq(session));
verify(dbFactory, never()).withSession(eq(session2));
verify(db, times(2)).drop();
verify(db).listCollections();
verify(session).close();
verify(session2, never()).close();
}
@Test // DATAMONGO-1920
public void suspendTransactionWhilePropagationRequiresNew() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.execute(db -> {
db.drop();
return null;
});
}
});
template.execute(MongoDatabase::listCollections);
txManager.commit(txStatus);
verify(session).startTransaction();
verify(session2).startTransaction();
verify(dbFactory, times(2)).withSession(eq(session));
verify(dbFactory).withSession(eq(session2));
verify(db).drop();
verify(db2).drop();
verify(db).listCollections();
verify(session).close();
verify(session2).close();
}
@Test // DATAMONGO-1920
public void readonlyShouldInitiateASessionStartAndCommitTransaction() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
DefaultTransactionDefinition readonlyTxDefinition = new DefaultTransactionDefinition();
readonlyTxDefinition.setReadOnly(true);
TransactionStatus txStatus = txManager.getTransaction(readonlyTxDefinition);
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
verify(dbFactory).withSession(eq(session));
txManager.commit(txStatus);
verify(session).startTransaction();
verify(session).commitTransaction();
verify(session).close();
}
@Test // DATAMONGO-1920
public void readonlyShouldInitiateASessionStartAndRollbackTransaction() {
MongoTransactionManager txManager = new MongoTransactionManager(dbFactory);
DefaultTransactionDefinition readonlyTxDefinition = new DefaultTransactionDefinition();
readonlyTxDefinition.setReadOnly(true);
TransactionStatus txStatus = txManager.getTransaction(readonlyTxDefinition);
MongoTemplate template = new MongoTemplate(dbFactory);
template.execute(db -> {
db.drop();
return null;
});
verify(dbFactory).withSession(eq(session));
txManager.rollback(txStatus);
verify(session).startTransaction();
verify(session).abortTransaction();
verify(session).close();
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.mongodb.SessionAwareMethodInterceptor.MethodCache;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
import com.mongodb.MongoClient;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
/**
* Unit tests for {@link SessionAwareMethodInterceptor}.
*
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class SessionAwareMethodInterceptorUnitTests {
@Mock ClientSession session;
@Mock MongoCollection<Document> targetCollection;
@Mock MongoDatabase targetDatabase;
MongoCollection collection;
MongoDatabase database;
@Before
public void setUp() {
collection = createProxyInstance(session, targetCollection, MongoCollection.class);
database = createProxyInstance(session, targetDatabase, MongoDatabase.class);
}
@Test // DATAMONGO-1880
public void proxyFactoryOnCollectionDelegatesToMethodWithSession() {
collection.find();
verify(targetCollection).find(eq(session));
}
@Test // DATAMONGO-1880
public void proxyFactoryOnCollectionWithSessionInArgumentListProceedsWithExecution() {
ClientSession yetAnotherSession = mock(ClientSession.class);
collection.find(yetAnotherSession);
verify(targetCollection).find(eq(yetAnotherSession));
}
@Test // DATAMONGO-1880
public void proxyFactoryOnDatabaseDelegatesToMethodWithSession() {
database.drop();
verify(targetDatabase).drop(eq(session));
}
@Test // DATAMONGO-1880
public void proxyFactoryOnDatabaseWithSessionInArgumentListProceedsWithExecution() {
ClientSession yetAnotherSession = mock(ClientSession.class);
database.drop(yetAnotherSession);
verify(targetDatabase).drop(eq(yetAnotherSession));
}
@Test // DATAMONGO-1880
public void justMoveOnIfNoOverloadWithSessionAvailable() {
collection.getReadPreference();
verify(targetCollection).getReadPreference();
}
@Test // DATAMONGO-1880
public void usesCacheForMethodLookup() {
MethodCache cache = (MethodCache) ReflectionTestUtils.getField(SessionAwareMethodInterceptor.class, "METHOD_CACHE");
Method countMethod = ClassUtils.getMethod(MongoCollection.class, "count");
assertThat(cache.contains(countMethod, MongoCollection.class)).isFalse();
collection.count();
assertThat(cache.contains(countMethod, MongoCollection.class)).isTrue();
}
@Test // DATAMONGO-1880
public void cachesNullForMethodsThatDoNotHaveASessionOverload() {
MethodCache cache = (MethodCache) ReflectionTestUtils.getField(SessionAwareMethodInterceptor.class, "METHOD_CACHE");
Method readConcernMethod = ClassUtils.getMethod(MongoCollection.class, "getReadConcern");
assertThat(cache.contains(readConcernMethod, MongoCollection.class)).isFalse();
collection.getReadConcern();
collection.getReadConcern();
assertThat(cache.contains(readConcernMethod, MongoCollection.class)).isTrue();
assertThat(cache.lookup(readConcernMethod, MongoCollection.class, ClientSession.class)).isEmpty();
}
@Test // DATAMONGO-1880
public void proxiesNewDbInstanceReturnedByMethod() {
MongoDatabase otherDb = mock(MongoDatabase.class);
when(targetDatabase.withCodecRegistry(any())).thenReturn(otherDb);
MongoDatabase target = database.withCodecRegistry(MongoClient.getDefaultCodecRegistry());
assertThat(target).isInstanceOf(Proxy.class).isNotSameAs(database).isNotSameAs(targetDatabase);
target.drop();
verify(otherDb).drop(eq(session));
}
@Test // DATAMONGO-1880
public void proxiesNewCollectionInstanceReturnedByMethod() {
MongoCollection otherCollection = mock(MongoCollection.class);
when(targetCollection.withCodecRegistry(any())).thenReturn(otherCollection);
MongoCollection target = collection.withCodecRegistry(MongoClient.getDefaultCodecRegistry());
assertThat(target).isInstanceOf(Proxy.class).isNotSameAs(collection).isNotSameAs(targetCollection);
target.drop();
verify(otherCollection).drop(eq(session));
}
private MongoDatabase proxyDatabase(com.mongodb.session.ClientSession session, MongoDatabase database) {
return createProxyInstance(session, database, MongoDatabase.class);
}
private MongoCollection proxyCollection(com.mongodb.session.ClientSession session, MongoCollection collection) {
return createProxyInstance(session, collection, MongoCollection.class);
}
private <T> T createProxyInstance(com.mongodb.session.ClientSession session, T target, Class<T> targetType) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.setInterfaces(targetType);
factory.setOpaque(true);
factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, MongoDatabase.class,
this::proxyDatabase, MongoCollection.class, this::proxyCollection));
return targetType.cast(factory.getProxy());
}
}

Some files were not shown because too many files have changed in this diff Show More