Micronaut with MongoDB and Kotlin

1. Introduction

2. Run MongoDB Server

2.1. Deploy MongoDB as a Container

docker pull mongo
docker run -d -p 27017-27019:27017-27019 --name mongodb mongo
docker ps

#command output
CONTAINER ID IMAGE COMMAND
26bd5c8dcc79 mongo "docker-entrypoint.s…"
CREATED STATUS 6 seconds ago Up 5 seconds PORTS NAMES
0.0.0.0:27017-27019->27017-27019/tcp mongodb

3. Set Up The Project

3.1. Generate The Project

3.2. Add The No-arg Compiler Plugin

plugins {
id("org.jetbrains.kotlin.plugin.noarg") version "1.4.10"
}

3.3. Configure application.yml

micronaut:
application:
name: example
mongodb:
uri: 'mongodb://localhost:27017/example'

4. Create @NoArg annotation

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class NoArg
noArg {
annotation("com.codersee.annotation.NoArg")
}

5. Create Models

@NoArg
data class Company(
var id: ObjectId? = null,
var name: String,
var address: String
)
@NoArg
data class Employee(
var id: ObjectId? = null,
var firstName: String,
var lastName: String,
var email: String,
var company: Company?,
)

5.1. What is ObjectId?

6. Add Custom Exception With Status Code

class NotFoundException(msg: String) : RuntimeException(msg)class ExceptionResponse(
val message: String?
)
@Produces
@Singleton
class NotFoundExceptionHandler :
ExceptionHandler<NotFoundException, HttpResponse<ExceptionResponse>> {

override fun handle(
request: HttpRequest<*>,
exception: NotFoundException
): HttpResponse<ExceptionResponse> =
HttpResponse.notFound(
ExceptionResponse(
message = exception.message
)
)

}

6.1. What is @Singleton and @Produces?

7. Create Repositories

7.1. Implement CompanyRepository

@Singleton
class CompanyRepository(
private val mongoClient: MongoClient
) {

fun create(company: Company): InsertOneResult =
getCollection()
.insertOne(company)

fun findAll(): List<Company> =
getCollection()
.find()
.toList()

fun findById(id: String): Company? =
getCollection()
.find(
Filters.eq("_id", ObjectId(id))
)
.toList()
.firstOrNull()

fun update(id: String, update: Company): UpdateResult =
getCollection()
.replaceOne(
Filters.eq("_id", ObjectId(id)),
update
)

fun deleteById(id: String): DeleteResult =
getCollection()
.deleteOne(
Filters.eq("_id", ObjectId(id))
)

private fun getCollection() =
mongoClient
.getDatabase("example")
.getCollection("company", Company::class.java)
}
@Singleton
class CompanyRepository(
private val mongoClient: MongoClient
)
private fun getCollection() =
mongoClient
.getDatabase("example")
.getCollection("company", Company::class.java)
fun findAll(): List<Company> =
getCollection()
.find()
.toList()

fun findById(id: String): Company? =
getCollection()
.find(
Filters.eq("_id", ObjectId(id))
)
.toList()
.firstOrNull()
fun create(company: Company): InsertOneResult =
getCollection()
.insertOne(company)

fun update(id: String, update: Company): UpdateResult =
getCollection()
.replaceOne(
Filters.eq("_id", ObjectId(id)),
update
)

fun deleteById(id: String): DeleteResult =
getCollection()
.deleteOne(
Filters.eq("_id", ObjectId(id))
)
}

7.2. Implement EmployeeRepository

@Singleton
class EmployeeRepository(
private val mongoClient: MongoClient
) {

fun create(employee: Employee): InsertOneResult =
getCollection()
.insertOne(employee)

fun findAll(): List =
getCollection()
.find()
.toList()

fun findAllByCompanyId(companyId: String): List<Employee> =
getCollection()
.find(
Filters.eq("company._id", ObjectId(companyId))
)
.toList()


fun findById(id: String): Employee? =
getCollection()
.find(
Filters.eq("_id", ObjectId(id))
)
.toList()
.firstOrNull()

fun update(id: String, update: Employee): UpdateResult =
getCollection()
.replaceOne(
Filters.eq("_id", ObjectId(id)),
update
)

fun deleteById(id: String): DeleteResult =
getCollection()
.deleteOne(
Filters.eq("_id", ObjectId(id))
)

private fun getCollection() =
mongoClient
.getDatabase("example")
.getCollection("employee", Employee::class.java)
}

