DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Data Engineering
  3. Data
  4. Multi-Tenancy Using JPA, Spring, and Hibernate (Part 2)

Multi-Tenancy Using JPA, Spring, and Hibernate (Part 2)

If you want to support multi-tenancy with Spring in your stack, you should know how Spring actually knows the tenants, then dive right in.

Jose Manuel García Maestre user avatar by
Jose Manuel García Maestre
·
Feb. 21, 17 · Tutorial
Like (9)
Save
Tweet
Share
28.18K Views

Join the DZone community and get the full member experience.

Join For Free

To understand how tenants work in Spring, it is important to first understand what multi-tenancy is and how it is implemented in JPA.

As I said in part 1, Hibernate/JPA knows the current tenant through the CurrentTenantIdentifierResolver, which is resolved by Spring. So the real question here is, “How does Spring know what tenants exist?” The answer to this question has a lot of different ways to be implemented, which is why it’s difficult to find a good reference.

In our case, before implementing it, we will set the basis to know how tenants are defined:

  • Each tenant has a property file that describes values for a dataSource in the form: database.<tenantId>.properties. We will call it a tenant-file in this post.
  • Each <tenantId> written in a filename is what identifies a tenantId. In this way, we don’t need to duplicate the information. If you need tenantX, just create a filename like database.tenantX.properties
  • There is a Map<String, DataSource>, the key will be the tenantId string, this map will be initialized by getting the properties from the tenant-files.
  • There is a default datasource called defaultDatasource. It contains default properties, so if a property is not specified in the tenant-file, we will get it from the default datasource. In this way, we can set a collection of properties as default and avoid property duplication.

As you can see from above, tenants are acquired from a tenant-file. Now let’s continue with our implementation.

Our Spring Implementation to Get Tenants

DataSourceLookup is a Spring interface that allows us to look up DataSources by name, we chose it since we want to find DataSource by TenantId. I found in the JavaDoc from Spring a class named MapDataSourceLookup, which has a Map<String,DataSource> and implements DataSourceLookup.

I decided to implement my own class so I could get the tenants from the properties file in a folder:

/**
 * It lookup the correct datasource to use, we have one per tenant
 * 
 * The tenant datasource has default properties from database.properties and
 * also properties in database.{tenantId}.properties whose properties override
 * the default ones.
 * 
 * @author jm
 *
 */
@Component(value = "dataSourceLookup")
public class MultiTenantDataSourceLookup extends MapDataSourceLookup {

  Log logger = LogFactory.getLog(getClass());

  private String tenantDbConfigs = TENANT_DB_CONFIGS; // For testing
  private String tenantDbConfigsOverride = TENANT_DB_CONFIGS_OVERRIDE; // For production
  private String tenantRegex = TENANT_REGEX;

  @Autowired
  public MultiTenantDataSourceLookup(BoneCPDataSource defaultDataSource) {
    super();

    try {
      initializeDataSources(defaultDataSource);
    } catch (IOException e) {
      e.printStackTrace();
    }

  }

  /**
   * It initialize all the datasources. If multitenancy is activated it also
   * add dataSources for different tenants on tenantDbConfigs or
   * tenantDbConfigsOverride
   * 
   * @param tenantResolver
   * @throws IOException
   */
  private void initializeDataSources(BoneCPDataSource defaultDataSource) throws IOException {
    //Get the path where server is stored, we will save configurations there,
    //so if we redeploy it will not be deleted
    String catalinaBase = System.getProperties().getProperty("catalina.base");

    logger.info("MultiTenancy configuration: ");
    logger.info("---------------------------------------------------");

    // Add the default tenant and datasource
    addDataSource(DEFAULT_TENANTID, defaultDataSource);
    logger.info("Configuring default tenant: DefaultTenant - Properties: " + defaultDataSource.toString());

    // Add the other tenants
    logger.info("-- CLASSPATH TENANTS --");
    addTenantDataSources(defaultDataSource, tenantDbConfigs);
    logger.info("-- GLOBAL TENANTS --");
    addTenantDataSources(defaultDataSource, "file:" + catalinaBase + tenantDbConfigsOverride);
    logger.info("---------------------------------------------------");
  }

  /**
   * Add Tenant datasources based on the default properties on
   * defaultDataSource and the configurations in dbConfigs.
   * 
   * @param defaultDataSource
   * @param dbConfigs
   */
  private void addTenantDataSources(BoneCPDataSource defaultDataSource, String dbConfigs) {
    // Add the custom tenants and datasources
    Pattern tenantPattern = Pattern.compile(this.tenantRegex);
    PathMatchingResourcePatternResolver fileResolver = new PathMatchingResourcePatternResolver();

    InputStream dbProperties = null;

    try {
      Resource[] resources = fileResolver.getResources(dbConfigs);
      for (Resource resource : resources) {
        // load properties
        Properties props = new Properties(defaultDataSource.getClientInfo());
        dbProperties = resource.getInputStream();
        props.load(dbProperties);

        // Get tenantId using the filename and pattern
        String tenantId = getTenantId(tenantPattern, resource.getFilename());

        // Add new datasource with own configuration per tenant
        -BoneCPDataSource customDataSource = createTenantDataSource(props, defaultDataSource);
        addDataSource(tenantId, customDataSource); // It replace if tenantId was already there.

        logger.info("Configured tenant: " + tenantId + " - Properties: " + customDataSource.toString());

      }
    } catch (FileNotFoundException fnfe) {
      logger.warn("Not tenant configurations or path not found: " + fnfe.getMessage());
    } catch (IOException ioe) {
      logger.error("Error getting the tenants: " + ioe.getMessage());
    } finally {
      if (dbProperties != null) {
        try {
          dbProperties.close();
        } catch (IOException e) {
          logger.error("Error closing a property tenant: " + dbProperties.toString());
          e.printStackTrace();
        }
      }
    }
  }

