首先,免责声明:如果您正在编写微服务 (每个人现在都对吗?)并希望它是惯用的 ,那么通常不会在其中使用几个不同的数据源。
为什么? 好吧,按照定义,微服务应该松散耦合,以便它们可以独立。 将多个微服务写入同一个数据库确实违反了这一原则,因为这意味着您的数据可以由几个独立的参与者以可能以不同的方式进行更改 ,这使得谈论数据一致性确实非常困难,而且,您很难说这些服务是独立的,因为它们至少具有它们共同依赖的一件事:共享(并且可能是固定的)数据。 因此,有一种称为数据库每个服务的设计模式,旨在通过对每个数据库实施一个服务来解决此问题。 这意味着每个微服务都充当客户端与其数据源之间的中介,并且只能通过该服务提供的接口来更改数据 。
但是,每个数据库一项服务等于一个服务一项数据库吗? 不,不是。 如果您考虑一下,那并不是一回事。
这意味着,如果我们有几个只能由一个微服务访问的数据库,并且通过该服务的接口实现了对这些数据库的任何外部访问,那么仍然可以认为该服务是惯用的。 它仍然是每个数据库一项服务,尽管不是每个服务一项数据库。
另外,也许您根本不关心微服务的惯用性。 这也是一个选择。 (不过这将取决于您的良心。)
那么,何时会有几个数据库要从同一服务访问? 我可以想到不同的选择:
- 数据太大,无法存放在一个数据库中。
- 您将数据库用作命名空间,以仅分隔属于不同域或功能区域的不同数据。
- 您需要对数据库的不同访问权限-也许其中一个是关键任务,因此您将其置于各种安全层的后面,而另一个则不是那么重要,也不需要这种保护。
- 这些数据库位于不同的区域,因为它们是由不同地方的人写入的,但需要从中央位置读取(反之亦然);
- 真的,所有其他一切都导致了这种情况,您只需要忍受它。
如果您的应用程序是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。 春天没有问题。 依存关系将以正确的顺序解决。
那么,我们如何检查一切正常? 还有一个步骤需要完成:我们需要初始化一些数据,然后从数据库中获取数据。
由于这只是一个示例,因此我们将在应用程序启动时立即创建一些非常基本的数据,以确保它们在那里。 我们将为此使用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