diff --git a/README.adoc b/README.adoc index d8b05de..9f05689 100644 --- a/README.adoc +++ b/README.adoc @@ -9,7 +9,7 @@ This repo contains the source code of the https://www.maoudia.com[maoudia.com] w built using https://gohugo.io/[Hugo] + https://github.com/Lednerb/bilberry-hugo-theme[Billeberry] theme and hosted on https://maoudia.pages.dev/[CloudFlare Pages] + https://pages.github.com/[GitHub Pages]. == Requirements -1. https://gohugo.io/getting-started/installing/[Hugo] (0.98.x) +1. https://gohugo.io/getting-started/installing/[Hugo] (0.100.x) 2. https://www.ruby-lang.org/en/documentation/installation/[Ruby] (2.7.x) 3. https://nodejs.org/[NodeJS] (16.15.x) 4. https://asciidoctor.org/[Asciidoctor] with required https://asciidoctor.org/docs/extensions/[extensions]: diff --git a/archetypes/article.adoc b/archetypes/article.adoc index 3e5e868..371a669 100644 --- a/archetypes/article.adoc +++ b/archetypes/article.adoc @@ -6,10 +6,12 @@ lastmod = 2021-05-19T08:30:00+01:00 showPublishDate = true showLastModificationDate = true showReadingTime = true +readingtime = 10 +showAuthor = true images = ["featuredImage.jpg"] categories = ["Tutorial", "Tip"] tags = ["Java", "Kotlin", "Android", "Hugo", "SpringBoot", "GraphQL", "Programming", "Quarkus"] -project_url = "https://github.com/gohugoio/hugo" +project_url = "https=//github.com/gohugoio/hugo" author = "Moncef AOUDIA" series = [] audio = [] @@ -19,8 +21,7 @@ resizeImages = false excludeFromIndex = false draft = true slug = "/" -exclude = false +exclude : false +++ - diff --git a/assets/css/4-aoudiamoncef.css b/assets/css/4-aoudiamoncef.css index 052eba2..2b1a776 100644 --- a/assets/css/4-aoudiamoncef.css +++ b/assets/css/4-aoudiamoncef.css @@ -21,10 +21,18 @@ h1, h2, h3, h4, h5, h6 { } .bilberry-hugo-theme article .content { - padding: 0em; + padding: 0; margin: 1.5em; } +.bilberry-hugo-theme article .content h2 { + font-size: 1.8em; +} + +.bilberry-hugo-theme article .content h3 { + font-size: 1.4em; +} + .bilberry-hugo-theme header { background-color: #000a12; } @@ -172,19 +180,19 @@ h1, h2, h3, h4, h5, h6 { } .bilberry-hugo-theme footer .container ul a { - color: #2b5c00; + color: #377300; } .bilberry-hugo-theme footer .container .right .languages a { - color: #2b5c00; + color: #377300; } .bilberry-hugo-theme .credits .container .author { - color: #2b5c00; + color: #377300; } .bilberry-hugo-theme .credits .container a { - color: #2b5c00; + color: #377300; } .bilberry-hugo-theme footer .container a:hover { @@ -207,13 +215,13 @@ h1, h2, h3, h4, h5, h6 { } .bilberry-hugo-theme footer .container .right .languages span { - color: #2b5c00; + color: #377300; } .bilberry-hugo-theme header .logo img { max-width: 100%; border-radius: 50%; - height: auto; + height: inherit; } .bilberry-hugo-theme pre code { @@ -235,6 +243,10 @@ h1, h2, h3, h4, h5, h6 { height: auto; } +.bilberry-hugo-theme article .content .meta { + color: #737373; +} + .bilberry-hugo-theme article .content img { width: 100%; height: auto; @@ -242,6 +254,27 @@ h1, h2, h3, h4, h5, h6 { margin: 0 auto; } +.bilberry-hugo-theme article .content.schema img { + width: 60%; +} + +@media (max-width: 1200px) { + .bilberry-hugo-theme article .content.schema img { + width: 70%; + } +} + +@media (max-width: 1024px) { + .bilberry-hugo-theme article .content.schema img { + width: 80%; + } +} +@media (max-width: 768px) { + .bilberry-hugo-theme article .content.schema img { + width: 100%; + } +} + .badge .image { display: inline-table; } @@ -292,6 +325,32 @@ details.badge summary { font-size: 0.95em; } -.content .listingblock .content { +.bilberry-hugo-theme .content .listingblock .content { margin: 0; } + +.bilberry-hugo-theme .container { + width: 1200px; + max-width: 100%; +} + +.bilberry-hugo-theme ul { + padding-left: revert; +} + +.bilberry-hugo-theme ol li { + list-style: decimal; +} + +.bilberry-hugo-theme ol { + padding-left: revert; + margin-top: 0; +} + +.bilberry-hugo-theme .ulist ul { + padding-left: inherit; +} + +.bilberry-hugo-theme .toc ul li ul { + padding-left: inherit; +} diff --git a/config/_default/config.toml b/config/_default/config.toml index a2db448..e7f1b7a 100644 --- a/config/_default/config.toml +++ b/config/_default/config.toml @@ -31,17 +31,17 @@ disableAliases = false [markup] [markup.tableOfContents] - endLevel = 5 - ordered = false + endLevel = 4 + ordered = true startLevel = 2 [markup.asciidocExt] backend = "html5" extensions = ["asciidoctor-html5s"] failureLevel = "fatal" noHeaderOrFooter = true - preserveTOC = false + preserveTOC = true safeMode = "unsafe" - sectionNumbers = false + sectionNumbers = true trace = false verbose = false workingFolderCurrent = false diff --git a/config/_default/params.toml b/config/_default/params.toml index f976cd9..1c86b6a 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -34,7 +34,7 @@ resizeImages = false gravatarEMail = "mf.aoudia@gmail.com" # avatarEmail = "mf.aoudia@gmail.com" # set an path to the image file you want to use | overwrites gravatar -customImage = "/images/favicons/splash.webp" +customImage = "/images/logo.png" # define the icon you want to use for the overlay for the customImage or gravatar. overlayIcon = "fa-home" # always display the top navigation (with pages and search) on non-mobile screens @@ -65,6 +65,8 @@ showReadingTime = true showPublishDate = true showLastModificationDate = true + +showAuthor = true # Minimum word count to display the Table of Contents tocMinWordCount = 400 # Footer configuration diff --git a/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.fr.png b/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.fr.png new file mode 100644 index 0000000..6fd5604 Binary files /dev/null and b/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.fr.png differ diff --git a/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.png b/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.png new file mode 100644 index 0000000..ec122dc Binary files /dev/null and b/content/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.png differ diff --git a/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.adoc b/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.adoc new file mode 100644 index 0000000..760cae4 --- /dev/null +++ b/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.adoc @@ -0,0 +1,649 @@ ++++ +title = "Bulk Update With Spring Data MongoDB Reactive" +date = 2022-06-20T00:00:00+02:00 +description = "We will implement a solution to enrich and update efficiently a large amount of data using Spring Data MongoDB Reactive." +author = "Moncef AOUDIA" +showAuthor = false +showReadingTime = true +readingtime = 10 +tags = ["EIP", "Java", "Reactor", "MongoDB", "Spring Boot", "Spring Data", "Spring WebFlux", "Docker Compose", "TestContainers"] +series = ["MongoDB Reactive CLI"] +categories = ["Tutorial", "Reactive Programming"] +slug = "bulk-update-with-spring-data-mongodb-reactive" +type = "article" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Bulk Update With Spring Data MongoDB Reactive" + description = "We will implement a solution to enrich and update efficiently a large amount of data using Spring Data MongoDB Reactive." + image = "https://www.maoudia.com/blog/bulk-update-with-spring-data-mongodb-reactive/featuredImage.png" ++++ + +:toc: macro +:toc-title: Table of contents +:toclevels: 4 +:imagesdir: /images/blog/bulk-update-with-spring-data-mongodb-reactive +ifdef::env-github[] +:imagesdir: ../../static/images/bulk-update-with-spring-data-mongodb-reactive +endif::[] + +In order to update documents in a MongoDB collection, we often use update requests, if the volume of data is too large, +it could lead to performance issues and overconsumption of hardware resources. + +We will implement a solution to enrich and update efficiently a large amount of data +using Spring Data MongoDB Reactive. + + + +toc::[] + +Before continuing the reading, if you are not familiar with Spring reactive stack and MongoDB, +I suggest you to check the *resources* section. + +== EIP content enricher + +++++ +
+
+ eip content enricher +
+
+++++ + +Enterprise Integration Pattern _Content Enricher_ appends information to an existing message from an external source. +It uses information inside the incoming message to perform the enrichment operation. + +We will implement a simplified version of the EIP : + +. Input message : represented by a MongoDB document. +. Enricher : our application. +. Resource : call to a RESTful API. +. Output message : we will keep only the enriched document. + +=== Integration flow + +++++ +
+
+ integration flow schema +
+
+++++ + +The application will read the address documents, add the product and save the enriched documents to the MongoDB database. + +== Project setup + +=== Requirements + +* Java 1.8+ +* Maven 3+ +* Docker Compose +* MongoDB Database Tools + +=== Generation + +We generate the project skeleton from https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.0&packaging=jar&jvmVersion=1.8&groupId=com.maoudia&artifactId=bulk-update-with-spring-data-mongodb&name=Bulk%20Update%20with%20Spring%20Data%20MongoDB%20Reactive&description=Bulk%20Update%20with%20Spring%20data%20MongoDB%20reactive&packageName=com.maoudia.tutorial&dependencies=data-mongodb-reactive,webflux,testcontainers[`Spring Initializr`, window=\"_blank\"]. + +=== Structure + +[source,shell,indent=0,linenums=true] +---- +. +│ .gitignore +│ docker-compose.yml +│ pom.xml +│ README.adoc +├───data +│ ├───mongodb +│ │ address.ndjson +│ └───product +│ db.json +└───src + ├───main + │ ├───java + │ │ └───com + │ │ └───maoudia + │ │ └───tutorial + │ │ Application.java + │ │ AppProperties.java + │ │ CollectionService.java + │ │ NetworkConfig.java + │ └───resources + │ application.yml + └───test + └───java + └───com + └───maoudia + └───tutorial + CollectionServiceTest.java +---- + +=== Containers + +Download https://github.com/maoudia/code.maoudia.com/tree/main/bulk-update-with-spring-data-mongodb-reactive/data[`data`] directory to the root of the project. + +We use `docker-compose` to create the needed containers for this tutorial. + +[source,yml,indent=0,linenums=true] +.docker-compose.yml +---- +services: + mongodb: // <1> + container_name: maoudia-mongodb + image: mongo:5.0.8 + environment: + - MONGO_INITDB_DATABASE=test + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=password + networks: + - mongodb-network + ports: + - 15015:27017 + volumes: + - ./data/mongodb:/data/mongodb + + mongo-express: // <2> + container_name: maoudia-mongo-express + image: mongo-express:0.54.0 + depends_on: + - mongodb + networks: + - mongodb-network + environment: + - ME_CONFIG_MONGODB_SERVER=maoudia-mongodb + - ME_CONFIG_MONGODB_ADMINUSERNAME=admin + - ME_CONFIG_MONGODB_ADMINPASSWORD=password + ports: + - 1515:8081 + volumes: + - ./data/mongodb:/data/mongodb + + product-api: // <3> + container_name: maoudia-product-api + image: clue/json-server:latest + ports: + - 1519:80 + volumes: + - ./data/product/db.json:/data/db.json + +networks: + mongodb-network: + driver: bridge +---- + +<1> MongoDB initialized with the `test` database. +<2> MongoExpress is a MongoDB administration interface. +<3> Product API which is configured from `db.json` file. + + +We start up the services : + +[source,shell,indent=0,linenums=true] +---- +docker-compose up -d +---- + +=== Data initialization + +We use a JSON document from the French address database. + +.Address +[source,json,indent=0,linenums=true] +---- +{ + "id": "59350", + "type": "municipality", + "name": "Lille", + "postcode": [ + "59000", + "59800", + "59260", + "59777", + "59160" + ], + "citycode": "59350", + "x": 703219.96, + "y": 7059335.72, + "lon": 3.045433, + "lat": 50.630992, + "population": 234475, + "city": "Lille", + "context": "59, Nord, Hauts-de-France", + "importance": 0.56333 +} +---- + +Import address collection : + +[source,shell,indent=0,linenums=true] +---- +mongoimport --uri "mongodb://admin:password@localhost:15015" --authenticationDatabase=admin --db test --collection address ./data/mongodb/address.ndjson +---- + +Ou : + +We use *MongoExpress* which is available at http://localhost:1515[`http://localhost:1515`]. + +Product represents a satellite internet offer. + +.Product +[source,json,indent=0,linenums=true] +---- +{ + "id": 1, + "available": true, + "company": "SPACEX", + "provider": "STARLINK", + "type": "SATELLITE" +} +---- + +Product API is available at http://localhost:1519[`http://localhost:1519`]. + +== Application + +=== Configuration + +We change file extension from `application.properties` to `application.yml`. + +[source,yml,indent=0,linenums=true] +.application.yml +---- +app: + buffer-max-size: 500 + bulk-size: 100 + collection-name: address + enriching-key: product + enriching-uri: http://localhost:1519/products/1 +spring: + main: + web-application-type: none + data: + mongodb: + database: test + uri: mongodb://admin:password@localhost:15015 +--- +spring.config.activate.on-profile: dev +logging: + level: + org.mongodb.driver: debug +--- +spring.config.activate.on-profile: test +app: + bulk-size: 2 +---- + +We declare a class which contains application configuration properties. + +[source,java,indent=0,linenums=true] +.AppProperties.java +---- +@ConfigurationProperties(prefix = "app") +public class AppProperties { + private int bulkSize; + private int bufferMaxSize; + private String collectionName; + private String enrichingKey; + private String enrichingUri; + // Getter and Setter are omitted +} +---- + +We create a `@Bean` of Spring non-blocking HTTP client. + +[source,java,indent=0,linenums=true] +.NetworkConfig.java +---- +@Configuration +public class NetworkConfig { + + @Bean + public WebClient client() { + return WebClient.create(); + } + +} +---- + +=== Implementation + +We create a `@Service` which contains application business logic. + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +@Service +public class CollectionService { + private final AppProperties properties; + private final ReactiveMongoTemplate template; + private final WebClient client; + + public CollectionService(AppProperties properties, + ReactiveMongoTemplate template, + WebClient client) { + this.properties = properties; + this.template = template; + this.client = client; + } + + public Flux enrichAll(String collectionName, String enrichingKey, String enrichingUri) { + return template.findAll(Document.class, collectionName) // <1> + .onBackpressureBuffer(properties.getBufferMaxSize()) // <2> + .flatMap(document -> enrich(document, enrichingKey, enrichingUri)) // <3> + .map(CollectionService::toReplaceOneModel) // <4> + .window(properties.getBulkSize()) // <5> + .flatMap(replaceOneModelFlux -> bulkWrite(replaceOneModelFlux, collectionName)); // <6> + } +} +---- + +<1> Creates a stream of documents from the collection. +<2> Limits the maximum number of loaded documents in the _RAM_ in case of consumption process is slower than production. +If the maximum buffer size is exceeded, an `IllegalStateException` is thrown. +<3> Enriches document asynchronously with the external one. +<4> Creates a `ReplaceOneModel` from document. +<5> Group documents into streams of fixed size. The last stream can be smaller. +<6> Calls bulk write function. + +[NOTE] +==== +Configuration property `app.bulk-size` can be adjusted according to the project needs and available hardware resources. +The larger the value of the maximum size, the higher the memory consumption and the size of the requests. +==== + +We create document enrichment functions. + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private Publisher enrich(Document document, String enrichingKey, String enrichingUri) { // <1> + return getEnrichingDocument(enrichingUri) + .map(enrichingDocument -> { + document.put(enrichingKey, enrichingDocument); + document.put("updatedAt", new Date()); + return document; + }); +} + +private Mono getEnrichingDocument(String enrichingUri) { // <2> + return client.get() + .uri(URI.create(enrichingUri)) + .retrieve() + .bodyToMono(Document.class); +} +---- + +<1> Adds the retrieved document from HTTP call to root of document to be enriched with the key passed in parameter. +<2> Retrieves a document from an URI. + +[NOTE] +==== +MongoDB converts and stores dates in UTC by default. +==== + + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private static final ReplaceOptions REPLACE_OPTIONS = new ReplaceOptions(); // <1> +private static ReplaceOneModel toReplaceOneModel (Document document) { + return new ReplaceOneModel<>( + Filters.eq("_id", document.get("_id")), // <2> + document, // <3> + REPLACE_OPTIONS + ); +} +---- + +<1> Instantiates default replacement configuration. +<2> Filter that allows matching by document identifier. +<3> Content to be replaced, represents the complete enriched document. + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private static final BulkWriteOptions BULK_WRITE_OPTIONS = new BulkWriteOptions().ordered(false); // <1> +private Flux bulkWrite(Flux> updateOneModelFlux, String collectionName) { + return updateOneModelFlux.collectList() // <2> + .flatMapMany(unused -> template.getCollection(collectionName) // <3> + .flatMapMany(collection -> collection.bulkWrite(updateOneModels, BULK_WRITE_OPTIONS))); // <4> +} +---- + +<1> Instantiates writing options with disabling operations order. +<2> Collects the stream into a list. +<3> Retrieves the collection passed as a parameter. +<4> Bulk writes documents into MongoDB collection. + +[NOTE] +==== +Transactions are supported on Replicaset since MongoDB 4.2. +If transactions are enabled, we can use `@Transactional` or `TransactionalOperator` to make a method transactional. +==== + +We implement the following interfaces: + +* `CommandLineRunner` : runs enrichment command at application startup. +* `ExitCodeGenerator` : manages application system exit code. + +[source,java,indent=0,linenums=true] +.Application.java +---- +@SpringBootApplication(exclude = MongoReactiveRepositoriesAutoConfiguration.class) // <1> +@ConfigurationPropertiesScan("com.maoudia.tutorial") // <2> +public class Application implements CommandLineRunner, ExitCodeGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); + private final AppProperties properties; + private final CollectionService service; + private int exitCode = 255; + + public static void main(String[] args) { + System.exit(SpringApplication.exit(SpringApplication.run(Application.class, args))); + } + + public Application(AppProperties properties, CollectionService service) { + this.properties = properties; + this.service = service; + } + + @Override + public void run(final String... args) { + service.enrichAll(properties.getCollectionName(), properties.getEnrichingKey(), properties.getEnrichingUri()) + .doOnSubscribe(unused -> LOGGER.info("------------------< Staring Collection Enriching Command >-------------------")) // <3> + .doOnNext(bulkWriteResult -> LOGGER.info("Bulk write result with {} modified document(s)", bulkWriteResult.getModifiedCount())) + .doOnError(throwable -> { + exitCode = 1; + LOGGER.error("Collection enriching failed due to : {}", throwable.getMessage(), throwable); + }) + .doOnComplete(() -> exitCode = 0) + .doOnTerminate(() -> LOGGER.info("------------------< Collection Enriching Command Finished >------------------")) + .blockLast(); // <4> + } + + @Override + public int getExitCode() { + return exitCode; + } + +} +---- + +<1> Disables auto-configuration of repositories, as we use `MongoReactiveTemplate` only. +<2> Allows scanning and detecting beans that carry the `@ConfigProperties` annotation. +<3> Subscribing to stream triggers the processing. +<4> Without a running web server, we have to subscribe indefinitely to the `Publisher` in order to trigger +and wait until the end of the execution. + +=== Demo + +We launch the application : + +[source,shell,indent=0,linenums=true] +---- +mvn spring-boot:run +---- + +Output : + +[source,console,indent=0,linenums=true] +---- +... +2022-06-10 00:36:45.152 INFO 7036 --- [ main] com.maoudia.tutorial.Application : Started Application in 2.755 seconds (JVM running for 3.251) +2022-06-10 00:36:45.227 INFO 7036 --- [ main] com.maoudia.tutorial.Application : ------------------< Staring Collection Enriching Command >------------------- +2022-06-10 00:36:45.297 INFO 7036 --- [ main] org.mongodb.driver.cluster : No server chosen by com.mongodb.reactivestreams.client.internal.ClientSessionHelper$$Lambda$543/543409470@4647881c from cluster description ClusterDescription{type=UNKNOWN, connectionMode=SINGLE, serverDescriptions=[ServerDescription{address=localhost:15015, type=UNKNOWN, state=CONNECTING}]}. Waiting for 30000 ms before timing out +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.connection : Opened connection [connectionId{localValue:1, serverValue:39}] to localhost:15015 +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.connection : Opened connection [connectionId{localValue:2, serverValue:40}] to localhost:15015 +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.cluster : Monitor thread successfully connected to server with description ServerDescription{address=localhost:15015, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=13, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=61576400} +2022-06-10 00:36:46.692 INFO 7036 --- [ntLoopGroup-2-3] org.mongodb.driver.connection : Opened connection [connectionId{localValue:3, serverValue:41}] to localhost:15015 +2022-06-10 00:36:48.355 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.482 INFO 7036 --- [ntLoopGroup-2-4] org.mongodb.driver.connection : Opened connection [connectionId{localValue:4, serverValue:42}] to localhost:15015 +2022-06-10 00:36:48.562 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.742 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.982 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.222 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.488 INFO 7036 --- [ntLoopGroup-2-4] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.701 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.852 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.031 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.105 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.106 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : ------------------< Collection Enriching Command Finished >------------------ +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 17.315 s +[INFO] Finished at: 2022-06-10T00:36:54+02:00 +[INFO] ------------------------------------------------------------------------ + +Process finished with exit code 0 +---- + +=== VisuelVM report + +*VisualVM* is a lightweight profiling tool. It is used to have an overview of the threads which are launched by the application. + +++++ +
+
+ visuelvm report +
+
+++++ + +There are two groups of threads that execute operations in parallel, each group forms an _event loop_. + +* MongoDB requests are executed by `nioEventLoopGroup`. +* HTTP requests are executed by `reactor-http-nio`. + +== Integration tests + +We use *JUnit 5* and the *Testcontainers MongoDB* module for the integration tests. +It allows to have a feedback close to the real behaviour of the application which essentially do read/write operations. + +To keep this tutorial short, we will only write one test. + +[source,java,indent=0,linenums=true] +.CollectionServiceTest.java +---- +@Profile("test") +@SpringBootTest +@Testcontainers // <1> +class CollectionServiceTest { + + @Container + private static final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:5.0.8") // <2> + .withReuse(true); + + @DynamicPropertySource + private static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); // <3> + } + + @Autowired + private AppProperties properties; + @Autowired + private CollectionService command; + @Autowired + private ReactiveMongoTemplate template; + + @Test + void multipleBulkWriteResultsAreReturned() { + Document givenDocument1 = new Document(); + givenDocument1.put("_id", "628ea3edb5110304e5e814f6"); + givenDocument1.put("type", "municipality"); + Document givenDocument2 = new Document(); + givenDocument2.put("_id", "628ea3edb5110304e5e814f7"); + givenDocument2.put("type", "street"); + Document givenDocument3 = new Document(); + givenDocument3.put("_id", "628ea3edb5110304e5e814f8"); + givenDocument3.put("type", "housenumber"); + + template.insert(Arrays.asList(givenDocument1, givenDocument2, givenDocument3), properties.getCollectionName()).blockLast(); + + BulkWriteResult expectedBulkWriteResult1 = BulkWriteResult.acknowledged(WriteRequest.Type.REPLACE, 2, 2, Collections.emptyList(), + Collections.emptyList()); + BulkWriteResult expectedBulkWriteResult2 = BulkWriteResult.acknowledged(WriteRequest.Type.REPLACE, 1, 1, Collections.emptyList(), + Collections.emptyList()); + + command.enrichAll( properties.getCollectionName(), properties.getEnrichingKey() , properties.getEnrichingUri()) + .as(StepVerifier::create) // <4> + .expectNext(expectedBulkWriteResult1) + .expectNext(expectedBulkWriteResult2) + .verifyComplete(); + } +} +---- + +<1> Adds TestContainers Junit 5 extension. +<2> Starts a MongoDB container. +<3> Sets up application with container's URI. +<4> Uses `StepVerifier` from *Reactor Test* to assert output stream. + +We launch the integration tests : + +[source,shell,indent=0,linenums=true] +---- +mvn test -Dspring.profiles.active=test +---- + +Test results : + +[source,console,indent=0,linenums=true] +---- +... +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 20.563 s - in com.maoudia.tutorial.CollectionServiceTest +[INFO] +[INFO] Results: +[INFO] +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 32.100 s +[INFO] Finished at: 2022-06-10T01:02:17+02:00 +[INFO] ------------------------------------------------------------------------ +---- + +== Conclusion + +In this tutorial, we managed to implement a complete solution to enrich and update efficiently a MongoDB collection. +Moreover, we have seen how to write integration tests with JUnit 5 and Testcontainers. + +The complete source code is available on https://github.com/maoudia/code.maoudia.com/tree/main/bulk-update-with-spring-data-mongodb-reactive[Github]. + +In the next chapter of *MongoDB Reactive CLI* series, we will add new features and use https://picocli.info/[Picocli] to facilitate interactions +with the application. + +== Resources + +* https://www.enterpriseintegrationpatterns.com/DataEnricher.html[EIP Data enricher] +* https://www.mongodb.com/try/download/database-tools[MongoDB Database Tools] +* https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/[French Adresses Data] +* https://mongodb.github.io/mongo-java-driver/4.6/driver-reactive/tutorials/bulk-writes/[MongoDB Java Driver Bulk operations] +* https://projectreactor.io/docs/core/release/reference/[Reactor 3 Reference Guide] +* https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/[Spring Data MongoDB Reference] +* https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html[Web on Reactive Stack] +* https://visualvm.github.io/[VisualVM] +* https://www.testcontainers.org/modules/databases/mongodb/[Testcontainers MongoDB] diff --git a/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.fr.adoc b/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.fr.adoc new file mode 100644 index 0000000..12ba170 --- /dev/null +++ b/content/blog/bulk-update-with-spring-data-mongodb-reactive/index.fr.adoc @@ -0,0 +1,649 @@ ++++ +title = "Mise À Jour En Masse Avec Spring Data MongoDB Reactive" +date = 2022-06-20T00:00:00+02:00 +description = "On va implémenter une solution pour enrichir et mettre à jour efficacement un grand volume de données en utilisant Spring Data MongoDB Reactive." +author = "Moncef AOUDIA" +showAuthor = false +showReadingTime = true +readingtime = 10 +tags = ["EIP", "Java", "Reactor", "MongoDB", "Spring Boot", "Spring Data", "Spring WebFlux", "Docker Compose", "TestContainers"] +series = ["MongoDB Reactive CLI"] +categories = ["Tutorial", "Reactive Programming"] +slug = "mise-a-jour-en-masse-avec-spring-data-mongodb-reactive" +type = "article" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Mise À Jour En Masse Avec Spring Data MongoDB Reactive" + description = "On va implémenter une solution pour enrichir et mettre à jour efficacement un grand volume de données en utilisant Spring Data MongoDB Reactive." + image = "https://www.maoudia.com/fr/blog/mise-a-jour-en-masse-avec-spring-data-mongodb-reactive/featuredImage.png" ++++ + +:toc: macro +:toc-title: Sommaire +:toclevels: 4 +:imagesdir: /images/blog/bulk-update-with-spring-data-mongodb-reactive +ifdef::env-github[] +:imagesdir: ../../static/images/bulk-update-with-spring-data-mongodb-reactive +endif::[] + +Afin de mettre à jour les documents d'une collection MongoDB, on passe souvent par des requêtes de mise à jour, si le volume des données est conséquent, +cela peut conduire à des problèmes de performance et une surconsommation des ressources matérielles. + +On va implémenter une solution pour enrichir et mettre à jour efficacement un grand volume de données +en utilisant Spring Data MongoDB Reactive. + + + +toc::[] + +Avant de continuer la lecture, si vous n'êtes pas familier avec la pile réactive de Spring et MongoDB, +je suggère de consulter la section *ressources*. + +== EIP enrichisseur de contenu + +++++ +
+
+ eip enrichisseur de contenu +
+
+++++ + +Le modèle d'intégration d'entreprise _enrichisseur de contenu_ permet d'ajouter des informations à un message existant depuis une source externe. +Il utilise les informations contenues dans le message entrant pour effectuer l'opération d'enrichissement. + +On va implémenter une version simplifiée de l'_EIP_ : + +. Message en entrée : représenté par un document MongoDB. +. Enrichisseur : notre application. +. Ressource : appel à une API RESTful. +. Message en sortie : nous ne conserverons que le document enrichi. + +=== Flux d'intégration + +++++ +
+
+ schema du flux d'intégration +
+
+++++ + +L'application va lire les documents adresse, ajouter le produit et sauvegarder les documents enrichis dans la base MongoDB. + +== Configuration du projet + +=== Prérequis + +* Java 1.8+ +* Maven 3+ +* Docker Compose +* MongoDB Database Tools + +=== Génération + +On génère le squelette du projet depuis https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.7.0&packaging=jar&jvmVersion=1.8&groupId=com.maoudia&artifactId=bulk-update-with-spring-data-mongodb&name=Bulk%20Update%20with%20Spring%20Data%20MongoDB%20Reactive&description=Bulk%20Update%20with%20Spring%20data%20MongoDB%20reactive&packageName=com.maoudia.tutorial&dependencies=data-mongodb-reactive,webflux,testcontainers[`Spring Initializr`, window=\"_blank\"]. + +=== Structure + +[source,shell,indent=0,linenums=true] +---- +. +│ .gitignore +│ docker-compose.yml +│ pom.xml +│ README.adoc +├───data +│ ├───mongodb +│ │ address.ndjson +│ └───product +│ db.json +└───src + ├───main + │ ├───java + │ │ └───com + │ │ └───maoudia + │ │ └───tutorial + │ │ Application.java + │ │ AppProperties.java + │ │ CollectionService.java + │ │ NetworkConfig.java + │ └───resources + │ application.yml + └───test + └───java + └───com + └───maoudia + └───tutorial + CollectionServiceTest.java +---- + +=== Conteneurs + +On télécharge le dossier https://github.com/maoudia/code.maoudia.com/tree/main/bulk-update-with-spring-data-mongodb-reactive/data[`data`] vers la racine du projet. + +On utilise `docker-compose` pour créer les conteneurs nécessaires pour ce tutoriel. + +[source,yml,indent=0,linenums=true] +.docker-compose.yml +---- +services: + mongodb: // <1> + container_name: maoudia-mongodb + image: mongo:5.0.8 + environment: + - MONGO_INITDB_DATABASE=test + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=password + networks: + - mongodb-network + ports: + - 15015:27017 + volumes: + - ./data/mongodb:/data/mongodb + + mongo-express: // <2> + container_name: maoudia-mongo-express + image: mongo-express:0.54.0 + depends_on: + - mongodb + networks: + - mongodb-network + environment: + - ME_CONFIG_MONGODB_SERVER=maoudia-mongodb + - ME_CONFIG_MONGODB_ADMINUSERNAME=admin + - ME_CONFIG_MONGODB_ADMINPASSWORD=password + ports: + - 1515:8081 + volumes: + - ./data/mongodb:/data/mongodb + + product-api: // <3> + container_name: maoudia-product-api + image: clue/json-server:latest + ports: + - 1519:80 + volumes: + - ./data/product/db.json:/data/db.json + +networks: + mongodb-network: + driver: bridge +---- + +<1> MongoDB initialisé avec la base de données `test`. +<2> MongoExpress est une interface d'administration MongoDB. +<3> L'API produit est configurée depuis le fichier `db.json`. + + +On démarre les services : + +[source,shell,indent=0,linenums=true] +---- +docker-compose up -d +---- + +=== Initialisation des données + +On utilise un document JSON issu de la base d'adresses française. + +.Adresse +[source,json,indent=0,linenums=true] +---- +{ + "id": "59350", + "type": "municipality", + "name": "Lille", + "postcode": [ + "59000", + "59800", + "59260", + "59777", + "59160" + ], + "citycode": "59350", + "x": 703219.96, + "y": 7059335.72, + "lon": 3.045433, + "lat": 50.630992, + "population": 234475, + "city": "Lille", + "context": "59, Nord, Hauts-de-France", + "importance": 0.56333 +} +---- + +On importe la collection d'adresses : + +[source,shell,indent=0,linenums=true] +---- +mongoimport --uri "mongodb://admin:password@localhost:15015" --authenticationDatabase=admin --db test --collection address ./data/mongodb/address.ndjson +---- + +Ou : + +On utilise MongoExpress qui est accessible sur http://localhost:1515[`http://localhost:1515`]. + +Le produit représente une offre d'internet par satellite. + +.Produit +[source,json,indent=0,linenums=true] +---- +{ + "id": 1, + "available": true, + "company": "SPACEX", + "provider": "STARLINK", + "type": "SATELLITE" +} +---- + +L'API produit est accessible sur http://localhost:1519[`http://localhost:1519`]. + +== Application + +=== Configuration + +On change l'extension du fichier de `application.properties` vers `application.yml`. + +[source,yml,indent=0,linenums=true] +.application.yml +---- +app: + buffer-max-size: 500 + bulk-size: 100 + collection-name: address + enriching-key: product + enriching-uri: http://localhost:1519/products/1 +spring: + main: + web-application-type: none + data: + mongodb: + database: test + uri: mongodb://admin:password@localhost:15015 +--- +spring.config.activate.on-profile: dev +logging: + level: + org.mongodb.driver: debug +--- +spring.config.activate.on-profile: test +app: + bulk-size: 2 +---- + +On déclare une classe qui va contenir les propriétés de configuration de l'application. + +[source,java,indent=0,linenums=true] +.AppProperties.java +---- +@ConfigurationProperties(prefix = "app") +public class AppProperties { + private int bulkSize; + private int bufferMaxSize; + private String collectionName; + private String enrichingKey; + private String enrichingUri; + // Les Getter et Setter sont omis +} +---- + +On crée un `@Bean` du client HTTP non bloquant de Spring. + +[source,java,indent=0,linenums=true] +.NetworkConfig.java +---- +@Configuration +public class NetworkConfig { + + @Bean + public WebClient client() { + return WebClient.create(); + } + +} +---- + +=== Implémentation + +On crée le `@Service` qui va contenir la logique métier de l'application. + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +@Service +public class CollectionService { + private final AppProperties properties; + private final ReactiveMongoTemplate template; + private final WebClient client; + + public CollectionService(AppProperties properties, + ReactiveMongoTemplate template, + WebClient client) { + this.properties = properties; + this.template = template; + this.client = client; + } + + public Flux enrichAll(String collectionName, String enrichingKey, String enrichingUri) { + return template.findAll(Document.class, collectionName) // <1> + .onBackpressureBuffer(properties.getBufferMaxSize()) // <2> + .flatMap(document -> enrich(document, enrichingKey, enrichingUri)) // <3> + .map(CollectionService::toReplaceOneModel) // <4> + .window(properties.getBulkSize()) // <5> + .flatMap(replaceOneModelFlux -> bulkWrite(replaceOneModelFlux, collectionName)); // <6> + } +} +---- + +<1> Crée un flux de documents à partir de la collection. +<2> Limite le nombre maximum de documents chargés dans la _RAM_ en cas de consommation plus lente que la production. +Si la taille maximale du tampon est dépassée, une `IllegalStateException` est levée. +<3> Enrichie le document avec le document externe d'une façon asynchrone. +<4> Crée un `ReplaceOneModel` à partir du document. +<5> Regroupe les documents en flux de taille fixe. Le dernier flux peut être de taille inférieure. +<6> Appel la fonction d'écriture en masse. + +[NOTE] +==== +La propriété de configuration `app.bulk-size` peut être ajustée en fonction des besoins et ressources matérielles disponibles. +Plus la taille du _bulk_ est grande, plus la consommation de mémoire et la taille des requêtes seront élevées. +==== + +On crée les fonctions d'enrichissement de document. + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private Publisher enrich(Document document, String enrichingKey, String enrichingUri) { // <1> + return getEnrichingDocument(enrichingUri) + .map(enrichingDocument -> { + document.put(enrichingKey, enrichingDocument); + document.put("updatedAt", new Date()); + return document; + }); +} + +private Mono getEnrichingDocument(String enrichingUri) { // <2> + return client.get() + .uri(URI.create(enrichingUri)) + .retrieve() + .bodyToMono(Document.class); +} +---- + +<1> Ajoute le document récupéré depuis l'appel _HTTP_ à la racine du document à enrichir avec la clef passée en paramètre. +<2> Récupère le document depuis l'_URI_. + +[NOTE] +==== +MongoDB convertie et stocke les dates en UTC par défaut. +==== + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private static final ReplaceOptions REPLACE_OPTIONS = new ReplaceOptions(); // <1> +private static ReplaceOneModel toReplaceOneModel (Document document) { + return new ReplaceOneModel<>( + Filters.eq("_id", document.get("_id")), // <2> + document, // <3> + REPLACE_OPTIONS + ); +} +---- + +<1> Instancie la configuration de remplacement par défaut. +<2> Le filtre permet la correspondance par identifiant document. +<3> Le contenu à remplacer, représente l'intégralité du document enrichi. + + +[source,java,indent=0,linenums=true] +.CollectionService.java +---- +private static final BulkWriteOptions BULK_WRITE_OPTIONS = new BulkWriteOptions().ordered(false); // <1> +private Flux bulkWrite(Flux> updateOneModelFlux, String collectionName) { + return updateOneModelFlux.collectList() // <2> + .flatMapMany(unused -> template.getCollection(collectionName) // <3> + .flatMapMany(collection -> collection.bulkWrite(updateOneModels, BULK_WRITE_OPTIONS))); // <4> +} +---- + +<1> Instancie les options d'écritures en désactivant l'ordre des opérations. +<2> Collecte le flux dans une liste. +<3> Récupère la collection passée en paramètre. +<4> Écrit en masse les documents dans la collection MongoDB. + +[NOTE] +==== +Les transactions sont supportées sur les _Replicaset_ depuis MongoDB 4.2. +Si les transactions sont activées, on peut utiliser `@Transactional` ou `TransactionalOperator` pour rendre une méthode transactionnelle. +==== + +On implémente les interfaces suivantes : + +* `CommandLineRunner` : exécute la commande d'enrichissement au démarrage de l'application. +* `ExitCodeGenerator` : gère le code de sortie système. + +[source,java,indent=0,linenums=true] +.Application.java +---- +@SpringBootApplication(exclude = MongoReactiveRepositoriesAutoConfiguration.class) // <1> +@ConfigurationPropertiesScan("com.maoudia.tutorial") // <2> +public class Application implements CommandLineRunner, ExitCodeGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); + private final AppProperties properties; + private final CollectionService service; + private int exitCode = 255; + + public static void main(String[] args) { + System.exit(SpringApplication.exit(SpringApplication.run(Application.class, args))); + } + + public Application(AppProperties properties, CollectionService service) { + this.properties = properties; + this.service = service; + } + + @Override + public void run(final String... args) { + service.enrichAll(properties.getCollectionName(), properties.getEnrichingKey(), properties.getEnrichingUri()) + .doOnSubscribe(unused -> LOGGER.info("------------------< Staring Collection Enriching Command >-------------------")) // <3> + .doOnNext(bulkWriteResult -> LOGGER.info("Bulk write result with {} modified document(s)", bulkWriteResult.getModifiedCount())) + .doOnError(throwable -> { + exitCode = 1; + LOGGER.error("Collection enriching failed due to : {}", throwable.getMessage(), throwable); + }) + .doOnComplete(() -> exitCode = 0) + .doOnTerminate(() -> LOGGER.info("------------------< Collection Enriching Command Finished >------------------")) + .blockLast(); // <4> + } + + @Override + public int getExitCode() { + return exitCode; + } + +} +---- + +<1> Désactive l'auto-configuration des repositories, car on utilise `MongoReactiveTemplate` seulement. +<2> Permet de scanner et détecter les _beans_ qui portent l'annotation `@ConfigProperties`. +<3> L'inscription au flux déclenche le traitement. +<4> Sans un serveur web en fonctionnement, nous devons nous abonner indéfiniment au `Publisher` afin de déclencher +et attendre la fin de l'exécution. + +=== Démo + +On lance l'application : + +[source,shell,indent=0,linenums=true] +---- +mvn spring-boot:run +---- + +Sortie : + +[source,console,indent=0,linenums=true] +---- +... +2022-06-10 00:36:45.152 INFO 7036 --- [ main] com.maoudia.tutorial.Application : Started Application in 2.755 seconds (JVM running for 3.251) +2022-06-10 00:36:45.227 INFO 7036 --- [ main] com.maoudia.tutorial.Application : ------------------< Staring Collection Enriching Command >------------------- +2022-06-10 00:36:45.297 INFO 7036 --- [ main] org.mongodb.driver.cluster : No server chosen by com.mongodb.reactivestreams.client.internal.ClientSessionHelper$$Lambda$543/543409470@4647881c from cluster description ClusterDescription{type=UNKNOWN, connectionMode=SINGLE, serverDescriptions=[ServerDescription{address=localhost:15015, type=UNKNOWN, state=CONNECTING}]}. Waiting for 30000 ms before timing out +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.connection : Opened connection [connectionId{localValue:1, serverValue:39}] to localhost:15015 +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.connection : Opened connection [connectionId{localValue:2, serverValue:40}] to localhost:15015 +2022-06-10 00:36:46.527 INFO 7036 --- [localhost:15015] org.mongodb.driver.cluster : Monitor thread successfully connected to server with description ServerDescription{address=localhost:15015, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=13, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=61576400} +2022-06-10 00:36:46.692 INFO 7036 --- [ntLoopGroup-2-3] org.mongodb.driver.connection : Opened connection [connectionId{localValue:3, serverValue:41}] to localhost:15015 +2022-06-10 00:36:48.355 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.482 INFO 7036 --- [ntLoopGroup-2-4] org.mongodb.driver.connection : Opened connection [connectionId{localValue:4, serverValue:42}] to localhost:15015 +2022-06-10 00:36:48.562 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.742 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:48.982 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.222 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.488 INFO 7036 --- [ntLoopGroup-2-4] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.701 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:49.852 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.031 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.105 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : Bulk write result with 100 modified document(s) +2022-06-10 00:36:50.106 INFO 7036 --- [ntLoopGroup-2-3] com.maoudia.tutorial.Application : ------------------< Collection Enriching Command Finished >------------------ +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 17.315 s +[INFO] Finished at: 2022-06-10T00:36:54+02:00 +[INFO] ------------------------------------------------------------------------ + +Process finished with exit code 0 +---- + +=== Rapport VisuelVM + +*VisualVM* est un outil de profilage léger. On l'utilise pour avoir une vue d'ensemble sur les threads qui sont lancés par l'application. + +++++ +
+
+ rapport VisualVM +
+
+++++ + +On observe deux groupes de threads qui exécutent les opérations en parallèle, chaque groupe forme une l'_event loop_. + +* Les requêtes MongoDB sont exécutées par `nioEventLoopGroup`. +* Les requêtes HTTP sont exécutées par `reactor-http-nio`. + +== Tests d'intégration + +On utilise *JUnit 5* et le module *Testcontainers MongoDB* pour les tests d'intégration. +Cela permet d'avoir un retour proche du comportement réel de l'application qui fait essentiellement des opérations de lecture/écriture. + +Pour que ce tutoriel reste court, on va se contenter d'écrire qu'un seul test. + +[source,java,indent=0,linenums=true] +.CollectionServiceTest.java +---- +@Profile("test") +@SpringBootTest +@Testcontainers // <1> +class CollectionServiceTest { + + @Container + private static final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:5.0.8") // <2> + .withReuse(true); + + @DynamicPropertySource + private static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); // <3> + } + + @Autowired + private AppProperties properties; + @Autowired + private CollectionService command; + @Autowired + private ReactiveMongoTemplate template; + + @Test + void multipleBulkWriteResultsAreReturned() { + Document givenDocument1 = new Document(); + givenDocument1.put("_id", "628ea3edb5110304e5e814f6"); + givenDocument1.put("type", "municipality"); + Document givenDocument2 = new Document(); + givenDocument2.put("_id", "628ea3edb5110304e5e814f7"); + givenDocument2.put("type", "street"); + Document givenDocument3 = new Document(); + givenDocument3.put("_id", "628ea3edb5110304e5e814f8"); + givenDocument3.put("type", "housenumber"); + + template.insert(Arrays.asList(givenDocument1, givenDocument2, givenDocument3), properties.getCollectionName()).blockLast(); + + BulkWriteResult expectedBulkWriteResult1 = BulkWriteResult.acknowledged(WriteRequest.Type.REPLACE, 2, 2, Collections.emptyList(), + Collections.emptyList()); + BulkWriteResult expectedBulkWriteResult2 = BulkWriteResult.acknowledged(WriteRequest.Type.REPLACE, 1, 1, Collections.emptyList(), + Collections.emptyList()); + + command.enrichAll( properties.getCollectionName(), properties.getEnrichingKey() , properties.getEnrichingUri()) + .as(StepVerifier::create) // <4> + .expectNext(expectedBulkWriteResult1) + .expectNext(expectedBulkWriteResult2) + .verifyComplete(); + } +} +---- + +<1> Ajoute l'extension Junit 5 de TestContainers. +<2> Démarre un conteneur MongoDB. +<3> Configure l'application avec l'URI du conteneur. +<4> Utilise `StepVerifier` de *Reactor Test* pour faire des assertions sur le flux en sortie. + +On lance les tests d'intégration : + +[source,shell,indent=0,linenums=true] +---- +mvn test -Dspring.profiles.active=test +---- + +Résultats des tests : + +[source,console,indent=0,linenums=true] +---- +... +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 20.563 s - in com.maoudia.tutorial.CollectionServiceTest +[INFO] +[INFO] Results: +[INFO] +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 32.100 s +[INFO] Finished at: 2022-06-10T01:02:17+02:00 +[INFO] ------------------------------------------------------------------------ +---- + +== Conclusion + +Dans ce tutoriel, on a réussi à implémenter une solution complète pour enrichir et mettre à jour efficacement une collection MongoDB. +De plus, on a vu comment écrire des tests d'intégration avec JUnit 5 et Testcontainers. + +Le code source complet est disponible sur https://github.com/maoudia/code.maoudia.com/tree/main/bulk-update-with-spring-data-mongodb-reactive[Github]. + +Dans le prochain chapitre de la série *MongoDB Reactive CLI*, on ajoutera de nouvelles fonctionnalités et utilisera https://picocli.info/[Picocli] afin de faciliter les interactions +avec l'application. + +== Ressources + +* https://www.enterpriseintegrationpatterns.com/DataEnricher.html[EIP Data enricher] +* https://www.mongodb.com/try/download/database-tools[MongoDB Database Tools] +* https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/[French Adresses Data] +* https://mongodb.github.io/mongo-java-driver/4.6/driver-reactive/tutorials/bulk-writes/[MongoDB Java Driver Bulk operations] +* https://projectreactor.io/docs/core/release/reference/[Reactor 3 Reference Guide] +* https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/[Spring Data MongoDB Reference] +* https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html[Web on Reactive Stack] +* https://visualvm.github.io/[VisualVM] +* https://www.testcontainers.org/modules/databases/mongodb/[Testcontainers MongoDB] diff --git a/content/page/about/index.adoc b/content/page/about/index.adoc index 552fc49..5acb1a8 100644 --- a/content/page/about/index.adoc +++ b/content/page/about/index.adoc @@ -7,6 +7,13 @@ showPublishDate = false showLastModificationDate = false showReadingTime = false slug = "about" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Moncef AOUDIA" + description = "1x engineer, open-source enthusiast/maintainer, simracing/flightsim lover." + image = "https://www.maoudia.com/images/banners/banner-1200x600.png" +++ :badges: /images/badges @@ -180,7 +187,7 @@ slug = "about" :zulip: image:{badges}/zulip.svg[zulip] -I am a software developer, open-source enthusiast/maintainer. +1x engineer, open-source enthusiast/maintainer, simracing/flightsim lover. diff --git a/content/page/about/index.fr.adoc b/content/page/about/index.fr.adoc index 6370a64..841c477 100644 --- a/content/page/about/index.fr.adoc +++ b/content/page/about/index.fr.adoc @@ -7,6 +7,13 @@ showPublishDate = false showLastModificationDate = false showReadingTime = false slug = "about" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Moncef AOUDIA" + description = "Ingénieur 1x, passionné/mainteneur d'open-source, amoureux de simulations automobiles et vols." + image = "https://www.maoudia.com/images/banners/banner-1200x600.png" +++ :badges: /images/badges @@ -179,7 +186,7 @@ slug = "about" :yammer: image:{badges}/yammer.svg[yammer] :zulip: image:{badges}/zulip.svg[zulip] -Je suis développeur logiciels, passionné/mainteneur d'open-source. +Ingénieur 1x, passionné/mainteneur d'open-source, amoureux de simulations automobiles et vols. diff --git a/content/page/uses/index.adoc b/content/page/uses/index.adoc index 9762757..bf16cf1 100644 --- a/content/page/uses/index.adoc +++ b/content/page/uses/index.adoc @@ -7,6 +7,13 @@ showPublishDate = false showLastModificationDate = false showReadingTime = false slug = "uses" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Moncef AOUDIA" + description = "Here are the software and hardware I use." + image = "https://www.maoudia.com/images/banners/banner-1200x600.png" +++ :oss: image:/images/badges/oss.svg[open source badge, 50, 20] diff --git a/content/page/uses/index.fr.adoc b/content/page/uses/index.fr.adoc index 8210064..3ac0b55 100644 --- a/content/page/uses/index.fr.adoc +++ b/content/page/uses/index.fr.adoc @@ -7,6 +7,13 @@ showPublishDate = false showLastModificationDate = false showReadingTime = false slug = "uses" +[twitter] + card = "summary_large_image" + site = "@AoudiaMoncef" + creator = "@AoudiaMoncef" + title = "Moncef AOUDIA" + description = "Voici les logiciels et le matériel que j'utilise." + image = "https://www.maoudia.com/images/banners/banner-1200x600.png" +++ :oss: image:/images/badges/oss.svg[open source badge, 50, 20] diff --git a/content/status/website/index.adoc b/content/status/website/index.adoc deleted file mode 100644 index 291a08d..0000000 --- a/content/status/website/index.adoc +++ /dev/null @@ -1,22 +0,0 @@ -+++ -date = "2021-05-19T07:30:00+01:00" -publishDate = "2021-05-19T07:30:00+01:00" -showPublishDate = false -noSummary = true -resizeImages = false -type = "status" -exclude = true -+++ - -.Available pages 🔗 -[NOTE] -==== -* link:https://www.maoudia.com/about/[About] - -* link:https://www.maoudia.com/uses/[Uses] -==== - -[WARNING] -==== -Work in progress... 👨‍💻 -==== diff --git a/content/status/website/index.fr.adoc b/content/status/website/index.fr.adoc deleted file mode 100644 index f44b7fb..0000000 --- a/content/status/website/index.fr.adoc +++ /dev/null @@ -1,22 +0,0 @@ -+++ -date = "2021-05-19T07:30:00+01:00" -publishDate = "2021-05-19T07:30:00+01:00" -showPublishDate = false -noSummary = true -resizeImages = false -type = "status" -exclude = true -+++ - -.Pages disponibles 🔗 -[NOTE] -==== -* link:https://www.maoudia.com/fr/about/[À propos] - -* link:https://www.maoudia.com/fr/uses/[Usages] -==== - -[WARNING] -==== -Travail en cours… 👨‍💻 -==== \ No newline at end of file diff --git a/i18n/en.toml b/i18n/en.toml index a3ead95..cd8658e 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -33,7 +33,7 @@ other = "Other languages" [readingTime] one = "1 min read" -other = "{{.Count}} min read" +other = "{{.Count}} mins read" [tableOfContents] other = "Table of Contents" @@ -78,4 +78,13 @@ other = "Rerouting..." other = "Offline mode ❌" [offlineMessage] -other = "Oops, please wait until your internet connection is back" \ No newline at end of file +other = "Oops, please wait until your internet connection is back" + +[recentContent] +other = "Recent content" + +[in] +other = "in" + +[on] +other = "on" \ No newline at end of file diff --git a/i18n/fr.toml b/i18n/fr.toml index 05061dd..c64c1ec 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -20,20 +20,20 @@ other = "Chercher ..." other = "Rien n'a été trouvé." [olderPosts] -other = "Articles plus anciens" +other = "Postes plus anciens" [newerPosts] -other = "messages plus récents" +other = "Postes plus récents" [continueReading] -other = "continuer la lecture" +other = "Continuer la lecture" [otherLanguages] other = "Autres langues" [readingTime] -one = "1 Min. lecture" -other = "{{.Count}} Min. lecture" +one = "1 min de lecture" +other = "{{.Count}} mins lecture" [series] other = "Séries" @@ -75,4 +75,13 @@ other = "Redirection..." other = "Mode hors ligne ❌" [offlineMessage] -other = "Oops, veuillez attendre le retour de votre connexion internet" \ No newline at end of file +other = "Oops, veuillez attendre le retour de votre connexion internet" + +[recentContent] +other = "Contenu récent" + +[in] +other = "sur" + +[on] +other = "sur" \ No newline at end of file diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index c7b01d1..f763613 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -9,8 +9,8 @@ {{ end }} {{ .Scratch.Set "image" $imageUrl }} - {{ .Scratch.Set "imageWidth" 700 }} - {{ .Scratch.Set "imageHeight" 350 }} + {{ .Scratch.Set "imageWidth" 1200 }} + {{ .Scratch.Set "imageHeight" 600 }} {{ else if .Resources.GetMatch "featuredImage.*" }} {{ $imageUrl := "" }} @@ -22,17 +22,17 @@ {{ end }} {{ .Scratch.Set "image" $imageUrl }} - {{ .Scratch.Set "imageWidth" 700 }} - {{ .Scratch.Set "imageHeight" 350 }} + {{ .Scratch.Set "imageWidth" 1200 }} + {{ .Scratch.Set "imageHeight" 600 }} {{ else if .Params.featuredImage }} {{ .Scratch.Set "image" (.Params.featuredImage | absURL) }} {{ else if .Params.mp4videoImage }} {{ .Scratch.Set "image" (.Params.mp4videoImage | absURL) }} {{ else }} - {{ .Scratch.Set "image" (printf "https://www.gravatar.com/avatar/%s?size=200" (md5 .Site.Params.gravatarEMail)) }} - {{ .Scratch.Set "imageWidth" 200 }} - {{ .Scratch.Set "imageHeight" 200 }} + {{ .Scratch.Set "image" ("/images/banners/banner-1200x600.png" | absURL) }} + {{ .Scratch.Set "imageWidth" 1200 }} + {{ .Scratch.Set "imageHeight" 600 }} {{ end }} {{- if ne .Description "" -}} @@ -92,6 +92,10 @@ + {{ if .Site.Params.enableTwitterCard | default true }} + {{ partial "twitter-card.html" . }} + {{ end }} + {{ if .Site.Params.enableOpenGraph | default true }} {{ partial "opengraph.html" . }} {{ end }} @@ -119,10 +123,6 @@ {{ end }} - - {{ if .Site.Params.enableTwitterCard | default true }} - {{ partial "twitter-card.html" . }} - {{ end }} diff --git a/layouts/blog/rss.xml b/layouts/blog/rss.xml index 37b8c23..300b03b 100644 --- a/layouts/blog/rss.xml +++ b/layouts/blog/rss.xml @@ -9,16 +9,16 @@ xmlns:webfeeds="http://webfeeds.org/rss/1.0"> {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} - {{ .Site.BaseURL }}images/favicons/splash.webp + {{ .Site.BaseURL }}images/logo.png 512 512 {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} - {{ .Site.BaseURL }}images/banners/banner-700x350.webp - 700 - 150 + {{ .Site.BaseURL }}images/banners/banner-1200x600.png + 1200 + 600 Hugo -- gohugo.io{{ with .Site.LanguageCode }} {{.}}{{end}}{{ with .Site.Author.email }} @@ -30,9 +30,9 @@ xmlns:webfeeds="http://webfeeds.org/rss/1.0"> {{ printf "" .Permalink .MediaType | safeHTML }} {{ end }} 000A12 - + {{ .Site.BaseURL }}images/favicons/favicon.ico - {{ .Site.BaseURL }}images/favicons/splash.webp + {{ .Site.BaseURL }}images/logo.png {{ if .Site.GoogleAnalytics }} diff --git a/layouts/index.rss.xml b/layouts/index.rss.xml index 1c03ca8..7683a0a 100644 --- a/layouts/index.rss.xml +++ b/layouts/index.rss.xml @@ -3,22 +3,22 @@ xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:webfeeds="http://webfeeds.org/rss/1.0"> - {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} {{ i18n "on" }} {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} - Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} + {{ i18n "recentContent" }} {{ if ne .Title .Site.Title }}{{ with .Title }}{{ i18n "in" }} {{.}} {{ end }}{{ end }}{{ i18n "on" }} {{ .Site.Title }} - {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} {{ i18n "on" }} {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} - {{ "images/favicons/splash.webp" | absURL }} + {{ "images/logo.png" | absURL }} 512 512 - {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} {{ i18n "on" }} {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} - {{ "images/banners/banner-700x350.webp" | absURL }} - 700 - 150 + {{ "images/banners/banner-1200x600.png" | absURL }} + 1200 + 600 Hugo -- gohugo.io{{ with .Site.LanguageCode }} {{.}}{{end}}{{ with .Site.Author.email }} @@ -30,9 +30,9 @@ xmlns:webfeeds="http://webfeeds.org/rss/1.0"> {{ printf "" .Permalink .MediaType | safeHTML }} {{ end }} 000A12 - + {{ "images/favicons/favicon.ico" | absURL }} - {{ "images/favicons/splash.webp" | absURL }} + {{ "images/logo.png" | absURL }} {{ if .Site.GoogleAnalytics }} diff --git a/layouts/partials/default-content.html b/layouts/partials/default-content.html index cc72678..67c1666 100644 --- a/layouts/partials/default-content.html +++ b/layouts/partials/default-content.html @@ -7,8 +7,8 @@

{{ if ( .ctx.Params.showPublishDate | default true ) }} + {{ i18n "published" }} {{ if ( .ctx.Site.Params.enableMomentJs | default true ) }} - {{ i18n "published" }} {{ .ctx.PublishDate.Format "2006-01-02" }} {{ else }} {{ .ctx.PublishDate.Format (.ctx.Site.Params.DateFormat | default "2006-01-02") }} @@ -30,13 +30,15 @@

{{ $lastMod := .ctx.Lastmod.Format (.ctx.Site.Params.DateFormat | default "2006-01-02") }} {{ $lastModTime := time $lastMod }} {{if ($lastModTime.After (time $publishDate))}} + | + {{ i18n "updated" }} {{ $lastMod }} {{ end }} {{ end }} {{ end }} - {{ if ( and (.ctx.Site.Params.showReadingTime | default false) (.ctx.Params.showReadingTime | default true)) }} + {{ if ( and (.ctx.Site.Params.showReadingTime | default true) (.ctx.Params.showReadingTime | default false)) }} | {{ if .ctx.Params.readingTime }} {{ i18n "readingTime" .ctx.Params.readingTime }} @@ -57,14 +59,16 @@

{{ end }} - {{ with .ctx.Params.author }} - | {{ i18n "by" }} - - {{ $urlValue := replace . " " "-" | lower}} - {{ with $.ctx.Site.GetPage (printf "/author/%s" $urlValue ) }} - {{ .Title }} - {{ end }} - + {{ if ( and (.ctx.Site.Params.showAuthor | default true) (.ctx.Params.showAuthor | default false)) }} + {{ with .ctx.Params.author }} + | {{ i18n "by" }} + + {{ $urlValue := replace . " " "-" | lower}} + {{ with $.ctx.Site.GetPage (printf "/author/%s" $urlValue ) }} + {{ .Title }} + {{ end }} + + {{ end }} {{ end }}

diff --git a/layouts/partials/featured-image.html b/layouts/partials/featured-image.html index 4238910..4a75a41 100644 --- a/layouts/partials/featured-image.html +++ b/layouts/partials/featured-image.html @@ -2,16 +2,16 @@ {{ else if and (isset .Params "featuredimage") (ne .Params.featuredImage "") }} {{ end }} diff --git a/layouts/partials/opengraph.html b/layouts/partials/opengraph.html index d08c9d6..6e42ea7 100644 --- a/layouts/partials/opengraph.html +++ b/layouts/partials/opengraph.html @@ -4,6 +4,8 @@ + + diff --git a/layouts/partials/twitter-card.html b/layouts/partials/twitter-card.html new file mode 100644 index 0000000..72ac533 --- /dev/null +++ b/layouts/partials/twitter-card.html @@ -0,0 +1,32 @@ +{{ with .Params.twitter }} + + + {{ if ne .card "app" -}} + + + + {{ with .image_alt -}}{{- end }} + {{- end }} + {{ with .creator -}}{{- end }} + + {{- if eq .card "app" }} + {{ with .app_country }}{{ end }} + {{ with .app_name_iphone }}{{ end }} + {{ with .app_id_iphone }}{{ end }} + {{ with .app_url_iphone }}{{ end }} + {{ with .app_name_ipad }}{{ end }} + {{ with .app_id_ipad }}{{ end }} + {{ with .app_url_ipad }}{{ end }} + {{ with .app_name_googleplay }}{{ end }} + {{ with .app_id_googleplay }}{{ end }} + {{ with .app_url_googleplay }}{{ end }} + {{- end }} + + {{- if eq .card "player" }} + {{ with .player }}{{ end }} + {{ with .player_width }}{{ end }} + {{ with .player_height }}{{ end }} + {{ with .player_stream }}{{ end }} + {{ with .player_stream_content_type }}{{ end }} + {{- end }} +{{ end }} \ No newline at end of file diff --git a/layouts/robots.txt b/layouts/robots.txt index e0ad40c..7d329b1 100644 --- a/layouts/robots.txt +++ b/layouts/robots.txt @@ -1,3 +1 @@ -Sitemap: https://www.maoudia.com/sitemap.xml User-agent: * -Disallow: /static \ No newline at end of file diff --git a/package.json b/package.json index 73ab562..85e82b9 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ "webpack": "5.53.0", "webpack-cli": "4.8.0", "workbox-webpack-plugin": "6.3.0" + }, + "volta": { + "node": "16.15.1" } } diff --git a/scripts/cloudflare_pages.sh b/scripts/cloudflare_pages.sh index 72ee741..905b70a 100755 --- a/scripts/cloudflare_pages.sh +++ b/scripts/cloudflare_pages.sh @@ -11,7 +11,7 @@ echo "Updating to latest theme submodule" git submodule update --init --recursive --remote echo "Building Hugo website" -hugo --minify +hugo $HUGO_ARGS echo "Building service worker" cd scripts && ./sw.sh diff --git a/static/images/banners/banner-1200x600.png b/static/images/banners/banner-1200x600.png new file mode 100644 index 0000000..24de1f4 Binary files /dev/null and b/static/images/banners/banner-1200x600.png differ diff --git a/static/images/banners/banner-700x350.webp b/static/images/banners/banner-700x350.webp deleted file mode 100644 index 9289167..0000000 Binary files a/static/images/banners/banner-700x350.webp and /dev/null differ diff --git a/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher-fr.svg b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher-fr.svg new file mode 100644 index 0000000..06dd558 --- /dev/null +++ b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher-fr.svg @@ -0,0 +1,3 @@ + + +Enrichisseur
Message
Basique
Message...
Message
Enrichi
Message...
Ressource
Ressource
Text is not SVG - cannot display
\ No newline at end of file diff --git a/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher.svg b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher.svg new file mode 100644 index 0000000..e94975e --- /dev/null +++ b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/content-enricher.svg @@ -0,0 +1,3 @@ + + +Enricher
Basic  Message
Basic  M...
Enriched  Message
Enriched...
Resource
Resource
Text is not SVG - cannot display
\ No newline at end of file diff --git a/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/integration-flow.svg b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/integration-flow.svg new file mode 100644 index 0000000..94f3dbd --- /dev/null +++ b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/integration-flow.svg @@ -0,0 +1,3 @@ + + +
1
1
mongodb
mongodb
2
2
4
4
application
application
3
3
product-api
product-api
Text is not SVG - cannot display
\ No newline at end of file diff --git a/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/visual-vm-report.webp b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/visual-vm-report.webp new file mode 100644 index 0000000..829199e Binary files /dev/null and b/static/images/blog/bulk-update-with-spring-data-mongodb-reactive/visual-vm-report.webp differ diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..d3de95a Binary files /dev/null and b/static/images/logo.png differ