  /**
   * Create a datasource with tenant properties, if a property is not found in Properties 
   * it takes the property from the defaultDataSource
   *
   * @param defaultDataSource a default datasource
   * @return a BoneCPDataSource based on tenant and default properties
   */
  private BoneCPDataSource createTenantDataSource(Properties tenantProps, BoneCPDataSource defaultDataSource)tenantProps)
  {
    BoneCPDataSource customDataSource = new BoneCPDataSource();

    //url, username and password must be unique per tenant so there is not default value
    customDataSource.setJdbcUrl(tenantProps.getProperty("database.url")); 
    customDataSource.setUsername(tenantProps.getProperty("database.username")); 
    customDataSource.setPassword(tenantProps.getProperty("database.password"));
    //These has default values in defaultDataSource
    customDataSource.setDriverClass(tenantProps.getProperty("database.driverClassName", defaultDataSource.getDriverClass()));
    customDataSource.setIdleConnectionTestPeriodInMinutes(Long.valueOf(tenantProps.getProperty(
      "database.idleConnectionTestPeriod",String.valueOf(defaultDataSource.getIdleConnectionTestPeriodInMinutes()))));
    customDataSource.setIdleMaxAgeInMinutes(Long.valueOf(tenantProps.getProperty(
      "database.idleMaxAge", String.valueOf(defaultDataSource.getIdleMaxAgeInMinutes()))));
    customDataSource.setMaxConnectionsPerPartition(Integer.valueOf(tenantProps.getProperty(
      "database.maxConnectionsPerPartition", String.valueOf(defaultDataSource.getMaxConnectionsPerPartition()))));
    customDataSource.setMinConnectionsPerPartition(Integer.valueOf(tenantProps.getProperty(
      "database.minConnectionsPerPartition", String.valueOf(defaultDataSource.getMinConnectionsPerPartition()))));
    customDataSource.setPartitionCount(Integer.valueOf(tenantProps.getProperty(
      "database.partitionCount", String.valueOf(defaultDataSource.getPartitionCount()))));
    customDataSource.setAcquireIncrement(Integer.valueOf(tenantProps.getProperty(
      "database.acquireIncrement", String.valueOf(defaultDataSource.getAcquireIncrement()))));
    customDataSource.setStatementsCacheSize(Integer.valueOf(tenantProps.getProperty(
      "database.statementsCacheSize",String.valueOf(defaultDataSource.getStatementCacheSize()))));
    customDataSource.setReleaseHelperThreads(Integer.valueOf(tenantProps.getProperty(
      "database.releaseHelperThreads", String.valueOf(defaultDataSource.getReleaseHelperThreads()))));customDataSource.setDriverClass(tenantProps.getProperty("database.driverClassName"));

    return customDataSource;
  }

  /**
   * Get the tenantId from filename using the pattern
   * 
   * @param tenantPattern
   * @param filename
   * @return tenantId
   * @throws IOException
   */
  private String getTenantId(Pattern tenantPattern, String filename) throws IOException {
    Matcher matcher = tenantPattern.matcher(filename);
    boolean findMatch = matcher.matches();
    if (!findMatch) {
      throw new IOException("Error reading tenant name in the filename");
    }
    return matcher.group(1);
  }

}


As there are comments in the code, I will focus just on the important parts.

Implementation Details

Firstly, we have some constants:

  • tenantDbConfigs: The directory used in development where tenant-files are located inside a folder of the project.
  • tenantDbConfigsOverride: The directory in production where tenant-files are located outside the project. In this way, if the application is redeployed it’s not deleted..
  • tenantRegex: Regular expression that we use to iterate about different tenant-files and get the tenantId.

It is in the constructor where we initialize the datasources, the method initializeDataSources(DataSource defaultDataSource) will put the defaultDataSource for the default tenant in the map firstly and then the others.

All the magic to get the tenants happens in addTenantDataSources(DataSource defaultDatasource, String folder). Basically, what we do is to use a regular expression to get the properties from each tenant in a folder. We also get the tenantId from the filename and override the DefaultDataSource properties with the ones in the property file of each tenantId. Finally, we add the entry to the map as <tenantId, DataSource>.

In this class we have two different processes:

  • Add the datasources for each tenant. These datasources will be used for Hibernate.
  • Get the properties for each tenant

The ideal should be to separate these processes: “Add datasources” and “get tenant properties.” This can be done by creating a new singleton class, PropertyManager, and using it in our MultiTenantDataSourceLookup. In this way, you could have every access to any property centralized in the PropertyManager. But for the sake of shortening and understanding this topic, I decided to post it in this way.

In future posts, we will talk about:

  • Hibernate schema export in multi-tenancy.
  • Different ways to save the tenant id in an HTTP session.

I hope that you find the article informative. If you have any questions please don’t hesitate to ask.

Spring Framework Hibernate Property (programming) Datasource

Published at DZone with permission of Jose Manuel García Maestre, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • OpenVPN With Radius and Multi-Factor Authentication
  • Strategies for Kubernetes Cluster Administrators: Understanding Pod Scheduling
  • Tackling the Top 5 Kubernetes Debugging Challenges
  • Integrate AWS Secrets Manager in Spring Boot Application

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: