说实话,我没有尝试克里斯的建议,而是选择了一种更精细调整的解决方案。这是我的解决方案。
- in my case, tenant = customer; each customer data is in its own database schema, potentially located in a dedicated DBMS instance (of whatever vendor); in other words, I have one different data source per customer
- since I use partitioning, this means that every customer has its own partition; each partition is identified by the corresponding unique customer id
- every user that logs into the application belongs to a different customer; I use Spring Security to handle authentication and authorization, hence I can retrieve information about the user (including its owning customer) by querying the
SecurityContextHolder
- I defined my own EclipseLink
PartitioningPolicy
which determines the customer of the currently logged in user as described in the previous point, and then returns a list containing an only Accessor
that identifies that customer partition
all my tables must be partitioned and I don't want to specify that on EVERY entity with annotations, so I registered this partitioning policy into EclipseLink on startup and set it as the default one; briefly:
JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class)
ServerSession serverSession = jpaEmf.getServerSession()
serverSession.getProject().addPartitioningPolicy(myCustomerPolicy)
serverSession.setPartitioningPolicy(myCustomerPolicy)
接下来,为了动态添加数据源到EclipseLink(在EclipseLink术语中称为“连接池”),使得上述策略中指定的客户ID与EclipseLink中已知的“连接池”相匹配,我执行以下操作:
- a listener intercepts any user successful login
this listener queries EclipseLink to see it already knows about a connection pool identified by the user customer id; if it does, we're done, EclipseLink can correctly handle the partition; otherwise a new connection pool is created and added to EclipseLink; proof of concept:
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中,数据分区意味着可识别的数据片段(即实体实例)只存在于一个分区中,或者在多个分区中平等地复制。实体实例的标识(即主键)确定实体实例的唯一性。这意味着不应该存在两个具有相同id=x 的类型为E的不同客户/租户T1和T2的实体实例,否则EclipseLink可能会认为它们是完全相同的实体实例。这可能导致在单个JPA会话期间读取/写入来自不同客户的混合数据 =& gt; 造成灾难。
可能的解决方案:
在这种情况下,要使用的分区由当前登录用户确定;这意味着在HTTP会话范围内执行的每个查询都将是相同的;由于我使用了事务作用域实体管理器,其生命周期最多等于请求持续时间(该时间又延伸到HTTP会话),因此仅禁用EclipseLink共享缓存即可避免来自不同客户的数据混合,但这仍然是不理想的。
我能找到的最佳选择是确保所有ID(主键)都是由EclipseLink集中交叉客户处理生成的,并且id=
x用于实体的生成只分配给一个客户的一个实体实例。这实际上意味着将ID分配序列“分区”到客户端,并防止使用MySQL自增列(也称为数据库标识生成类型)。因此,我选择使用表生成类型用于实体标识符,并将该表放置在存储用户和客户信息的中央数据库中。
实现选项2的最后一个小问题是,即使EclipseLink文档说可以使用
eclipselink.connection-pool.sequence
配置选项指定专用于表序列的连接池(=数据源),但在设置了如上所述的默认分区策略时,似乎会被忽略。事实上,我的客户端分区策略会为每个查询调用,甚至用于ID分配的查询。因此,该策略必须拦截这些查询并将它们路由到中央数据源。
我找不到此问题的明确解决方法,但我能想到的最佳选项是:
- 如果查询的SQL字符串以“UPDATE SEQUENCE”开头,则意味着它是用于ID分配的查询,假设专用于序列分配的表称为SEQUENCE(这是默认值)。
- 如果您采用向生成器添加SEQUENCE后缀的约定,则如果执行的查询名称以“SEQUENCE”结尾,则表示它是用于ID分配的查询。
我选择了选项2,并正确定义了我的ID生成映射。
@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实现中发生破坏此“启发式算法”的情况。
这基本上就是整个情况。目前,它一直运作良好。
欢迎反馈、改进和建议。