From d71e09ef28e6a4d5dda16899345c030c36a9b4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 Nov 2024 22:37:58 +0100 Subject: [PATCH 1/2] docs(website): Initialize the website --- docs/website/docs/index.md | 141 ++++++++++++++++++++++++++++++++++++- docs/website/mkdocs.yml | 8 +-- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/docs/website/docs/index.md b/docs/website/docs/index.md index 4f5ca0b6..6714f529 100644 --- a/docs/website/docs/index.md +++ b/docs/website/docs/index.md @@ -4,4 +4,143 @@ template: home.html # Welcome! -The OpenSavvy Playground is a project template with pre-configured CI/CD, documentation generator, etc. +## Towards the future of MongoDB in Kotlin + +**KtMongo is a complete rethink of [KMongo](https://litote.org/kmongo/) based on the official Kotlin MongoDB driver.** KMongo was created in 2016 and made MongoDB usage in Kotlin much better, largely participating in the popularity of MongoDB in the Kotlin ecosystem. In 2023, when the official Kotlin driver was released, KMongo was deprecated. However, the official drivers lack most the utilities that made KMongo so attractive. Our goal is to bring back the readability of KMongo, pushing type-safety even further. + +How much further? Let's look at a basic example; we want to query some data: +```kotlin +class Document( + val _id: ObjectId, + val user: User, +) + +class User( + val gender: String, + val age: Int, +) +``` + +With the Kotlin driver, we can easily write a query: +```kotlin title="With the official Kotlin driver" +collection.find( + and( + eq("user.gender", "female"), + gt("user.age", 29) + ) +) +``` +However, that query offers no type-safety guarantees. We could have made a typo in the name of a field (or, the name could change in the future), values aren't typed to their field, and we could accidentally use the wrong operator (for example, the aggregation `$eq` instead of the filter `$eq`). + +KMongo brought a type-safe DSL that enabled the compiler to check the name, structure and types of our fields during type-checking, vastly diminishing the risk of incorrect code: +```kotlin title="With KMongo (deprecated)" +collection.find( + and( + Document::user / User::gender eq "female" //(1)! + Document::user / User::age eq 29 //(2)! + ) +) +``` + +1. The `Document::user / User::gender` syntax is expanded to `"user.gender"`. Although this syntax is slightly more verbose, it also makes it impossible to use the wrong field paths. +2. KMongo checks the type of the passed parameters. If we tried to compare the `age` field with a string, we would get a compile error, ensuring our code stays correct. + +While this example is slightly more verbose, it is also much safer, and thus more maintainable. If we want to rename a field in a document, we can use our IDE's built-in refactoring feature, and all requests are automatically kept up-to-date. + +However, KMongo doesn't verify at compile-time the coherence of requests. For example, we could use the `$set` operator in a query, which would error out at runtime. By replacing intermediary values by DSLs, we can make the above example shorter and safer: +```kotlin title="With KtMongo" +collection.find { + Document::user / User::gender eq "female" + Document::user / User::age eq 29 +} +``` + +In this new DSL, all the benefits of KMongo remain, the `$and` operator is implied by the presence of multiple filters, and operators cannot be used in incoherent ways (we cannot use `$set` in a `find()`). + +Additionally, this new DSL is easier to inspect: the `this` value injected into all DSL scopes has a `toString` implementation that displays the exact JSON query that would be sent to the database. + +## Going further: optional filter parameters + +A pattern we very often see in the wild is the presence of some kind of optional filter. For example, if we have optional filters for a date range. These optional filters quickly make queries harder to read: + +=== "With list builders" + + ```kotlin title="With KMongo (deprecated)" + collection.find( + and( + buildList { + add(Document::user / User::name eq "Bob") + + if (minCreationDate != null) + add(Document::user / User::creationDate gte minCreationDate) + + if (maxCreationDate != null) + add(Document::user / User::creationDate lte maxCreationDate) + } + ) + ) + ``` + +=== "With nullability" + + ```kotlin title="With KMongo (deprecated)" + collection.find( + and( + listOfNotNull( + Document::user / User::name eq "Bob", + minCreationDate?.let { Document::user / User::creationDate gte it }, + maxCreationDate?.let { Document::user / User::creationDate lte it }, + ) + ) + ) + ``` + +Since KtMongo uses a DSL, query generation can take full advantage of the Kotlin language directly: +```kotlin title="With KtMongo" +collection.find { + Document::user / User::name eq "Bob" + + if (minCreationDate != null) + Document::user / User::creationDate gte minCreationDate + + if (maxCreationDate != null) + Document::user / User::creationDate lte maxCreationDate +} +``` +The same can be said of all other Kotlin language features: conditions, loops, but also creating functions to abstract away a common query that may be parameterized. + +In fact, the specific case of optional query parameters is so common that we added special operators to facilitate it: the `notNull` family. Using them, the previous query can be rewritten to: +```kotlin title="With KtMongo" +collection.find { + Document::user / User::name eq "Bob" + Document::user / User::creationDate gteNotNull minCreationDate + Document::user / User::creationDate lteNotNull maxCreationDate +} +``` + +KtMongo provides multiple features following this trend: adding operators to facilitate common usage in ways that follow the helpfulness of the Kotlin ecosystem. + +## Objectives of KtMongo + +Broadly-speaking, our objectives can be described as follows: + +**Ease of use in new projects.** Adopting KtMongo in a new project should be as simple as possible. Ideally as simple as using the official drivers. + +**Ease of use in existing KMongo projects.** If you have a large codebase using KMongo, we want to let you insert KtMongo incrementally, so you can benefit from our added features without planning a massive rewrite. + +**Ease of debugging.** As much as possible, KtMongo classes have a `toString` implementation that displays the actual BSON that would be sent to the database. If you log the requests or use a debugger, you can understand them at a glance, and run the same query in MongoDB Compass or any other tool trivially. + +**Documentation.** KtMongo is documented in depth: almost all functions have an example of usage, each operator has a link to the official MongoDB documentation, and DSL scopes list their operators with the MongoDB syntax so you can easily find the Kotlin function, even if it is named differently. + +**Convenience for the real world.** MongoDB is used in massive codebases in the industry. We want to facilitate real-world usage patterns, taking advantage of the power of Kotlin. `*notNull` operator variants and filtered collections are examples of such utilities. + +**Keeping the door open for multiplatform.** While we are not actively developing KtMongo on other platforms than the JVM, all modules are already configured to ensure the addition of other platforms in the future is possible. In particular, we're thinking of NodeJS (for scripting) and WASM (for future backends). If you'd like to contribute in this direction, feel free to get in touch! + +## Where do I start? + +[//]: # (TODO: add links) + +- **Configuring KtMongo in a new project** +- **Using KtMongo alongside the official Kotlin driver** +- **Migrating from KMongo** +- **Discovering the new features** diff --git a/docs/website/mkdocs.yml b/docs/website/mkdocs.yml index 9ead25d7..ad33f382 100644 --- a/docs/website/mkdocs.yml +++ b/docs/website/mkdocs.yml @@ -1,7 +1,7 @@ -site_name: OpenSavvy Playground +site_name: OpenSavvy KtMongo site_author: OpenSavvy & contributors site_description: > - Project template with CI/CD and IDE configuration. + The next MongoDB driver for Kotlin theme: name: material @@ -87,9 +87,7 @@ nav: - Getting started: [] - - Module1: [] - - - Module2: [] + - Features: [] - Reference: - reference.md -- 2.51.2 From 0c6e208d66dac1c72eec57e0ccaf6e08db1dc615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 15 Nov 2024 19:25:05 +0100 Subject: [PATCH 2/2] docs(website): Add a tutorial --- docs/website/docs/tutorials/index.md | 250 +++++++++++++++++++++++++++ docs/website/mkdocs.yml | 3 +- 2 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 docs/website/docs/tutorials/index.md diff --git a/docs/website/docs/tutorials/index.md b/docs/website/docs/tutorials/index.md new file mode 100644 index 00000000..c57d83db --- /dev/null +++ b/docs/website/docs/tutorials/index.md @@ -0,0 +1,250 @@ +# Getting started + +KtMongo is a modern DSL for interacting with MongoDB in Kotlin. On the JVM, KtMongo is built on top of the official Kotlin driver. + +This guide shows you how to create an application that uses the KtMongo driver to connect to a MongoDB instance and interact with data. + +If you are a KMongo user, read the [KMongo migration guide](from-kmongo/setup.md) instead. + +??? tip "TL;DR" + If you're used to Kotlin projects using Gradle, here are the important steps: + + - Add a dependency on `dev.opensavvy.ktmongo:driver-coroutines` or `dev.opensavvy.ktmongo:driver-sync` + - Add a dependency on any serialization library supported by the official Kotlin driver + - Use `MongoCollection.asKtMongo()` to access the classes of the driver + +## Creating a Kotlin project + +First, create a regular Kotlin project, using your build tool of choice. We recommend using an IDE, such as IntelliJ IDEA (Community or Ultimate) or Eclipse. + +To do so, follow one of these tutorials: + +- [Creating a project with Gradle with IntelliJ](https://kotlinlang.org/docs/get-started-with-jvm-gradle-project.html) (recommended) +- [Creating a project with Gradle in the CLI](https://docs.gradle.org/current/samples/sample_building_kotlin_applications.html) +- [Creating a project with Maven](https://kotlinlang.org/docs/maven.html) + +### Adding KtMongo + +KtMongo is available in two variants: the coroutines-aware variant, and the synchronous variant. +The coroutines driver is recommended for modern Kotlin projects, especially if you're using Ktor or another coroutines-first library. + +=== "Gradle" + + Open the `build.gradle.kts` file and edit the `dependencies {}` block: + ```kotlin + dependencies { + implementation("dev.opensavvy.ktmongo:driver-coroutines:VERSION_HERE") + } + ``` + + Replace `VERSION_HERE` by one of [the available versions](https://search.maven.org/artifact/dev.opensavvy.ktmongo/driver-coroutines). + + After editing the file, synchronize your project to ensure the dependencies are downloaded: + + - In IntelliJ, press 'shift' twice then type "Sync all Gradle projects", then press enter. + - In the CLI, run `./gradlew build`. + +=== "Maven" + + Open the `pom.xml` file and edit the `` tag: + ```xml + + + dev.opensavvy.ktmongo + driver-coroutines-jvm + VERSION_HERE + + + ``` + + Replace `VERSION_HERE` by one of [the available versions](https://search.maven.org/artifact/dev.opensavvy.ktmongo/driver-coroutines-jvm). + + After editing the file, refresh the project in your IDE. + +### Adding a serialization library + +MongoDB requires a serialization library, which is responsible for translating your objects to and from the database. +The official driver supports two serialization library. + +=== "KotlinX.Serialization (recommended)" + [KotlinX.Serialization](https://kotlinlang.org/docs/serialization.html) is a first-party library adding compile-time serialization to Kotlin. It is extensively used in the ecosystem, including by Ktor. + + Since the analysis is done at compile-time, it is much faster than reflection-based libraries, and greatly reduces the risk of serialization-born security flaws. Each class we want to serialize must be annotated with `@Serializable`. + + Open your `build.gradle.kts` file and add the KotlinX.Serialization plugin: + ```kotlin + plugins { + kotlin("jvm") version "…" + kotlin("plugin.serialization") version "…" + } + ``` + The KotlinX.Serialization plugin should use the **same** version as the Kotlin plugin itself. + + Following the same steps as previously, add a dependency on: + + - `org.jetbrains.kotlinx:kotlinx-serialization-core` ([available versions](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-serialization-core)) + - `org.mongodb:bson-kotlinx` ([available versions](https://search.maven.org/artifact/org.mongodb/bson-kotlinx)) + +=== "Data class reflection" + MongoDB provides a reflection-based serialization library, that is able to serialize basic Kotlin types (primitives, collections, `data class` instances). + + Following the same steps as previously, add a dependency on: + + - `org.mongodb:bson-kotlin` ([available versions](https://search.maven.org/artifact/org.mongodb/bson-kotlin)) + +=== "Other" + It is possible to add support for any serialization library you prefer, directly through the Kotlin driver's `Codec` system. + + - [Learn more in the Coroutines driver documentation](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/fundamentals/data-formats/codecs/) + - [Learn more in the Synchronous driver documentation](https://www.mongodb.com/docs/languages/kotlin/kotlin-sync-driver/current/data-formats/codecs/) + +## Running MongoDB + +=== "Docker Compose" + Docker is a popular way to encapsulate programs without impacting the user's system. Docker is particularly common to create development environment on the developer's machine. + + If you haven't already, start by [installing Docker](https://docs.docker.com/engine/install/). + + Create a file `docker-compose.yml`: + ```yaml + version: "3.6" + + services: + mongo: + image: "mongo:8.0.0" + ports: + - "27017:27017" + ``` + See the [available versions](https://hub.docker.com/_/mongo/tags). + + Then, run `docker compose up -d` or click the green triangle in IntelliJ. + + **Connection URI: `mongodb://localhost:27017`**. It will be required in the next steps. + +=== "MongoDB Atlas" + MongoDB inc. offers MongoDB Atlas, a hosted MongoDB solution. You can create a MongoDB Atlas instance by following [this tutorial](https://www.mongodb.com/docs/atlas/getting-started/). + + Once you have done so, obtain your **connection URI** by following the steps outlined [here](https://www.mongodb.com/docs/languages/kotlin/kotlin-sync-driver/current/get-started/create-a-connection-string/). It will be required in the next steps. + +=== "Helm" + Helm charts allow easy deployment onto Kubernetes. + + - [Official Helm charts](https://github.com/mongodb/helm-charts) + - [Bitnami Helm charts](https://artifacthub.io/packages/helm/bitnami/mongodb) + +## Make a request + +We can now start writing some code. +Depending on how you created your project, you may already have a sample code file (in `src/main/kotlin`) with a `main` function. If you don't have it, create it. + +### Connecting to the database + +We first need to create a `MongoClient` (low-level representation of the connection between our program and MongoDB) and select a database (namespace of collections): +```kotlin +val client = MongoClient.create("CONNECTION_URI") //(1)! +val database = client.getDatabase("my_first_project") +``` + +1. Replace `CONNECTION_URI` by the connection URI you obtained in the previous step. + +If the database doesn't exist, it will be created automatically. + +### Representing data + +Then, we create a class that represents the schema of the data we want to store: +```kotlin +data class Counter( + val name: String, + val value: Int, +) +``` + +Note that additional configuration may be necessary depending on your serialization library. For example, when using KotlinX.Serialization, you should annotate this class with `@Serializable`. + +With KotlinX.Serialization, collection classes are not necessarily declared as `data class`, regular classes, sealed classes and other Kotlin types can be used as well. + +We can now obtain a collection of that data: +```kotlin +val counters = database.getCollection("counters").asKtMongo() +``` + +Notice the call to `asKtMongo()` which converts the `MongoCollection` from the Kotlin driver into KtMongo's representation. + +If the collection doesn't exist, it will be created automatically. + +### Reading and writing data + +We can now make simple requests to read the data (for example with `find()` or `count()`) or modify it (for example with `updateOne`). + +In this tutorial, we will make an application that prints a new number each time it is run. To do this, we can write the query: +```kotlin +counters.upsertOne( //(1)! + filter = { + Counter::name eq "simple-counter" //(2)! + }, + update = { + Counter::value inc 1 //(3)! + } +) +``` + +1. `upsertOne` finds a document matching the filter and updates it. If no documents are found, a new one is created. +2. We want to upsert a document with a `name` of `"simple-counter"`. We use [Kotlin's property references](https://kotlinlang.org/docs/reflection.html#property-references) to reference a field, and the `eq` infix function to use MongoDB's, `$eq` operator. +3. Similarly as how the filter uses the `eq` infix function to insert an `$eq` operator, we use the `inc` infix function to insert an `$inc` operator. + +If a document already exists with the name `simple-counter`, its value is increased by one. If none exist, a new document is created with the name `simple-counter` and the value 1. + +```kotlin +val counter = counters.find { + Counter::name eq "simple-counter" +} + +println("Current counter: $counter") +``` + +## Final code + +At this stage, your file should look like: +```kotlin +import kotlinx.serialization.Serializable +import com.mongodb.kotlin.client.MongoClient +import opensavvy.ktmongo.coroutines.asKtMongo + +@Serializable +data class Counter( + val name: String, + val value: Int, +) + +suspend fun main() { + val client = MongoClient.create("CONNECTION_URI") + val database = client.getDatabase("my_first_project") + val counters = database.getCollection("counters").asKtMongo() + + counters.upsertOne( + filter = { + Counter::name eq "simple-counter" + }, + update = { + Counter::value inc 1 + } + ) + + val counter = counters.find { + Counter::name eq "simple-counter" + } + + println("Current counter: $counter") +} +``` + +If you run this program, you will see that each time it is run, the counter is incremented once. + +Don't hesitate to edit the requests to try different things. You may also be interested in: + +- Putting a breakpoint in the `filter` or `update` lambdas: notice how the debugger shows the current JSON for the request. +- The JSON representation can also be obtained by inserting `println(this)` in any lambda of the DSL. +- Notice how all operations and operators have an example of usage, and link to the MongoDB website, if you look at their documentation (CTRL Q by default in IntelliJ). + +??? failure "Troubleshooting: no documentation in IntelliJ" + In `File | Settings | Advanced Settings | Build Tools. Gradle`, enable "Download sources" and re-sync the project. diff --git a/docs/website/mkdocs.yml b/docs/website/mkdocs.yml index ad33f382..838f1414 100644 --- a/docs/website/mkdocs.yml +++ b/docs/website/mkdocs.yml @@ -85,7 +85,8 @@ use_directory_urls: false nav: - Home: index.md - - Getting started: [] + - Getting started: + - tutorials/index.md - Features: [] -- 2.51.2