相关文章推荐
暴躁的石榴  ·  翻译 - Dolibarr ERP CRM ...·  1 月前    · 
不羁的饺子  ·  Debezium | Apache Flink·  1 月前    · 
讲道义的烈酒  ·  Debezium-JSON--流式计算 ...·  1 月前    · 
从容的大脸猫  ·  零代码第三方数据接入 | TDengine ...·  1 月前    · 
迷茫的马克杯  ·  从VBA中的范围中删除特殊字符开发者社区·  4 周前    · 
留胡子的硬币  ·  python指针引用的区别_python ...·  9 月前    · 
考研的猕猴桃  ·  使用PHP的http请求客户端guzzle如 ...·  10 月前    · 
沉着的作业本  ·  使用循环 | Salesforce ...·  1 年前    · 
被表白的米饭  ·  sql触发器实现自动递增 - CSDN文库·  1 年前    · 
有腹肌的充值卡  ·  我们已经不用AOP做操作日志了!-腾讯云开发 ...·  2 年前    · 
Code  ›  Using Hibernate and Spring to Build Multi-Tenant Java Apps - Citus Data
string
https://www.citusdata.com/blog/2018/02/13/using-hibernate-and-spring-to-build-multitenant-java-apps/
淡定的野马
2 周前

The schedule is out 🗓️ for POSETTE: An Event for Postgres 2026!

Skip navigation
GitHub icon go to Citus GitHub repo with 10,100 stargazers
  • Download
  • Overview
    • Overview
    • Multi-Tenant Applications
    • Real-Time Analytics
    • Getting Started
    • Docs
    • Newsletters
    • FAQ
    • Podcast
    • Contact Us
    • Our Story
    • Events
    • Careers
  • Updates
  • Blog
  • Docs
  • GitHub icon go to Citus GitHub repo with 10,100 stargazers
  • Close Menu
  • Download

Using Hibernate and Spring to Build Multi-Tenant Java Apps

Written by Joe Kutner
February 13, 2018

SHARE THIS POST

X icon Share on X LinkedIn icon Share on
LinkedIn
Copy link

Get our monthly newsletter

If you're building a Java app, there's a good chance you're using Hibernate . The Hibernate ORM is a nearly ubiquitous choice for Java developers who need to interact with a relational database. It's mature, widely supported, and feature rich—as demonstrated by its support for multi tenant applications.

Hibernate officially supports two different multi-tenancy mechanisms: separate database and separate schema. Unfortunately, both of these mechanisms come with some downsides in terms of scaling. A third Hibernate multi-tenancy mechanism, a tenant discriminator, also exists, and it’s usable—but it’s still considered a work-in-progress by some. Unlike the separate database and separate schema approaches, which require distinct database connections for each tenant, Hibernate’s tenant discriminator model stores tenant data in a single database and partitions records with either a simple column value or a complex SQL formula.

But fear not, despite the unfinished state of Hibernate's built-in support for a tenant discriminator (or in simple terms tenant_id ), it's possible to implement your own discriminator using standard Spring, Hibernate, and AspectJ mechanisms that work quite well. The Hibernate tenant discriminator model works well as you start small on a single-node Postgres database, and even better, tenant discriminator can continue to scale as your data grows by leveraging the Citus extension to Postgres.

In this post, you'll learn how to add a tenant id to a Spring Boot 2 application, and use it to partition database records.

Getting Started with Spring Boot 2

We'll use an existing Spring Boot 2 example to demonstrate multi-tenancy. But you can apply the same methods described in this post to any standard Spring app.

The example app has a single Employee model and the necessary Repository, Service, and Controller classes surrounding it to create a RESTful HTTP interface. It's configured to use a PostgreSQL database, which you'll need to run locally if you want to test the app.

Otherwise, the example is fairly standard—except for its multi-tenant support. Let's look at how the multi-tenant app support is implemented.

Creating a Tenant Context

