MongoDB在可以存储的内容方面确实很灵活。我们不必像关系DB一样与特定的模式联系在一起。随着时间的变化,Mongo适应它并不重要。从域的角度而不是从数据角度来看,它使设计应用程序更容易。考虑到这一点,我们想通过代码在我们的表中存储不同的结构。官方Micronaut指南是一个很好的起点,但是我花了一段时间才学习如何存储在包含其他物体的Mongo物体中。这是解决方案。
基础
在这种情况下,我将扩展我在integration testing上的上一篇文章中介绍的一些应用程序。我想创建词典结构,该字典结构将以一种语言和翻译的方式保持单词。为了实现这一点,我已经准备了遵循结构:
data class Word(val word: String, val translations: List<Translation>)
data class Translation(val language: String, val translation: String)
如果我们想将其存储在关系数据库中,默认情况下,它将需要两个表 - 一个用于Word
行,另一个用于Translation
行,则参考特定的Word
。默认情况下,Mongo允许我们在一个表中使用两个对象。它将包含一个Word
,以及JSON格式的Translation
列表作为单独的字段。
就蒙哥而言,上述设置很容易实现。我们可以使用两个字段来创建表word
和translations
,其中第一个将是字符串值,后者将是包含具有language
和translation
字段的对象列表的JSON。
核
Micronaut带有名为micronaut-serde-processor
的专用插件,使我们能够 ser ialize和 de 序列化。我们可以用@Serdeable
注释注释类,以将其标记为将来将来交换的类。由于我们不使用micronaut-data
,这可以使事情变得更容易(但我无法实现此类嵌套序列化),我们将需要依靠手动poinitng如何序列化Mongo使用的BSON
字段。要启用这样的课程,我们还需要添加@Introspected
注释。
如前所述,我们将不得不指出如何转换我们的实体。最简单的方法是通过构造函数做到这一点。为了使其正常工作,我们需要使用@Creator
和@BsonCreator
注释来标记构造函数。我们的实体将通过构造函数转换,其中包含所有必需的字段。为了进行适当的转换,我们还需要展示将考虑哪些字段。每个人都需要通过@field:BsonProperty("name")
和@param:BsonProperty("name")
注释来注释。这是将属性标记为类(field
)和构造函数(param
)属性。拥有这样的课程,我们不必担心默认情况下的播放器和Getter的声明是序列化过程。我们的课程看起来像:
- mongoword
@Introspected
@Serdeable
data class MongoWord @Creator @BsonCreator constructor(
@field:BsonProperty("word") @param:BsonProperty("word") val word: String,
@field:BsonProperty("translations") @param:BsonProperty("translations") val translations: List<MongoTranslation>
)
- mongotranslation
@Introspected
@Serdeable
data class MongoTranslation @Creator @BsonCreator constructor(
@field:BsonProperty("language") @param:BsonProperty("language") val language: String,
@field:BsonProperty("translation") @param:BsonProperty("translation") val translation: String
)
域和实体的分离
将我们域逻辑中使用的类中使用的类与用于与外部世界进行交流的类是一个好实践。我喜欢快速转换各种方式的能力。通过静态工厂方法。在Kotlin中,我们可以使用 companion对象实现这一目标。这样的对象看起来像我们的 word 类:
companion object {
fun fromWord(word: Word): MongoWord {
return MongoWord(word.word, word.translations.map { MongoTranslation.fromTranslation(it) })
}
}
当我们要直接从实体中创建域对象时,我们可以使用在实例上执行的方法
fun toWord(): Word {
return Word(word, translations.map { it.toTranslation() })
}
在运输类中使用此方法将使我们可以从域 Word 和 translation 对象隐藏实现详细信息。多亏了这一点,我们可以专注于实际的业务逻辑,而无需考虑如何序列化和应对序列化。
存储库
准备所有准备都别无其他。这将是一个单身人士,它将接受并返回 word 对象。作为用来构建它的字段,我们需要MongoClient
以及我们将要操作的数据库和收集名称。然后,我们要做的就是实施负责从存储库中存储和获取单词的方法。以下是表示我们如何实现这一目标的代码。
@Singleton
class WordRepositoryMongo(
mongoClient: MongoClient,
@Property(name = "word.database") databaseName: String,
@Property(name = "word.collection") collectionName: String
) : WordRepository {
private val collection: MongoCollection<MongoWord>
init {
val db = mongoClient.getDatabase(databaseName)
collection = db.getCollection(collectionName, MongoWord::class.java)
}
override fun findWord(word: String): Word? {
return collection.find(eq("word", word)).firstOrNull()?.toWord()
}
override fun putWord(word: Word) {
collection.insertOne(MongoWord.fromWord(word))
}
}
测试
Testcontainers
是真正功能强大的工具,它使我们能够测试刚刚编写的所有代码针对实际的mongoDB实例。多亏了Micronaut io.micronaut.test-resources
插件,我们唯一需要做的就是提供对Testcontainers
的依赖性,并且一切都会插入开箱即用。无需配置。在编写测试之前,我们需要确保每次执行DB状态将被清除。为此,我们可以按照以下操作:
@BeforeEach
fun beforeEach() {
mongoClient.getDatabase(databaseName)
.getCollection(collectionName)
.deleteMany(Document())
}
它使用注射的MongoClient
,就像我们在WordRepositoryMongo
类中使用的那样。从宣布为班级字段的集合中,我们将删除所有现有文档。准备好后,我们可以执行样本测试。
@Test
fun shouldStoreWordInRepository() {
//Given
val word = Word(
"hello", listOf(
Translation("polish", "czesc"),
Translation("deutsch", "hallo")
)
)
//When
repository.putWord(word)
//Then
val wordFromRepository = repository.findWord("hello")
Assertions.assertTrue(wordFromRepository != null)
Assertions.assertTrue(wordFromRepository!!.translations.size == 2)
Assertions.assertTrue(wordFromRepository!!.translations
.filter { it.language == "polish" && it.translation == "czesc" }
.size == 1)
Assertions.assertTrue(wordFromRepository!!.translations
.filter { it.language == "deutsch" && it.translation == "hallo" }
.size == 1)
}
它测试是否可以稍后联系。
结论
对于我来说,找到如何将对象结构存储为Mongo表的参数并不是一件容易的事。 Micronaut仍然不像Spring那样受欢迎,因此社区支持还没有那么活跃。我希望这篇文章可以帮助您设计能够实现域潜力的桌子,而无需考虑配置nitpicks。
本文中使用的所有代码您可以在hello
包中找到here。