authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
André is a versatile and talented developer with 10+ years of industry experience. 他精通Java、Java EE、JavaScript等.
When we talk about cloud applications where each client has their own separate data, 我们需要考虑如何存储和操作这些数据. 即使有所有伟大的NoSQL解决方案, 有时我们仍然需要使用老式的关系数据库. The first solution that might come to mind to separate data is to add an identifier in every table, 所以可以单独处理. 这是可行的,但是如果客户端请求他们的数据库呢? It would be very cumbersome to retrieve all those records hidden among the others.
不久前,Hibernate团队提出了一个解决这个问题的方案. They provide some extension points that enable one to control from where data should be retrieved. This solution has the option to control the data via an identifier column, 多个数据库, 还有多种模式. 本文将介绍多模式解决方案.
所以,让我们开始工作吧!
如果你是一个更 有经验的Java开发人员 并且知道如何配置一切, 或者如果您已经有了自己的Java EE项目, 你可以跳过这一节.
首先,我们必须创建一个新的Java项目. 我正在使用Eclipse和Gradle, 但是您可以使用自己喜欢的IDE和构建工具, 比如IntelliJ和Maven.
If you want to use the same tools as me, you can follow these steps to create your project:
Great! 这应该是初始文件结构:
javaee-mt
|- src/main/java
| - src / main /资源
|- src/test/java
| - src /测试/资源
|- JRE系统库
|- Gradle Dependencies
|- build
|- src
|- build.gradle
You can delete all files that come inside the source folders, as they are just sample files.
要运行项目, I use Wildfly, and I will show how to configure it (again you can use your favorite tool here):
现在,让我们配置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 already has these dependencies, and it would cause conflicts with the app’s ones otherwise.
At this point, 您可以右键单击您的项目, select Gradle (STS) -> Refresh All to import the dependencies we just declared.
是时候创建和配置“持久性”了.xml”文件,该文件包含Hibernate需要的信息:
该文件的内容必须类似于以下内容, changing jta-data-source to match the datasource you created in Wildfly and the package com.toptal.andrehil.mt.hibernate
to the one you are going to create in the next section (unless you choose the same package name):
java:/JavaEEMTDS
添加到持久性的配置.xml指向两个自定义类MultiTenantProvider和SchemaResolver. The first class is responsible for providing connections configured with the right schema. The second class is responsible for resolving the name of the schema to be used.
下面是这两个类的实现:
public class MultiTenantProvider implements 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
public Connection getConnection(String tenantIdentifier) throws SQLException {
连接= getAnyConnection();
try {
connection.createStatement ().execute("SET SCHEMA '" + tenantIdentifier + "'");
} catch (final SQLException e) {
throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e);
}
返回连接;
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
try {
connection.createStatement ().execute("SET SCHEMA 'public'");
} catch (final SQLException e) {
抛出新的HibernateException("试图更改schema [public]时出错",e);
}
connection.close();
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
releaseAnyConnection(连接);
}
}
The syntax being used in the statements above work with PostgreSQL and some other databases, this must be changed in case your database has a different syntax to change the current schema.
公共类SchemaResolver实现CurrentTenantIdentifierResolver {
private String tenantIdentifier = "public";
@Override
resolveCurrentTenantIdentifier() {
返回tenantIdentifier;
}
@Override
公共布尔validateexistingcurrentssessions () {
return false;
}
公共无效setTenantIdentifier(字符串tenantIdentifier) {
this.tenantIdentifier = tenantIdentifier;
}
}
此时,已经可以测试应用程序了. For now, 我们的解析器直接指向一个硬编码的公共模式, 但它已经被调用了. 为此,请停止正在运行的服务器,然后重新启动它. You can try to run it in debug mode and place breakpoint at any point of the classes above to check if it is working.
那么,解析器如何包含模式的正确名称呢?
One way to achieve this is to keep an identifier in the header of all requests and then create a filter to inject the name of the schema.
让我们实现一个过滤器类来举例说明这种用法. 解析器可以通过Hibernate的SessionFactory访问, so we will take advantage of that to get it and inject the right schema name.
@Provider
AuthRequestFilter实现ContainerRequestFilter
@PersistenceUnit(unitName = "pu")
实体管理工厂;
@Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory ();
SchemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver ();
用户名= containerRequestContext.getHeaderString(“用户名”);
schemaResolver.setTenantIdentifier(用户名);
}
}
Now, 当任何类获得EntityManager来访问数据库时, 它已经配置了正确的模式.
为了简单起见, the implementation shown here is getting the identifier directly from a string in the header, but it is a good idea to use an authentication token and store the identifier in the token. 如果你有兴趣了解更多关于这个主题, 我建议看看JSON Web令牌(JWT). JWT是一个用于令牌操作的漂亮而简单的库.
一切都配置好了, there is nothing else needed to do in your entities and/or classes that interact with EntityManager
. Anything you run from an EntityManager will be directed to the schema resolved by the created filter.
Now, all you need to do is to intercept requests on the client side and inject the identifier/token in the header to be sent to the server side.
The link at the end of the article points to the project used to write this article. It uses Flyway to create 2 schemas and contains an entity class called Car and a rest service class called CarService
可以用来测试这个项目. 您可以遵循以下所有步骤, 而不是创建你自己的项目, 你可以克隆它,然后用这个. Then, when running you can use a simple HTTP client (like Postman extension for Chrome) and make a GET request to http://localhost:8080/javaee-mt/rest/cars with the headers key:value:
By doing this, 请求将返回不同的值, 哪些在不同的模式中, 一只叫乔,另一只叫弗雷德。.
This is not the only solution to create multitenancy applications in the Java world, 但这是实现这一目标的简单方法.
One thing to keep in mind is that Hibernate doesn’t generate DDL when using multitenancy configuration. 我的建议是看看Flyway或liquubase, 哪些库可以很好地控制数据库的创建. 这是一件很好的事情,即使您不打算使用多租户, as the Hibernate team advises to not use their auto database generation in production.
The source code used to create this article and environment configuration can be found at github.com/andrehil/JavaEEMT
André is a versatile and talented developer with 10+ years of industry experience. 他精通Java、Java EE、JavaScript等.
17
世界级的文章,每周发一次.
世界级的文章,每周发一次.