javascript
操作方法:具有多个Mongo存储库和Kotlin的Spring Boot 2 Web应用程序
首先,免責聲明:如果您正在編寫微服務 (每個人現在都對嗎?)并希望它是慣用的 ,那么通常不會在其中使用幾個不同的數據源。
圖片取自Pixabay? https: //pixabay.com/illustrations/software-binary-system-1-0-binary-557616/為什么? 好吧,按照定義,微服務應該松散耦合,以便它們可以獨立。 將多個微服務寫入同一個數據庫確實違反了這一原則,因為這意味著您的數據可以由幾個獨立的參與者以可能以不同的方式進行更改 ,這使得談論數據一致性確實非常困難,而且,您很難說這些服務是獨立的,因為它們至少具有它們共同依賴的一件事:共享(并且可能是固定的)數據。 因此,有一種稱為數據庫每個服務的設計模式,旨在通過對每個數據庫實施一個服務來解決此問題。 這意味著每個微服務都充當客戶端與其數據源之間的中介,并且只能通過該服務提供的接口來更改數據 。
但是,每個數據庫一項服務等于一個服務一項數據庫嗎? 不,不是。 如果您考慮一下,那并不是一回事。
這意味著,如果我們有幾個只能由一個微服務訪問的數據庫,并且通過該服務的接口實現了對這些數據庫的任何外部訪問,那么仍然可以認為該服務是慣用的。 它仍然是每個數據庫一項服務,盡管不是每個服務一項數據庫。
另外,也許您根本不關心微服務的慣用性。 這也是一個選擇。 (不過這將取決于您的良心。)
那么,何時會有幾個數據庫要從同一服務訪問? 我可以想到不同的選擇:
- 數據太大,無法存放在一個數據庫中。
- 您將數據庫用作命名空間,以僅分隔屬于不同域或功能區域的不同數據。
- 您需要對數據庫的不同訪問權限-也許其中一個是關鍵任務,因此您將其置于各種安全層的后面,而另一個則不是那么重要,也不需要這種保護。
- 這些數據庫位于不同的區域,因為它們是由不同地方的人寫入的,但需要從中央位置讀取(反之亦然);
- 真的,所有其他一切都導致了這種情況,您只需要忍受它。
如果您的應用程序是Spring Boot應用程序,并且您將Mongo用作數據庫,那么最簡單的方法就是使用Spring Data Repositories 。 您只需為mongo入門數據設置依賴項(我們將在此處以Gradle項目為例)。
dependencies {implementation("org.springframework.boot:spring-boot-starter-data-mongodb")implementation("org.springframework.boot:spring-boot-starter-web")implementation("com.fasterxml.jackson.module:jackson-module-kotlin")implementation("org.jetbrains.kotlin:kotlin-reflect")implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")testImplementation("org.springframework.boot:spring-boot-starter-test") }實際上,我們是使用Spring Initializer生成此示例項目的,因為這是開始基于Spring的新示例的最簡單方法。 我們剛剛在生成器設置中選擇了Kotlin和Gradle,并添加了Spring Web Starter和Spring Data MongoDB作為依賴項。 我們稱這個項目為multimongo。
當我們創建一個項目并下載源代碼時,我們可以看到Spring默認情況下創建了一個application.properties文件。 我更喜歡yaml ,所以我們將其重命名為application.yml并完成它。
所以。 我們如何使用Spring Data設置對默認mongo數據庫的訪問權限? 沒什么容易的。 這就是application.yml 。
# possible MongoProperties # spring.data.mongodb.authentication-database= # Authentication database name. # spring.data.mongodb.database= # Database name. # spring.data.mongodb.field-naming-strategy= # Fully qualified name of the FieldNamingStrategy to use. # spring.data.mongodb.grid-fs-database= # GridFS database name. # spring.data.mongodb.host= # Mongo server host. Cannot be set with URI. # spring.data.mongodb.password= # Login password of the mongo server. Cannot be set with URI. # spring.data.mongodb.port= # Mongo server port. Cannot be set with URI. # spring.data.mongodb.repositories.type=auto # Type of Mongo repositories to enable. # spring.data.mongodb.uri=mongodb://localhost/test # Mongo database URI. Cannot be set with host, port and credentials. # spring.data.mongodb.username= # Login user of the mongo server. Cannot be set with URI.spring:data:mongodb:uri: mongodb://localhost:27017database: multimongo-core現在,讓我們想象一下一個非常簡單而愚蠢的數據拆分案例。 假設我們有一個core數據庫,用于存儲我們的網上商店的產品。 然后,我們獲得了有關產品價格的數據; 此數據不需要任何訪問限制,因為網絡上的任何用戶都可以看到價格,因此我們將其稱為external 。 但是,我們還有價格歷史記錄,可用于分析目的。 這是有限的訪問信息,所以我們說,好的,它進入一個單獨的數據庫,我們將對其進行保護并調用internal 。
顯然,就我而言,所有這些都仍在localhost上,并且不受保護,但是請允許我,這只是一個示例。
# Predefined spring data properties don't help us anymore. # Therefore, we're creating our own configuration for the additional mongo instances.additional-db:internal:uri: mongodb://localhost:27017database: multimongo-internalexternal:uri: mongodb://localhost:27017database: multimongo-external我們還將創建三個不同的目錄,以將與數據訪問相關的代碼保留在其中: data.core , data.external和data.internal 。
我們的Product.kt保留產品的實體和存儲庫, ProductPrice.kt和ProductPriceHistory.kt代表產品的當前價格和歷史價格。 實體和存儲庫非常基礎。
@Document data class Product(@Idval id: String? = null,val sku: String,val name: String )interface ProductRepository : MongoRepository<Product, String>@Document(collection = "productPrice") data class ProductPrice(@Idval id: String? = null,val sku: String,val price: Double )interface ProductPriceRepository : MongoRepository<ProductPrice, String>@Document(collection = "priceHistory") data class PriceHistory(@Idval id: String? = null,val sku: String,val prices: MutableList<PriceEntry> = mutableListOf() )data class PriceEntry(val price: Double,val expired: Date? = null )interface PriceHistoryRepository : MongoRepository<PriceHistory, String>現在,讓我們為default mongo創建配置。
@Configuration @EnableMongoRepositories(basePackages = ["com.example.multimongo.data.core"]) @Import(value = [MongoAutoConfiguration::class]) class CoreMongoConfiguration {@Beanfun mongoTemplate(mongoDbFactory: MongoDbFactory): MongoTemplate {return MongoTemplate(mongoDbFactory)} }我們在這里使用MongoAutoConfiguration類創建默認的mongo客戶端實例。 但是,我們仍然需要一個明確定義的MongoTemplate bean。
如您所見, core配置僅掃描core目錄。 這實際上是一切的關鍵:我們需要將我們的存儲庫放在不同的目錄中,并且這些存儲庫將由不同的mongo模板進行掃描。 因此,讓我們創建那些附加的mongo模板。 我們將使用一個基類,該基類將保留一些共享功能,我們將重復使用這些功能來創建mongo客戶端。
@Configuration class ExtraMongoConfiguration {val uri: String? = nullval host: String? = nullval port: Int? = 0val database: String? = null/*** Method that creates MongoClient*/private val mongoClient: MongoClientget() {if (uri != null && !uri.isNullOrEmpty()) {return MongoClient(MongoClientURI(uri!!))}return MongoClient(host!!, port!!)}/*** Factory method to create the MongoTemplate*/protected fun mongoTemplate(): MongoTemplate {val factory = SimpleMongoDbFactory(mongoClient, database!!)return MongoTemplate(factory)} }然后,最后,我們創建兩個配置以容納external和internal數據庫的mongo模板實例。
@EnableMongoRepositories(basePackages = ["com.example.multimongo.data.external"],mongoTemplateRef = "externalMongoTemplate") @Configuration class ExternalDatabaseConfiguration : ExtraMongoConfiguration() {@Value("\${additional-db.external.uri:}")override val uri: String? = null@Value("\${additional-db.external.host:}")override val host: String? = null@Value("\${additional-db.external.port:0}")override val port: Int? = 0@Value("\${additional-db.external.database:}")override val database: String? = null@Bean("externalMongoTemplate")fun externalMongoTemplate(): MongoTemplate = mongoTemplate() }@EnableMongoRepositories(basePackages = ["com.example.multimongo.data.internal"],mongoTemplateRef = "internalMongoTemplate") @Configuration class InternalDatabaseConfiguration : ExtraMongoConfiguration() {@Value("\${additional-db.internal.uri:}")override val uri: String? = null@Value("\${additional-db.internal.host:}")override val host: String? = null@Value("\${additional-db.internal.port:0}")override val port: Int? = 0@Value("\${additional-db.internal.database:}")override val database: String? = null@Bean("internalMongoTemplate")fun internalMongoTemplate(): MongoTemplate = mongoTemplate() }因此,我們現在有三個mongo模板bean,它們由mongoTemplate() , externalMongoTemplate()和internalMongoTemplate()在三種不同的配置中創建。 這些配置掃描不同的目錄,并通過@EnableMongoRepositories批注中的直接引用使用這些不同的mongo模板@EnableMongoRepositories這意味著它們將使用創建的bean。 春天沒有問題。 依存關系將以正確的順序解決。
那么,我們如何檢查一切正常? 還有一個步驟需要完成:我們需要初始化一些數據,然后從數據庫中獲取數據。
由于這只是一個示例,因此我們將在應用程序啟動時立即創建一些非?;镜臄祿?#xff0c;以確保它們在那里。 我們將為此使用ApplicationListener 。
@Component class DataInitializer(val productRepo: ProductRepository,val priceRepo: ProductPriceRepository,val priceHistoryRepo: PriceHistoryRepository ) : ApplicationListener<ContextStartedEvent> {override fun onApplicationEvent(event: ContextStartedEvent) {// clean upproductRepo.deleteAll()priceRepo.deleteAll()priceHistoryRepo.deleteAll()val p1 = productRepo.save(Product(sku = "123", name = "Toy Horse"))val p2 = productRepo.save(Product(sku = "456", name = "Real Horse"))val h1 = PriceHistory(sku = p1.sku)val h2 = PriceHistory(sku = p2.sku)for (i in 5 downTo 1) {if (i == 5) {// current pricepriceRepo.save(ProductPrice(sku = p1.sku, price = i.toDouble()))priceRepo.save(ProductPrice(sku = p2.sku, price = (i * 2).toDouble()))// current price historyh1.prices.add(PriceEntry(price = i.toDouble()))h2.prices.add(PriceEntry(price = (i * 2).toDouble()))} else {// previous priceval expiredDate = Date(ZonedDateTime.now().minusMonths(i.toLong()).toInstant().toEpochMilli())h1.prices.add(PriceEntry(price = i.toDouble(), expired = expiredDate))h2.prices.add(PriceEntry(price = (i * 2).toDouble(), expired = expiredDate))}}priceHistoryRepo.saveAll(listOf(h1, h2))} }我們如何檢查數據是否已保存到數據庫? 由于它是一個Web應用程序,因此我們將在REST控制器中公開數據。
@RestController @RequestMapping("/api") class ProductResource(val productRepo: ProductRepository,val priceRepo: ProductPriceRepository,val priceHistoryRepo: PriceHistoryRepository ) {@GetMapping("/product")fun getProducts(): List<Product> = productRepo.findAll()@GetMapping("/price")fun getPrices(): List<ProductPrice> = priceRepo.findAll()@GetMapping("/priceHistory")fun getPricesHistory(): List<PriceHistory> = priceHistoryRepo.findAll() }REST控制器只是使用我們的存儲庫來調用findAll()方法。 我們沒有對數據轉換做任何事情,我們沒有分頁或排序,我們只是想看看有什么東西。 最后,可以啟動應用程序,然后看看會發生什么。
[{"id": "5d5e64d80a986d381a8af4ce","name": "Toy Horse","sku": "123"},{"id": "5d5e64d80a986d381a8af4cf","name": "Real Horse","sku": "456"} ]是的,我們創建了兩個產品! 我們可以看到Mongo在保存時為其分配了自動生成的ID,我們僅定義了名稱和偽SKU代碼。
我們還可以在http:// localhost:8080 / api / price和http:// localhost:8080 / api / priceHistory上檢查數據 ,并確保是的,實際上,這些實體也確實已創建。 我不會在此處粘貼此JSON,因為它并不相關。
但是,我們如何確保數據確實已保存到其他數據庫(或從中讀取)? 為此,我們可以使用任何允許我們連接到本地mongo實例的mongo客戶端應用程序(我正在使用mongo的官方工具-MongoDB Compass )。
讓我們檢查保持當前價格的數據庫中的內容。
如果我們想做對的事情(實際上不是所有的事,我們也可以使用集成測試來檢查數據,而不是手動處理)(實際上不是所有的事情;我們需要使用嵌入式mongo數據庫進行測試,但是這里我們將跳過這一部分)不會使教程太復雜)。 為此 ,我們將利用spring-test庫中的MockMvc 。
<
@RunWith(SpringRunner::class) @SpringBootTest class MultimongoApplicationTests {@Autowiredprivate val productRepo: ProductRepository? = null@Autowiredprivate val priceRepo: ProductPriceRepository? = null@Autowiredprivate val priceHistoryRepo: PriceHistoryRepository? = null@Autowiredprivate val initializer: DataInitializer? = null@Autowiredprivate val context: ApplicationContext? = nullprivate var mvc: MockMvc? = null@Beforefun setUp() {val resource = ProductResource(productRepo!!,priceRepo!!,priceHistoryRepo!!)this.mvc = MockMvcBuilders.standaloneSetup(resource).build()initializer!!.onApplicationEvent(ContextStartedEvent(context!!))}@Testfun productsCreated() {mvc!!.perform(get(“/api/product”)).andExpect(status().isOk).andDo {println(it.response.contentAsString)}.andExpect(jsonPath(“$.[*].sku”).isArray).andExpect(jsonPath(“$.[*].sku”).value(hasItems(“123”, “456”)))}@Testfun pricesCreated() {mvc!!.perform(get(“/api/price”)).andExpect(status().isOk).andDo {println(it.response.contentAsString)}.andExpect(jsonPath(“$.[*].sku”).isArray).andExpect(jsonPath(“$.[*].sku”).value(hasItems(“123”, “456”))).andExpect(jsonPath(“$.[0].price”).value(5.0)).andExpect(jsonPath(“$.[1].price”).value(10.0))}@Testfun pricesHistoryCreated() {mvc!!.perform(get(“/api/priceHistory”)).andExpect(status().isOk).andDo {println(it.response.contentAsString)}.andExpect(jsonPath(“$.[*].sku”).isArray).andExpect(jsonPath(“$.[*].sku”).value(hasItems(“123”, “456”))).andExpect(jsonPath(“$.[0].prices.[*].price”).value(hasItems(5.0, 4.0, 3.0, 2.0, 1.0))).andExpect(jsonPath(“$.[1].prices.[*].price”).value(hasItems(10.0, 8.0, 6.0, 4.0, 2.0)))} }你可以找到完整的工作示例這里在我的github回購。 希望這可以幫助您解決在一個Spring Boot Web應用程序中使用多個mongo實例的問題! 這不是一個難題,但也不是一件容易的事。
當我在網上查看其他示例時,我還閱讀了這篇文章 (Azadi Bogolubov 撰寫的 “ Spring Data Configuration:Multiple Mongo Databases” ),它相當不錯而且很全面。 但是,它不太適合我的情況,因為它完全覆蓋了自動mongo配置。 另一方面,我仍然希望將其保留在我的默認數據庫中,而不是其他數據庫。 但是該文章中的方法基于相同的原理,即使用不同的mongo模板掃描不同的存儲庫 。
只是,使用默認配置,例如,一旦發生某些更改并且所有數據再次進入同一數據庫,您就可以輕松擺脫多余的類。
然后,您可以輕松清除非默認配置,但仍保留默認配置,僅更改其掃描范圍。 該應用程序仍將繼續正常運行。 但是這兩種方式都是完全有效的 。
本文也在此處的 Medium中發布。
翻譯自: https://www.javacodegeeks.com/2019/09/spring-application-multiple-mongo-repositories-kotlin.html
總結
以上是生活随笔為你收集整理的操作方法:具有多个Mongo存储库和Kotlin的Spring Boot 2 Web应用程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全民奇迹安卓苹果互通吗(全民奇迹安卓)
- 下一篇: war解压缩(war解压 linux)