作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
andr
当我们谈论云应用程序时,每个客户端都有自己独立的数据, 我们需要考虑如何存储和操作这些数据. 即使有所有伟大的NoSQL解决方案, 有时我们仍然需要使用老式的关系数据库. 首先想到的分离数据的解决方案是在每个表中添加标识符, 所以可以单独处理. 这是可行的,但是如果客户端请求他们的数据库呢? 要检索隐藏在其他记录中的所有记录将是非常麻烦的.
不久前,Hibernate团队提出了一个解决这个问题的方案. 它们提供了一些扩展点,使人们能够控制从何处检索数据. 该解决方案可以选择通过标识符列控制数据, 多个数据库, 还有多种模式. 本文将介绍多模式解决方案.
所以,让我们开始工作吧!
如果你是一个更 有经验的Java开发人员 并且知道如何配置一切, 或者如果您已经有了自己的Java EE项目, 你可以跳过这一节.
首先,我们必须创建一个新的Java项目. 我正在使用Eclipse和Gradle, 但是您可以使用自己喜欢的IDE和构建工具, 比如IntelliJ和Maven.
如果你想使用和我一样的工具,你可以按照下面的步骤来创建你的项目:
Great! 这应该是初始文件结构:
javaee-mt
|- src/main/java
| - src / main /资源
|- src/test/java
| - src /测试/资源
| JRE系统库
Gradle Dependencies
|- build
|- src
|- build.gradle
您可以删除源文件夹中的所有文件,因为它们只是示例文件.
要运行项目, I use Wildfly, 我将展示如何配置它(你也可以在这里使用你最喜欢的工具):
现在,让我们配置Wildfly来了解数据库:
好了,我们将Eclipse和Wildfly配置在一起!
这是项目外部所需的所有配置. 让我们转到项目配置.
现在我们已经配置了Eclipse和Wildfly,并创建了我们的项目, 我们需要配置我们的项目.
我们要做的第一件事是编辑build.gradle. 它应该是这样的:
应用插件:'java'
应用插件:'war'
应用插件:'eclipse'
应用插件:'eclipse-wtp'
sourccompatibility = '1.8'
compileJava.options.encoding = 'UTF-8'
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
repositories {
jcenter()
}
eclipse {
wtp {
}
}
dependencies {
providedCompile”组织.hibernate: hibernate-entitymanager: 5.0.7.Final'
providedCompile”组织.jboss.resteasy: resteasy-jaxrs: 3.0.14.Final'
providedCompile javax: javaee-api: 7.0'
}
依赖项都声明为" providedCompile ", 因为这个命令不会在最终的war文件中添加依赖项. Wildfly已经有了这些依赖项,否则它会与应用程序的依赖项产生冲突.
At this point, 您可以右键单击您的项目, select Gradle (STS) -> Refresh All to import the dependencies we just declared.
是时候创建和配置“持久性”了.xml”文件,该文件包含Hibernate需要的信息:
该文件的内容必须类似于以下内容, 更改jta-data-source以匹配您在Wildfly中创建的数据源 package com.toptal.andrehil.mt.hibernate
到您将在下一节中创建的包(除非您选择相同的包名):
java:/JavaEEMTDS
添加到持久性的配置.xml指向两个自定义类MultiTenantProvider和SchemaResolver. 第一个类负责提供配置了正确模式的连接. 第二个类负责解析要使用的模式的名称.
下面是这两个类的实现:
公共类MultiTenantProvider实现MultiTenantConnectionProvider, ServiceRegistryAwareService {
private static final serialVersionUID = 1L;
private DataSource;
@Override
公共布尔支持侵略性释放(){
return false;
}
@Override
public void injectServices(ServiceRegistryImplementor serviceRegistry) {
try {
final Context init = new InitialContext();
dataSource = (dataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name
} catch (final NamingException e) {
抛出新的RuntimeException(e);
}
}
@SuppressWarnings(“rawtypes”)
@Override
公共布尔isUnwrappableAs(类clazz) {
return false;
}
@Override
public T unwrap(Class clazz) {
return null;
}
@Override
getAnyConnection()抛出SQLException {
最终连接连接=数据源.getConnection();
返回连接;
}
@Override
getConnection(String tenantIdentifier)抛出SQLException {
连接= getAnyConnection();
try {
connection.createStatement ().execute("SET SCHEMA '" + tenantIdentifier + "'");
} catch (final SQLException e) {
抛出新的HibernateException("试图修改schema [" + tenantIdentifier + "]时出错",e);
}
返回连接;
}
@Override
公共void releaseAnyConnection(连接连接)抛出SQLException {
try {
connection.createStatement ().execute("SET SCHEMA 'public'");
} catch (final SQLException e) {
抛出新的HibernateException("试图更改schema [public]时出错",e);
}
connection.close();
}
@Override
公共void releaseConnection(字符串tenantIdentifier,连接)抛出SQLException {
releaseAnyConnection(连接);
}
}
上述语句中使用的语法适用于PostgreSQL和其他一些数据库, 如果数据库有不同的语法来更改当前模式,则必须更改此设置.
公共类SchemaResolver实现CurrentTenantIdentifierResolver {
private String tenantIdentifier = "public";
@Override
resolveCurrentTenantIdentifier() {
返回tenantIdentifier;
}
@Override
公共布尔validateexistingcurrentssessions () {
return false;
}
公共无效setTenantIdentifier(字符串tenantIdentifier) {
this.tenantIdentifier = tenantIdentifier;
}
}
此时,已经可以测试应用程序了. For now, 我们的解析器直接指向一个硬编码的公共模式, 但它已经被调用了. 为此,请停止正在运行的服务器,然后重新启动它. 您可以尝试在调试模式下运行它,并在上述类的任何点设置断点,以检查它是否正在工作.
那么,解析器如何包含模式的正确名称呢?
实现这一点的一种方法是在所有请求的头中保留一个标识符,然后创建一个过滤器来注入模式的名称.
让我们实现一个过滤器类来举例说明这种用法. 解析器可以通过Hibernate的SessionFactory访问, 因此,我们将利用它来获取它并注入正确的模式名称.
@Provider
AuthRequestFilter实现ContainerRequestFilter
@PersistenceUnit(unitName = "pu")
实体管理工厂;
@Override
public void filter(ContainerRequestContext)抛出IOException {
最终SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory ();
SchemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver ();
用户名= containerRequestContext.getHeaderString(“用户名”);
schemaResolver.setTenantIdentifier(用户名);
}
}
Now, 当任何类获得EntityManager来访问数据库时, 它已经配置了正确的模式.
为了简单起见, 这里展示的实现是直接从头中的字符串获取标识符, 但是使用身份验证令牌并将标识符存储在令牌中是一个好主意. 如果你有兴趣了解更多关于这个主题, 我建议看看JSON Web令牌(JWT). JWT是一个用于令牌操作的漂亮而简单的库.
一切都配置好了, 在与之交互的实体和/或类中不需要做任何其他事情 EntityManager
. 从EntityManager运行的任何内容都将被定向到由创建的过滤器解析的模式.
Now, 您所需要做的就是在客户端拦截请求,并在要发送到服务器端的报头中注入标识符/令牌.
本文末尾的链接指向用于编写本文的项目. 它使用Flyway创建2个模式,并包含一个名为Car的实体类和一个名为rest的服务类 CarService
可以用来测试这个项目. 您可以遵循以下所有步骤, 而不是创建你自己的项目, 你可以克隆它,然后用这个. Then, 当运行时,你可以使用一个简单的HTTP客户端(如Chrome邮差扩展),并使一个GET请求http://localhost:8080/javaee-mt/rest/cars与头键:值:
By doing this, 请求将返回不同的值, 哪些在不同的模式中, 一只叫乔,另一只叫弗雷德。.
这并不是Java世界中创建多租户应用程序的唯一解决方案, 但这是实现这一目标的简单方法.
需要记住的一点是,在使用多租户配置时,Hibernate不会生成DDL. 我的建议是看看Flyway或liquubase, 哪些库可以很好地控制数据库的创建. 这是一件很好的事情,即使您不打算使用多租户, 因为Hibernate团队建议不要在生产中使用他们的自动数据库生成.
用于创建本文和环境配置的源代码可在 github.com/andrehil/JavaEEMT
森林公园,伊利诺伊州,美国
2016年2月29日成为会员
andr
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.