我正在编写一个必须是多租户的Web应用程序。我正在使用JPA作为持久层,我正在兴趣地评估EclipseLink。
我想要使用的多租户策略是:每个客户一个架构。 Hibernate支持这样的策略(http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html#d5e4771),我已经成功地使用了它。但是,AFAIK只有在使用本机Hibernate API时才支持它,而我想使用JPA。
另一方面,EclipseLink支持单表和多表多租户策略。但是,它还支持分区,并且通过简单的自定义分区策略,我可以轻松地为每个客户设置一个分区。第一个问题可能是对此用例使用分区是否合适。
然而,主要问题是客户群可能(希望)随着时间的推移而增长,因此我必须动态地让EclipseLink“了解”新客户(即:不重新启动webapp)。根据我的理解,要在EclipseLink中设置分区,我必须使用不同的“连接池”(或“节点”)设置我的持久性单元:每个节点都有其配置的数据源和名称。另一方面,分区策略将根据其名称确定要使用的节点。到目前为止一切顺利,但我计划使用Spring的LocalContainerEntityManagerFactoryBean
来设置我的持久性单元。在处理LocalContainerEntityManagerFactoryBean
时,我可能会在启动时动态发现客户,这样我就可以在那时传递所有节点/客户所需的所有属性,但如果之后添加了新客户会怎样?我不认为动态更改持久性单元属性会对已构造的EntityManagerFactory
单例实例产生任何影响......我担心如果我请求的分区中没有相应的节点,EclipseLink会抱怨{1}}创作时间。如果我错了,请纠正我。
我认为将EntityManagerFactory
范围声明为“原型”bean将是一个非常糟糕的主意,我认为它根本不起作用。另一方面,由于客户交互绑定到特定的HTTP会话,我可以通过将LocalContainerEntityManagerFactoryBean
范围声明为“会话”来使用“中间”方法,但我认为在这种情况下我会有管理多个LocalContainerEntityManagerFactoryBean
之间增加的内存消耗和共享缓存协调等问题(在给定时间使用该应用程序的每个客户一个)。
如果我不能使这个策略工作,我想我将不得不放弃整个分区并回归到“动态数据源路由”方法,但在这种情况下我关注EclipseLink共享缓存一致性(我想我必须完全禁用它,这将是一个真正的缺点)。
提前感谢您对此的任何反馈。
答案 0 :(得分:1)
老实说,我没有尝试克里斯的建议,但选择了更精细的解决方案。这是我的解决方案。
SecurityContextHolder
PartitioningPolicy
,它确定了当前登录用户的客户,如上一点所述,然后返回一个包含唯一标识该客户分区的Accessor
的列表我的所有表都必须进行分区,并且我不想在带有注释的每个实体上指定它,因此我在启动时将此分区策略注册到EclipseLink中并将其设置为默认值;简要地:
JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class);
ServerSession serverSession = jpaEmf.getServerSession();
serverSession.getProject().addPartitioningPolicy(myCustomerPolicy);
serverSession.setPartitioningPolicy(myCustomerPolicy);
然后,将数据源动态添加到EclipseLink(它们被称为#34;连接池"在EclipseLink术语中),以便上面的策略指定的客户ID与已知的"连接匹配池"在EclipseLink中,我执行以下操作:
此侦听器查询EclipseLink以查看它已知道用户客户ID标识的连接池;如果确实如此,我们已经完成,EclipseLink可以正确处理分区;否则创建一个新的连接池并添加到EclipseLink;概念证明:
String customerId = principal.getCustomerId();
JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class);
ServerSession serverSession = jpaEmf.getServerSession();
if (!serverSession.getConnectionPools().containsKey(customerId)) {
DataSource customerDataSource = createDataSourceForCustomer(customerId);
DatabaseLogin login = new DatabaseLogin();
login.useDataSource(customerId);
login.setConnector(new JNDIConnector(customerDataSource));
Class<? extends DatabasePlatform> databasePlatformClass = determineDbVendorPlatform(customerId);
login.usePlatform(databasePlatformClass.newInstance());
ConnectionPool connectionPool = new ExternalConnectionPool(customerId, login, serverSession);
connectionPool.startUp();
serverSession.addConnectionPool(connectionPool);
}
用户登录操作当然是针对中央数据库(或任何其他身份验证源)执行的,因此上述代码在执行任何客户特定的JPA查询之前发生(因此客户连接池被添加到分区策略之前的EclipseLink引用它。)
但是,有一个重要的方面需要考虑。在EclipseLink中,数据分区意味着可识别的数据片段(=实体实例)只在一个分区中,或者在多个分区中同等复制。实体实例标识通过标识符(=主键)确定。这意味着对于两个不同的客户/租户 T1 和<不应存在两个不同的 E 类型的实体实例,其id = x em> T2 ,否则EclipseLink可能会认为它们是完全相同的实体实例。这可能导致在单个JPA会话期间读取/写入来自不同客户的混合数据=&gt;灾难。 可能的解决方案:
正确实现选项2要解决的最后一个小问题是,即使EclipseLink文档说可以使用{{1}指定专用于表排序的连接池(=数据源)配置选项,当如上所述设置默认分区策略时,这似乎被忽略。实际上,我的客户partitioining策略会被调用用于每个查询,甚至是那些用于id分配的查询。因此,策略必须拦截这些查询并将它们路由到中央数据源。 我无法找到解决这个问题的最终解决方案,但我能想到的最佳选择是:
我通过正确定义我的id生成映射选择了选项2:
eclipselink.connection-pool.sequence
这使得EclipseLink使用名为@Entity
public class MyEntity {
@Id
@TableGenerator(name = "MyEntity_SEQUENCE", allocationSize = 10)
@GeneratedValue(generator = "MyEntity_SEQUENCE")
private Long id;
}
的表,其中包含SEQUENCE
列值为SEQ_NAME
的一行。用于更新此序列以进行ID分配的查询将命名为MyEntity_SEQUENCE
,我们已完成。
但是我使我的partitioining策略可配置,以便我可以随时从一个序列查询识别策略切换到另一个序列查询识别策略,以防EclipseLink实现中的某些更改打破了这个&#34;启发式&#34;。
这基本上就是全貌。目前,它一直运作良好。 欢迎提供反馈,改进和建议。
答案 1 :(得分:0)
在这里描述的EclipseLink EntityManagerFactory类上查看refreshMetadata:http://wiki.eclipse.org/EclipseLink/DesignDocs/340192#EntityManagerFactory这将导致单例重新加载配置数据。它不会影响正在运行的EntityManager实例,但会导致获得的任何新EntityManagers使用新的配置数据,这似乎符合您的用法。
需要解包EntityManagerFactory才能访问http://javadox.com/org.eclipse.persistence/eclipselink/2.5.0/org/eclipse/persistence/jpa/JpaEntityManagerFactory.html界面:
JpaHelper.getEntityManagerFactory(em).refreshMetadata(properties);