我正在编写一个必须是 Multi-Tenancy 的Web应用程序。我正在将JPA用于持久层,并且正在有兴趣地评估EclipseLink。
我要使用的 Multi-Tenancy 策略是:每个客户一个模式。 Hibernate支持这种策略(http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html#d5e4771),并且我已经成功地使用了它。但是,AFAIK仅在使用本地Hibernate API时才支持它,而我想使用JPA。
另一方面,EclipseLink支持单表和多表 Multi-Tenancy 策略。但是,它也支持分区,并且通过简单的自定义分区策略,我可以轻松地为每个客户设置一个分区。
第一个问题可能是在此用例中使用分区是否合适。
但是,主要问题是,客户群可能(希望)随着时间的推移而增长,因此我必须使EclipseLink动态“了解”新客户(即:无需重新启动Webapp)。据我了解,要在EclipseLink中设置分区,我必须使用不同的“连接池”(或“节点”)来设置我的持久性单元:每个节点都有其配置的数据源和名称。另一方面,分区策略将通过其名称确定要使用的节点。到目前为止一切顺利,但是我计划使用Spring的LocalContainerEntityManagerFactoryBean
设置我的持久性单元。在处理LocalContainerEntityManagerFactoryBean
时,我可能会在启动时动态发现客户,这样我就可以在那时传递所有节点/客户的所有必需属性,但是如果之后添加新客户会怎样?我认为动态更改持久性单元属性不会对已经构造的EntityManagerFactory
单例实例产生任何影响...并且我担心EclipseLink会抱怨如果我请求一个在EntityManagerFactory
创建时尚不知道其对应节点的分区。如我错了请纠正我。
我认为将LocalContainerEntityManagerFactoryBean
范围声明为“prototype” bean将是一个非常糟糕的主意,并且我认为它根本不起作用。另一方面,由于客户交互绑定到特定的HTTP会话,因此我可以通过将LocalContainerEntityManagerFactoryBean
范围声明为“会话”来使用“中间”方法,但是我认为在这种情况下,我将不得不处理诸如增加了内存消耗,并在多个EntityManagerFactories
之间共享了缓存(在给定时间使用该应用程序的每个客户一个)。
如果我无法使该策略生效,那么我认为我将不得不放弃整个分区,而只能使用“动态数据源路由”方法,但是在这种情况下,我担心的是EclipseLink共享缓存的一致性(我想我必须完全禁用它,这将是一个真正的缺点)。
预先感谢您对此的任何反馈。
最佳答案
老实说,我没有尝试克里斯的建议,而是选择了更精细的解决方案。这是我的解决方案。
在我的情况下,
SecurityContextHolder
PartitioningPolicy
,它如上一点所述确定了当前登录用户的客户,然后返回一个列表,该列表仅包含标识该客户分区Accessor
。JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class);
ServerSession serverSession = jpaEmf.getServerSession();
serverSession.getProject().addPartitioningPolicy(myCustomerPolicy);
serverSession.setPartitioningPolicy(myCustomerPolicy);
然后,要将数据源动态添加到EclipseLink(在EclipseLink术语中称为“连接池”),以使上述策略指定的客户ID与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和T2,不应存在两个具有相同id = x的E类型的不同实体实例,否则EclipseLink可能会认为它们是完全相同的实体实例。这可能导致在单个JPA会话期间读取/写入来自不同客户的混合数据=>灾难。
可能的解决方案:
在这种情况下,
要正确实现选项2,要解决的最后一个小问题是,即使EclipseLink文档说可以使用
eclipselink.connection-pool.sequence
配置选项指定专用于表排序的连接池(=数据源),当如上所述设置默认分区策略。实际上,我的客户参与策略会针对每个查询(甚至那些用于ID分配的查询)都被调用。因此,策略必须拦截这些查询并将其路由到中央数据源。我找不到解决此问题的最终解决方案,但我能想到的最佳选择是:
我通过正确定义我的id生成映射选择了选项2:
@Entity
public class MyEntity {
@Id
@TableGenerator(name = "MyEntity_SEQUENCE", allocationSize = 10)
@GeneratedValue(generator = "MyEntity_SEQUENCE")
private Long id;
}
这使EclipseLink使用名为
SEQUENCE
的表,该表包含一行SEQ_NAME
列值为MyEntity_SEQUENCE
的行。用于更新此ID分配序列的查询将被命名为MyEntity_SEQUENCE
,我们已经完成。但是,我已将我的参与策略设置为可配置的,以便在EclipseLink实现中发生某些更改而打破了这种“启发式”方法时,可以随时从一种顺序查询标识策略切换到另一种。
这基本上是整个图片。目前,它运行良好。
欢迎提供反馈,改进,建议。
关于spring - 使用分区与动态租户进行多方案 Multi-Tenancy ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26863928/