To start, you'll need a way to determine which tenant a client is making requests for. Every multi-tenant application does this a little differently depending on business needs, but for this example you'll use an HTTP header. The HTTP header is read by a Servlet Filter and stored in a ThreadLocal variable. The Servlet Filter's doFilter() method looks like this:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  HttpServletResponse response = (HttpServletResponse) servletResponse;
  HttpServletRequest request = (HttpServletRequest) servletRequest;
  String tenantHeader = request.getHeader("X-TenantID");
  if (tenantHeader != null && !tenantHeader.isEmpty()) {
    TenantContext.setCurrentTenant(tenantHeader);
  } else {
    response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.getWriter().write("{\"error\": \"No tenant header supplied\"}");
    response.getWriter().flush();
    return;
  filterChain.doFilter(servletRequest, servletResponse);

The filter gets the value of the "X-TenantID" HTTP header for every request and sends it to the TenantContext class. If no header is provided, it responds with an error. The TenantContext looks like this:

public class TenantContext {
  private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
  public static String getCurrentTenant() {
    return currentTenant.get();
  public static void setCurrentTenant(String tenant) {
    currentTenant.set(tenant);
  public static void clear() {
    currentTenant.set(null);

When setCurrentTenant() is called, it adds the tenant value to the ThreadLocal instance for the current thread. This is the thread that will handle the entire lifecycle of the request, which means the value will be available anywhere in the application that this request is being processed.

Now you can use the TenantContext to partion your database records.

Discriminating Tenants by ID

The example application's Employee model has a few simple properties and a tenantId . The tenantId is what you'll use to determine which tenant a record belongs to. The value of tenantId will correlate to the value passed in with the HTTP header and added to the ThreadLocal context.

The Employee model is shown here:

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "string")})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Employee implements TenantSupport {
  @GeneratedValue
  private UUID id;
  private String firstName;
  private String lastName;
  private String tenantId;

The class is defined with standard JPA and Hibernate annotations. Notably, the @FilterDef and @Filter annotations will allow us to inject a tenant discriminator clause to every SQL query generated for this model. To do this, you'll use AspectJ advice, which looks like this:

@Aspect
@Component
public class EmployeeServiceAspect {
  @Before("execution(* com.example.service.EmployeeService.*(..)) && !execution(* com.example.service.EmployeeService.run(..)) && target(employeeService)")
  public void aroundExecution(JoinPoint pjp, EmployeeService employeeService) throws Throwable {
    org.hibernate.Filter filter = employeeService.entityManager.unwrap(Session.class).enableFilter("tenantFilter");
    filter.setParameter("tenantId", TenantContext.getCurrentTenant());
    filter.validate();

The aroundExecution() method enables the Hibernate filter on the Employee model when any of the data-access methods on the EmployeeService class are executed. It populates the filter criteria with the tenant value from the TenantContext to limit the query results to only those records that match.

Finally, you must ensure a tenant discriminator is added whenever a database recorded is created or destroyed. For this, you'll use a Hibernate Interceptor, as shown here:

new EmptyInterceptor() {
  @Override
  public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    if (entity instanceof TenantSupport) {
      ((TenantSupport) entity).setTenantId(TenantContext.getCurrentTenant());
    return false;
  @Override
  public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    if (entity instanceof TenantSupport) {
      ((TenantSupport) entity).setTenantId(TenantContext.getCurrentTenant());
  @Override
  public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
    if (entity instanceof TenantSupport) {
      ((TenantSupport) entity).setTenantId(TenantContext.getCurrentTenant());
    return false;

Like the AspectJ advice, the Interceptor uses the value from the TenantContext to populate the tenantId field.

Now you're ready to give it a spin.

Testing Your Tenants

You can run the sample app locally, or deploy it to Heroku by creating a Heroku account , installing the Heroku CLI , and running the following commands:

$ heroku create
$ git push heroku master

To run the app locally, you'll need to start PostgreSQL and create a database with this command:

$ createdb spring-multi-tenancy

Then run the application with the following command on Mac and Linux:

$ ./mvnw spring-boot:run

Or this command on Windows:

$ mvnw.cmd spring-boot:run

In another terminal, use cURL or a similar HTTP client to test the service. First, get the list of employees for "tenant1", which has been pre-populated with a John Doe employee (if you're running the app on Heroku, replace localhost:8080 with the herokuapp.com URL for your app):

$ curl localhost:8080/employees -H "X-TenantID: tenant1"
[{"userId":"16b5308b-6bb8-4a75-ae93-66dc71a0b981","firstName":"John","lastName":"Doe","tenantId":"tenant1"}]

Then create your own tenant byt changing the X-TenantID header, and get a list of its employees:

$ curl localhost:8080/employees -H "X-TenantID: citus"

The list empty because no employees have been created yet. Create the first one with this command:

$ curl localhost:8080/employees -H "X-TenantID: citus" \
-X POST -d '{"firstName":"Joe", "lastName":"Kutner"}' \
 
推荐文章
暴躁的石榴  ·  翻译 - Dolibarr ERP CRM Wiki
1 月前
不羁的饺子  ·  Debezium | Apache Flink
1 月前
讲道义的烈酒  ·  Debezium-JSON--流式计算 Flink版-火山引擎
1 月前
从容的大脸猫  ·  零代码第三方数据接入 | TDengine 文档 | 涛思数据
1 月前
迷茫的马克杯  ·  从VBA中的范围中删除特殊字符开发者社区
4 周前
留胡子的硬币  ·  python指针引用的区别_python – 在ctypes中LP_ *指针和* _p指针之间有什么区别? (与结构的奇怪交互)...-CSDN博客
9 月前
考研的猕猴桃  ·  使用PHP的http请求客户端guzzle如何添加请求头_guzzle 如何携带header-CSDN博客
10 月前
沉着的作业本  ·  使用循环 | Salesforce Trailhead
1 年前
被表白的米饭  ·  sql触发器实现自动递增 - CSDN文库
1 年前
有腹肌的充值卡  ·  我们已经不用AOP做操作日志了!-腾讯云开发者社区-腾讯云
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号