8. Create Services

8.1. Implement CompanyService

class CompanyRequest(
val name: String,
val address: String
)
@Singleton
class CompanyService(
private val companyRepository: CompanyRepository,
private val employeeRepository: EmployeeRepository
) {

fun createCompany(request: CompanyRequest): BsonValue? {
val insertedCompany = companyRepository.create(
Company(
name = request.name,
address = request.address
)
)
return insertedCompany.insertedId
}

fun findAll(): List<Company> {
return companyRepository.findAll()
}

fun findById(id: String): Company {
return companyRepository.findById(id)
?: throw NotFoundException("Company with id $id was not found")
}

fun updateCompany(id: String, request: CompanyRequest): Company {
val updateResult = companyRepository.update(
id = id,
update = Company(name = request.name, address = request.address)
)

if (updateResult.modifiedCount == 0L)
throw throw RuntimeException("Company with id $id was not updated")

val updatedCompany = findById(id)
updateCompanyEmployees(updatedCompany)

return updatedCompany
}

fun deleteById(id: String) {
val deleteResult = companyRepository.deleteById(id)

if (deleteResult.deletedCount == 0L)
throw throw RuntimeException("Company with id $id was not deleted")
}

private fun updateCompanyEmployees(updatedCompany: Company) {
employeeRepository
.findAllByCompanyId(updatedCompany.id!!.toHexString())
.map {
employeeRepository.update(
it.id!!.toHexString(),
it.apply { it.company = updatedCompany }
)
}
}
}
{
"firstName": "Piotr",
"lastName": "Wolak",
"email": "contact@codersee.com",
"company": {
"_id": {
"$oid": "5fcb90f830e3af4497f5de14"
},
"name": "Company Two",
"company_address": "Address Two"
},
"_class": "com.codersee.mongocrud.model.Employee"
}

8.2. Implement EmployeeService

class EmployeeRequest(
val firstName: String,
val lastName: String,
val email: String,
val companyId: String?
)
@Singleton
class EmployeeService(
private val employeeRepository: EmployeeRepository,
private val companyService: CompanyService

) {

fun createEmployee(request: EmployeeRequest): BsonValue? {
val company = request.companyId?.let { companyService.findById(it) }

val insertedEmployee = employeeRepository.create(
Employee(
firstName = request.firstName,
lastName = request.lastName,
email = request.email,
company = company
)
)
return insertedEmployee.insertedId
}

fun findAll(): List<Employee> =
employeeRepository.findAll()

fun findById(id: String): Employee =
employeeRepository.findById(id)
?: throw NotFoundException("Employee with id $id not found")

fun updateEmployee(id: String, request: EmployeeRequest): Employee {
val foundCompany = request.companyId?.let { companyService.findById(it) }

val updateResult = employeeRepository.update(
id = id,
update = Employee(
firstName = request.firstName,
lastName = request.lastName,
email = request.email,
company = foundCompany
)
)

if (updateResult.modifiedCount == 0L)
throw throw RuntimeException("Employee with id $id was not updated")

return findById(id)
}

fun deleteById(id: String) {
val deleteResult = employeeRepository.deleteById(id)

if (deleteResult.deletedCount == 0L)
throw throw RuntimeException("Company with id $id was not deleted")
}
}

9. Implement REST Controllers

