【问题标题】:Can I use JPA by specifing the database during runtime?我可以通过在运行时指定数据库来使用 JPA 吗?
【发布时间】:2019-01-24 15:03:19
【问题描述】:

我在生产服务器上使用 MySQL,每个客户都有自己的数据库和自己的数据库用户来连接到他的数据库。每个数据库都具有完全相同的结构。在服务器的生命周期中添加客户(因此也包括数据库)。

目前我与java.sql.DriverManager 类建立了一个 MySQL 数据库连接,它工作得很好。数据库 url、用户、密码和驱动程序名称存储在几个属性文件中。 servlet 从当前客户读取属性文件并创建java.sql.Connectionjava.sql.DriverManager

我真的很想使用 JPA,但据我所知,JPA 只能通过在 web 容器(在我的例子中是 Tomcat)的配置中明确指定数据库来使用。但这意味着,当新客户在我的平台上注册时,我必须将新数据库添加到 webcontainer-configuration 并重新启动 webcontainer,这听起来不是一个好主意。

有没有一种方法可以使用 JPA 并且在 Web 容器的生命周期内仍然可以灵活地添加新数据库?

【问题讨论】:

  • 与您的问题无关,但您不应在 Web 应用程序中使用 DriverManager。请改用数据源(带有连接池)。
  • 但据我了解,当我使用 JNDI 时,我遇到了与上述相同的问题。我必须在 webcontainer 的配置中明确指定数据库。
  • Hibernate 允许您在代码中对其进行配置,例如 spring-jpa 也是如此。

标签: jpa jakarta-ee jdbc jndi


【解决方案1】:

你绝对可以做到你所要求的,但魔鬼在细节中。

我将其称为 JPA 多租户,here 是随机搜索提供的一篇有趣的文章,它提出了使用 CDI 的有效方法。考虑到您还需要在运行时添加新租户这一事实,我将从更通用的方法开始。

还要注意,Hibernate 为多租户提供了native solutions。我不知道您使用的是哪个 JPA 提供程序,但我猜其他人也有类似的功能。

基础知识

必须使用单独的、特定于租户的 EntityManagerFactory 实例来完成。确切的数据库连接 URL 可以通过传递给 Persistence.createEntityManagerFactory() 的映射给出。例如,假设存在META-INF/persistence.xml,则适用以下代码:

HashMap props = new HashMap();
props.put("javax.persistence.jdbc.url", /* tenant-specific JDBC URL*/);
EntityManagerFactory tenantSpecificEntityManagerFactory =
    Persistence.createEntityManagerFactory("name-of-persistence-unit-from-persistence.xml", props);

props 通常会覆盖persistence.xml 中指定的任何属性。可能只覆盖 JDBC URL 就足够了,也许您还需要覆盖用户/密码或其他内容。从那时起,您可以获取特定于租户的持久性上下文并使用它:

EntityManager tenantSpecificEm = tenantSpecificEntityManagerFactory.createEntityManager();

捕捉:效率

您可以在每次需要特定于租户的EntityManager 时执行上述代码(然后也关闭工厂)。但这会效率非常低,原因如下:

  1. 为每个请求重新创建 EntityManagerFactory 速度很慢
  2. 为每个请求重新创建数据库连接甚至更慢

要解决这些问题,您需要:

  1. 缓存EntityManagerFactory 实例
  2. 以某种方式使用连接池

缓存EntityManagerFactory 实例

我假设有一个强大的机制将每个请求与适当的租户相关联。此外,您将需要一个应用程序范围的 Map 租户名称到相应的 EntityManagerFactory 实例。如何存储和使用取决于应用程序,例如是否有任何依赖注入框架?第一个链接有一个使用 CDI 的解决方案,类似的解决方案适用于其他 DI 容器。

连接池

应用服务器提供数据库连接池。 Tomcat 也是如此。应用程序服务器可能允许您在运行时添加数据库连接池,而无需重新启动服务器。我不知道您使用的 Tomcat 版本是否支持它(我猜不支持,但请纠正我)。所以:

  • 如果应用程序服务器(在本例中为 Tomcat)支持运行时连接池创建,请执行此操作并调整 props 以使用它
  • 否则您将不得不使用适用于每个EntityManagerFactory 的自定义连接池。 Hibernate 至少似乎有这个feature

保持设置

我猜你已经想通了,但是在运行时应用的设置(租户、租户名称到连接属性映射)必须以某种方式持久化,以便在服务器重新启动时重新应用它们。这可能是一个配置文件或另一个“管理”数据库。

【讨论】:

  • 非常感谢您的详细解答。您对这种方法的内存消耗有任何经验吗?目前大约有 1000 个客户,这意味着我必须缓存 1000 个EntityManagerFactory 实例和 1000 个数据库连接池。另一个问题:对于数据库连接池我可以使用JNDI,对吗?
  • 哇,1000 位客户!对您的业务有好处:) 但您有理由担心:上述方法可能不适合您的情况!可能有一些方法可以解决这些限制,但这在很大程度上取决于您应用程序的确切工作量。例如。您可以将 EMF 保存在一个大小有限、最近使用的缓存中,或者调整 JPA 实现的缓存特性。或者想一个完全不同的方法。总而言之,我会:(1) 考虑这个和备选方案 (2) 对所选解决方案进行 PoC,并使用模拟的、真实的工作负载进行测试。
  • 至于JNDI:如果你使用应用服务器来创建连接池,那么,由于应用服务器通常将连接池放在JNDI中,你实际上不得不使用JNDI。 Hibernate 的原生解决方案没有将池放置在 JNDI 中(但我不知道是否还有配置它的方法)。但是,归根结底,你想要连接池,JNDI只是获取它的一种手段。
  • 感谢这对我帮助很大!我会试试你的建议。
【解决方案2】:

不确定这是否对您的特定情况有所帮助,但我设置了一个环境,其中在 hibernate.cfg.xml 中指定了 JDBC 连接 URL

<property name="connection.url">
    jdbc:ucanaccess://C:/Users/Public/UCanHibernate.accdb;newDatabaseVersion=V2010
</property>

可以像这样被 Java 系统属性覆盖:

StandardServiceRegistryBuilder ssrb = new StandardServiceRegistryBuilder()
        .configure(); // configures settings from hibernate.cfg.xml

// allow tester to specify their own connection URL (via -D JVM argument)
String runtimeUrl = System.getProperty("HIBERNATE_CONNECTION_URL"); 
if (runtimeUrl != null) {
    ssrb.applySetting("hibernate.connection.url", runtimeUrl);
}

您可能会从其他来源获得覆盖信息,但该原则可能仍然适用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-28
    • 2012-06-12
    • 1970-01-01
    • 1970-01-01
    • 2011-07-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多