class CompanyResponse(
val id: String,
val name: String,
val address: String
) {
companion object {
fun fromEntity(company: Company): CompanyResponse =
CompanyResponse(
id = company.id!!,
name = company.name,
address = company.address
)
}
}
class EmployeeResponse(
val id: String,
val firstName: String,
val lastName: String,
val email: String,
val company: CompanyResponse?
) {
companion object {
fun fromEntity(employee: Employee): EmployeeResponse =
EmployeeResponse(
id = employee.id!!.toHexString(),
firstName = employee.firstName,
lastName = employee.lastName,
email = employee.email,
company = employee.company?.let { CompanyResponse.fromEntity(it) }
)
}
}

9.1. Create CompanyController

@Controller("/api/company")
class CompanyController(
private val companyService: CompanyService
) {

@Post
fun create(@Body request: CompanyRequest): HttpResponse<Void> {
val createdId = companyService.createCompany(request)

return HttpResponse.created(
URI.create(
createdId!!.asObjectId().value.toHexString()
)
)
}

@Get
fun findAll(): HttpResponse<List<CompanyResponse>> {
val companies = companyService
.findAll()
.map { CompanyResponse.fromEntity(it) }

return HttpResponse.ok(companies)
}

@Get("/{id}")
fun findById(@PathVariable id: String): HttpResponse<CompanyResponse> {
val company = companyService.findById(id)

return HttpResponse.ok(
CompanyResponse.fromEntity(company)
)
}

@Put("/{id}")
fun update(
@PathVariable id: String,
@Body request: CompanyRequest
): HttpResponse<CompanyResponse> {
val updatedCompany = companyService.updateCompany(id, request)

return HttpResponse.ok(
CompanyResponse.fromEntity(updatedCompany)
)
}

@Delete("/{id}")
fun deleteById(@PathVariable id: String): HttpResponse<Void> {
companyService.deleteById(id)

return HttpResponse.noContent()
}
}

9.2. Create EmployeeController

@Controller("/api/employee")
class EmployeeController(
private val employeeService: EmployeeService
) {

@Post
fun create(@Body request: EmployeeRequest): HttpResponse {
val createdId = employeeService.createEmployee(request)

return HttpResponse.created(
URI.create(
createdId!!.asObjectId().value.toHexString()
)
)
}

@Get
fun findAll(): HttpResponse<List> {
val employees = employeeService
.findAll()
.map { EmployeeResponse.fromEntity(it) }

return HttpResponse.ok(employees)
}

@Get("/{id}")
fun findById(@PathVariable id: String): HttpResponse {
val employee = employeeService.findById(id)

return HttpResponse.ok(
EmployeeResponse.fromEntity(employee)
)
}

@Put("/{id}")
fun update(
@PathVariable id: String,
@Body request: EmployeeRequest
): HttpResponse {
val updatedEmployee = employeeService.updateEmployee(id, request)

return HttpResponse.ok(
EmployeeResponse.fromEntity(updatedEmployee)
)
}

@Delete("/{id}")
fun deleteById(@PathVariable id: String): HttpResponse {
employeeService.deleteById(id)

return HttpResponse.noContent()
}
}

10. Test with CURL

# 1st company
curl --location --request POST 'localhost:8080/api/company' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Company 1",
"address": "Address 1"
}'

# 2nd company
curl --location --request POST 'localhost:8080/api/company' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Company 2",
"address": "Address 2"
}'
curl --location --request GET 'localhost:8080/api/company'

# Example output
[
{
"id": "5fcba07a30e3af4497f5de16",
"name": "Company 1",
"address": "Address 1"
},
{
"id": "5fcba07c30e3af4497f5de17",
"name": "Company 2",
"address": "Address 2"
}
]
curl --location --request GET 'localhost:8080/api/company/5fcba07c30e3af4497f5de17'

# Expected output
{
"id": "5fcba07c30e3af4497f5de17",
"name": "Company 2",
"address": "Address 2"
}
curl --location --request PUT 'localhost:8080/api/company/5fcba07c30e3af4497f5de17' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Company 2 Updated",
"address": "Address 2 Updated"
}'
curl --location --request DELETE 'localhost:8080/api/company/5fcba07c30e3af4497f5de17'

11. Summary

Hi, my name is Piotr and I am the founder of Codersee- a technical blog, where I am teaching programming by practical step by step tutorials.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store