HHH-7011 - Document multi-tenancy
This commit is contained in:
parent
89911003e3
commit
8bead4f084
|
@ -21,6 +21,7 @@
|
|||
<xi:include href="Native_SQL.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<xi:include href="JMX.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<xi:include href="Envers.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<xi:include href="chapters/multi-tenancy/Multi_Tenancy.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<xi:include href="appendix-Configuration_Properties.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<xi:include href="Revision_History.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
|
||||
<index />
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
<?xml version='1.0' encoding='utf-8' ?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
|
||||
<!ENTITY % BOOK_ENTITIES SYSTEM "../../Hibernate_Development_Guide.ent">
|
||||
%BOOK_ENTITIES;
|
||||
]>
|
||||
|
||||
<chapter>
|
||||
<title>Multi-tenancy</title>
|
||||
|
||||
<section>
|
||||
<title>What is multi-tenancy?</title>
|
||||
<para>
|
||||
The term multi-tenancy in general is applied to software development to indicate an architecture in which
|
||||
a single running instance of an application simultaneously serves multiple clients (tenants). This is
|
||||
highly common in SaaS solutions. Isolating information (data, customizations, etc) pertaining to the
|
||||
various tenants is a particular challenge in these systems. This includes the data owned by each tenant
|
||||
stored in the database. It is this last piece, sometimes called multi-tenant data, on which we will focus.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Multi-tenant data approaches</title>
|
||||
<para>
|
||||
There are 3 main approaches to isolating information in these multi-tenant systems.
|
||||
</para>
|
||||
|
||||
<section>
|
||||
<title>Separate database</title>
|
||||
<para>
|
||||
Each tenant's data is kept in a physically separate database instance. Generally an application
|
||||
would define a JDBC Connection pool per tenant and select the pool based on the tenant of the currently
|
||||
logged in user.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Separate schema</title>
|
||||
<para>
|
||||
Each tenant's data is kept in a distinct database schema on a single database instance. An application
|
||||
could choose to either:
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
use distinct JDBC Connection pool per tenant where the schema is part of the URL or
|
||||
otherwise specified to the Connection pool
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
use a single Connection pool but point the Connection to the correct schema based on the
|
||||
tenant of currently logged in user based on ALTER SESSION command or similar
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Partitioned (discriminator) data</title>
|
||||
<para>
|
||||
All data is kept in a single database schema. The data for each tenant is partitioned by the use of
|
||||
partition value or discriminator. The complexity of this discriminator might range from a simple
|
||||
column value to a complex SQL formula.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Multi-tenancy in Hibernate</title>
|
||||
<para>
|
||||
Using Hibernate with multi-tenant data comes down to both an API and then integration piece(s). As
|
||||
usual Hibernate strives to keep the API simple and isolated from any underlying integration complexities.
|
||||
The API is really just defined by passing the tenant identifier as part of opening any session.
|
||||
</para>
|
||||
<example>
|
||||
<title>Specifying tenant identifier from <interfacename>SessionFactory</interfacename></title>
|
||||
<programlisting role="JAVA"><xi:include href="extras/tenant-identifier-from-SessionFactory.java" xmlns:xi="http://www.w3.org/2001/XInclude" parse="text"/></programlisting>
|
||||
</example>
|
||||
<para>
|
||||
Additionally, when specifying configuration, a <classname>org.hibernate.MultiTenancyStrategy</classname>
|
||||
should be named using the <property>hibernate.multiTenancy</property> setting. Hibernate will perform
|
||||
validations based on the type of strategy you specify. The strategy here correlates to the isolation
|
||||
approach discussed above.
|
||||
</para>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term>NONE</term>
|
||||
<listitem>
|
||||
<para>
|
||||
(the default) No multi-tenancy is expected. In fact, it is considered an error if a tenant
|
||||
identifier is specified when opening a session using this strategy.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>SCHEMA</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Correlates to the separate schema approach. It is an error to attempt to open a session without
|
||||
a tenant identifier using this strategy. Additionally, a
|
||||
<interfacename>org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider</interfacename>
|
||||
must be specified.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>DATABASE</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Correlates to the separate database approach. It is an error to attempt to open a session without
|
||||
a tenant identifier using this strategy. Additionally, a
|
||||
<interfacename>org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider</interfacename>
|
||||
must be specified.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>DISCRIMINATOR</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Correlates to the partitioned (discriminator) approach. It is an error to attempt to open a
|
||||
session without a tenant identifier using this strategy.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<section>
|
||||
<title><interfacename>MultiTenantConnectionProvider</interfacename></title>
|
||||
<para>
|
||||
When using either the DATABASE or SCHEMA approach, Hibernate needs to be able to obtain Connections
|
||||
in a tenant specific manner. That is the role of the
|
||||
<interfacename>org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider</interfacename>
|
||||
contract. Application developers will need to provide an implementation of this
|
||||
contract. Most of its methods are extremely self-explanatory. The only ones which might not be are
|
||||
<methodname>getAnyConnection</methodname> and <methodname>releaseAnyConnection</methodname>. It is
|
||||
important to note also that these methods do not accept the tenant identifier. Hibernate uses these
|
||||
methods during startup to perform various configuration, mainly via the
|
||||
<classname>java.sql.DatabaseMetaData</classname> object.
|
||||
</para>
|
||||
<para>
|
||||
The <interfacename>MultiTenantConnectionProvider</interfacename> to use can be specified in a number of
|
||||
ways:
|
||||
</para>
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
Use the <property>hibernate.multi_tenant_connection_provider</property> setting. It could
|
||||
name a <interfacename>MultiTenantConnectionProvider</interfacename> instance, a
|
||||
<interfacename>MultiTenantConnectionProvider</interfacename> implementation class reference or
|
||||
a <interfacename>MultiTenantConnectionProvider</interfacename> implementation class name.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
Passed directly to the <classname>org.hibernate.service.ServiceRegistryBuilder</classname>.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
If none of the above options match, but the settings do specify a
|
||||
<property>hibernate.connection.datasource</property> value, Hibernate will assume it should
|
||||
use the specific
|
||||
<classname>org.hibernate.service.jdbc.connections.spi.DataSourceBasedMultiTenantConnectionProviderImpl</classname>
|
||||
implementation which works on a number of pretty reasonable assumptions when running inside of
|
||||
an app server and using one <interfacename>javax.sql.DataSource</interfacename> per tenant.
|
||||
See its javadocs for more details.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title><interfacename>CurrentTenantIdentifierResolver</interfacename></title>
|
||||
<para>
|
||||
When applications use either the standard jta or thread based implementations of the
|
||||
<interfacename>org.hibernate.context.spi.CurrentSessionContext</interfacename> feature, Hibernate will
|
||||
need to open a session if it cannot find an existing one in scope. However, when a session is opened
|
||||
in a multi-tenant environment the tenant identifier has to be specified. This is the role of the
|
||||
<interfacename>org.hibernate.context.spi.CurrentTenantIdentifierResolver</interfacename> contract. It
|
||||
will resolve the tenant identifier to use. The implementation to use is either passed directly to
|
||||
<classname>Configuration</classname> via its
|
||||
<methodname>setCurrentTenantIdentifierResolver</methodname> method. It can also be specified via
|
||||
the <property>hibernate.tenant_identifier_resolver</property> setting.
|
||||
</para>
|
||||
<para>
|
||||
Additionally, if the <interfacename>CurrentTenantIdentifierResolver</interfacename> implementation
|
||||
returns <literal>true</literal> for its <methodname>validateExistingCurrentSessions</methodname>
|
||||
method, Hibernate will make sure any existing sessions that are found in scope have a matching
|
||||
tenant identifier.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Caching</title>
|
||||
<para>
|
||||
Multi-tenancy support in Hibernate works seamlessly with the Hibernate second level cache. The key
|
||||
used to cache data encodes the tenant identifier.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Odds and ends</title>
|
||||
<para>
|
||||
Currently schema export will not really work with multi-tenancy. That may not change.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Strategies for <interfacename>MultiTenantConnectionProvider</interfacename> implementors</title>
|
||||
<example>
|
||||
<title>Implementing MultiTenantConnectionProvider using different connection pools</title>
|
||||
<programlisting role="JAVA"><xi:include href="extras/MultiTenantConnectionProviderImpl-multi-cp.java" xmlns:xi="http://www.w3.org/2001/XInclude" parse="text"/></programlisting>
|
||||
</example>
|
||||
<para>
|
||||
The approach above is valid for the DATABASE approach. It is also valid for the SCHEMA approach
|
||||
provided the underlying database allows naming the schema to which to connect in the connection URL.
|
||||
</para>
|
||||
<example>
|
||||
<title>Implementing MultiTenantConnectionProvider using single connection pool</title>
|
||||
<programlisting role="JAVA"><xi:include href="extras/MultiTenantConnectionProviderImpl-single-cp.java" xmlns:xi="http://www.w3.org/2001/XInclude" parse="text"/></programlisting>
|
||||
</example>
|
||||
<para>
|
||||
This approach is only relevant to the SCHEMA approach.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Simplisitc implementation for illustration purposes supporting 2 hard coded providers (pools) and leveraging
|
||||
* the support class {@link org.hibernate.service.jdbc.connections.spi.AbstractMultiTenantConnectionProvider}
|
||||
*/
|
||||
public class MultiTenantConnectionProviderImpl extends AbstractMultiTenantConnectionProvider {
|
||||
private final ConnectionProvider acmeProvider = ConnectionProviderUtils.buildConnectionProvider( "acme" );
|
||||
private final ConnectionProvider jbossProvider = ConnectionProviderUtils.buildConnectionProvider( "jboss" );
|
||||
|
||||
@Override
|
||||
protected ConnectionProvider getAnyConnectionProvider() {
|
||||
return acmeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
|
||||
if ( "acme".equals( tenantIdentifier ) ) {
|
||||
return acmeProvider;
|
||||
}
|
||||
else if ( "jboss".equals( tenantIdentifier ) ) {
|
||||
return jbossProvider;
|
||||
}
|
||||
throw new HibernateException( "Unknown tenant identifier" );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Simplisitc implementation for illustration purposes showing a single connection pool used to serve
|
||||
* multiple schemas using "connection altering". Here we use the T-SQL specific USE command; Oracle
|
||||
* users might use the ALTER SESSION SET SCHEMA command; etc.
|
||||
*/
|
||||
public class MultiTenantConnectionProviderImpl
|
||||
implements MultiTenantConnectionProvider, Stoppable {
|
||||
private final ConnectionProvider connectionProvider = ConnectionProviderUtils.buildConnectionProvider( "master" );
|
||||
|
||||
@Override
|
||||
public Connection getAnyConnection() throws SQLException {
|
||||
return connectionProvider.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseAnyConnection(Connection connection) throws SQLException {
|
||||
connectionProvider.closeConnection( connection );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String tenantIdentifier) throws SQLException {
|
||||
final Connection connection = getAnyConnection();
|
||||
try {
|
||||
connection.createStatement().execute( "USE " + tenanantIdentifier );
|
||||
}
|
||||
catch ( SQLException e ) {
|
||||
throw new HibernateException(
|
||||
"Could not alter JDBC connection to specified schema [" +
|
||||
tenantIdentifier + "]",
|
||||
e
|
||||
);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
|
||||
try {
|
||||
connection.createStatement().execute( "USE master" );
|
||||
}
|
||||
catch ( SQLException e ) {
|
||||
// on error, throw an exception to make sure the connection is not returned to the pool.
|
||||
// your requirements may differ
|
||||
throw new HibernateException(
|
||||
"Could not alter JDBC connection to specified schema [" +
|
||||
tenantIdentifier + "]",
|
||||
e
|
||||
);
|
||||
}
|
||||
connectionProvider.closeConnection( connection );
|
||||
}
|
||||
|
||||
...
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Session session = sessionFactory.withOptions()
|
||||
.tenantIdentifier( yourTenantIdentifier )
|
||||
...
|
||||
.openSession();
|
|
@ -466,14 +466,6 @@ public interface AvailableSettings {
|
|||
*/
|
||||
public static final String NON_CONTEXTUAL_LOB_CREATION = "hibernate.jdbc.lob.non_contextual_creation";
|
||||
|
||||
/**
|
||||
* Strategy for multi-tenancy.
|
||||
|
||||
* @see org.hibernate.MultiTenancyStrategy
|
||||
* @since 4.0
|
||||
*/
|
||||
public static final String MULTI_TENANT = "hibernate.multiTenancy";
|
||||
|
||||
/**
|
||||
* Names the {@link ClassLoader} used to load user application classes.
|
||||
* @since 4.0
|
||||
|
@ -547,7 +539,35 @@ public interface AvailableSettings {
|
|||
*/
|
||||
public static final String CUSTOM_ENTITY_DIRTINESS_STRATEGY = "hibernate.entity_dirtiness_strategy";
|
||||
|
||||
public static final String TENANT_IDENTIFIER_RESOLVER = "hibernate.tenant_identifier_resolver";
|
||||
/**
|
||||
* Strategy for multi-tenancy.
|
||||
|
||||
* @see org.hibernate.MultiTenancyStrategy
|
||||
* @since 4.0
|
||||
*/
|
||||
public static final String MULTI_TENANT = "hibernate.multiTenancy";
|
||||
|
||||
/**
|
||||
* Names a {@link org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider} implementation to
|
||||
* use. As MultiTenantConnectionProvider is also a service, can be configured directly through the
|
||||
* {@link org.hibernate.service.ServiceRegistryBuilder}
|
||||
*
|
||||
* @since 4.1
|
||||
*/
|
||||
public static final String MULTI_TENANT_CONNECTION_PROVIDER = "hibernate.multi_tenant_connection_provider";
|
||||
|
||||
/**
|
||||
* Names a {@link org.hibernate.context.spi.CurrentTenantIdentifierResolver} implementation to use.
|
||||
* <p/>
|
||||
* Can be<ul>
|
||||
* <li>CurrentTenantIdentifierResolver instance</li>
|
||||
* <li>CurrentTenantIdentifierResolver implementation {@link Class} reference</li>
|
||||
* <li>CurrentTenantIdentifierResolver implementation class name</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 4.1
|
||||
*/
|
||||
public static final String MULTI_TENANT_IDENTIFIER_RESOLVER = "hibernate.tenant_identifier_resolver";
|
||||
|
||||
public static final String FORCE_DISCRIMINATOR_IN_SELECTS_BY_DEFAULT = "hibernate.discriminator.force_in_select";
|
||||
}
|
||||
|
|
|
@ -618,7 +618,7 @@ public final class SessionFactoryImpl
|
|||
return explicitResolver;
|
||||
}
|
||||
|
||||
final Object value = properties.get( AvailableSettings.TENANT_IDENTIFIER_RESOLVER );
|
||||
final Object value = properties.get( AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER );
|
||||
if ( value == null ) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -100,8 +100,10 @@ public class ConnectionProviderInitiator implements BasicServiceInitiator<Connec
|
|||
|
||||
@Override
|
||||
public ConnectionProvider initiateService(Map configurationValues, ServiceRegistryImplementor registry) {
|
||||
if ( MultiTenancyStrategy.determineMultiTenancyStrategy( configurationValues ) != MultiTenancyStrategy.NONE ) {
|
||||
final MultiTenancyStrategy strategy = MultiTenancyStrategy.determineMultiTenancyStrategy( configurationValues );
|
||||
if ( strategy == MultiTenancyStrategy.DATABASE || strategy == MultiTenancyStrategy.SCHEMA ) {
|
||||
// nothing to do, but given the separate hierarchies have to handle this here.
|
||||
return null;
|
||||
}
|
||||
|
||||
final ClassLoaderService classLoaderService = registry.getService( ClassLoaderService.class );
|
||||
|
|
|
@ -25,7 +25,13 @@ package org.hibernate.service.jdbc.connections.internal;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.hibernate.MultiTenancyStrategy;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.service.classloading.spi.ClassLoaderService;
|
||||
import org.hibernate.service.classloading.spi.ClassLoadingException;
|
||||
import org.hibernate.service.jdbc.connections.spi.DataSourceBasedMultiTenantConnectionProviderImpl;
|
||||
import org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider;
|
||||
import org.hibernate.service.spi.BasicServiceInitiator;
|
||||
import org.hibernate.service.spi.ServiceRegistryImplementor;
|
||||
|
@ -35,6 +41,7 @@ import org.hibernate.service.spi.ServiceRegistryImplementor;
|
|||
*/
|
||||
public class MultiTenantConnectionProviderInitiator implements BasicServiceInitiator<MultiTenantConnectionProvider> {
|
||||
public static final MultiTenantConnectionProviderInitiator INSTANCE = new MultiTenantConnectionProviderInitiator();
|
||||
private static final Logger log = Logger.getLogger( MultiTenantConnectionProviderInitiator.class );
|
||||
|
||||
@Override
|
||||
public Class<MultiTenantConnectionProvider> getServiceInitiated() {
|
||||
|
@ -42,12 +49,52 @@ public class MultiTenantConnectionProviderInitiator implements BasicServiceIniti
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings( {"unchecked"})
|
||||
public MultiTenantConnectionProvider initiateService(Map configurationValues, ServiceRegistryImplementor registry) {
|
||||
if ( MultiTenancyStrategy.determineMultiTenancyStrategy( configurationValues ) == MultiTenancyStrategy.NONE ) {
|
||||
final MultiTenancyStrategy strategy = MultiTenancyStrategy.determineMultiTenancyStrategy( configurationValues );
|
||||
if ( strategy == MultiTenancyStrategy.NONE || strategy == MultiTenancyStrategy.DISCRIMINATOR ) {
|
||||
// nothing to do, but given the separate hierarchies have to handle this here.
|
||||
}
|
||||
|
||||
// for now...
|
||||
return null;
|
||||
final Object configValue = configurationValues.get( AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER );
|
||||
if ( configValue == null ) {
|
||||
// if they also specified the data source *name*, then lets assume they want
|
||||
// org.hibernate.service.jdbc.connections.spi.DataSourceBasedMultiTenantConnectionProviderImpl
|
||||
final Object dataSourceConfigValue = configurationValues.get( AvailableSettings.DATASOURCE );
|
||||
if ( dataSourceConfigValue != null && String.class.isInstance( dataSourceConfigValue ) ) {
|
||||
return new DataSourceBasedMultiTenantConnectionProviderImpl();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( MultiTenantConnectionProvider.class.isInstance( configValue ) ) {
|
||||
return (MultiTenantConnectionProvider) configValue;
|
||||
}
|
||||
else {
|
||||
final Class<MultiTenantConnectionProvider> implClass;
|
||||
if ( Class.class.isInstance( configValue ) ) {
|
||||
implClass = (Class) configValue;
|
||||
}
|
||||
else {
|
||||
final String className = configValue.toString();
|
||||
final ClassLoaderService classLoaderService = registry.getService( ClassLoaderService.class );
|
||||
try {
|
||||
implClass = classLoaderService.classForName( className );
|
||||
}
|
||||
catch (ClassLoadingException cle) {
|
||||
log.warn( "Unable to locate specified class [" + className + "]", cle );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return implClass.newInstance();
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn( "Unable to instantiate specified class [" + implClass.getName() + "]", e );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
|
||||
* indicated by the @author tags or express copyright attribution
|
||||
* statements applied by the authors. All third-party contributions are
|
||||
* distributed under license by Red Hat Inc.
|
||||
*
|
||||
* This copyrighted material is made available to anyone wishing to use, modify,
|
||||
* copy, or redistribute it subject to the terms and conditions of the GNU
|
||||
* Lesser General Public License, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this distribution; if not, write to:
|
||||
* Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor
|
||||
* Boston, MA 02110-1301 USA
|
||||
*/
|
||||
package org.hibernate.service.jdbc.connections.spi;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.hibernate.service.UnknownUnwrapTypeException;
|
||||
|
||||
/**
|
||||
* Basic support for implementations of {@link MultiTenantConnectionProvider} based on DataSources.
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public abstract class AbstractDataSourceBasedMultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
|
||||
protected abstract DataSource selectAnyDataSource();
|
||||
protected abstract DataSource selectDataSource(String tenantIdentifier);
|
||||
|
||||
@Override
|
||||
public Connection getAnyConnection() throws SQLException {
|
||||
return selectAnyDataSource().getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseAnyConnection(Connection connection) throws SQLException {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String tenantIdentifier) throws SQLException {
|
||||
return selectDataSource( tenantIdentifier ).getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAggressiveRelease() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnwrappableAs(Class unwrapType) {
|
||||
return MultiTenantConnectionProvider.class.equals( unwrapType ) ||
|
||||
AbstractMultiTenantConnectionProvider.class.isAssignableFrom( unwrapType );
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings( {"unchecked"})
|
||||
public <T> T unwrap(Class<T> unwrapType) {
|
||||
if ( isUnwrappableAs( unwrapType ) ) {
|
||||
return (T) this;
|
||||
}
|
||||
else {
|
||||
throw new UnknownUnwrapTypeException( unwrapType );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
|
||||
* indicated by the @author tags or express copyright attribution
|
||||
* statements applied by the authors. All third-party contributions are
|
||||
* distributed under license by Red Hat Inc.
|
||||
*
|
||||
* This copyrighted material is made available to anyone wishing to use, modify,
|
||||
* copy, or redistribute it subject to the terms and conditions of the GNU
|
||||
* Lesser General Public License, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this distribution; if not, write to:
|
||||
* Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor
|
||||
* Boston, MA 02110-1301 USA
|
||||
*/
|
||||
package org.hibernate.service.jdbc.connections.spi;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.service.config.spi.ConfigurationService;
|
||||
import org.hibernate.service.jndi.spi.JndiService;
|
||||
import org.hibernate.service.spi.ServiceRegistryAwareService;
|
||||
import org.hibernate.service.spi.ServiceRegistryImplementor;
|
||||
import org.hibernate.service.spi.Stoppable;
|
||||
|
||||
/**
|
||||
* A concrete implementation of the {@link MultiTenantConnectionProvider} contract bases on a number of
|
||||
* reasonable assumptions. We assume that:<ul>
|
||||
* <li>
|
||||
* The {@link DataSource} instances are all available from JNDI named by the tenant identifier relative
|
||||
* to a single base JNDI context
|
||||
* </li>
|
||||
* <li>
|
||||
* {@link org.hibernate.cfg.AvailableSettings#DATASOURCE} is a string naming either the {@literal any}
|
||||
* data source or the base JNDI context. If the latter, {@link #TENANT_IDENTIFIER_TO_USE_FOR_ANY_KEY} must
|
||||
* also be set.
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class DataSourceBasedMultiTenantConnectionProviderImpl
|
||||
extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl
|
||||
implements ServiceRegistryAwareService, Stoppable {
|
||||
|
||||
public static final String TENANT_IDENTIFIER_TO_USE_FOR_ANY_KEY = "hibernate.multi_tenant.datasource.identifier_for_any";
|
||||
|
||||
private Map<String,DataSource> dataSourceMap;
|
||||
private JndiService jndiService;
|
||||
private String tenantIdentifierForAny;
|
||||
private String baseJndiNamespace;
|
||||
|
||||
@Override
|
||||
protected DataSource selectAnyDataSource() {
|
||||
return selectDataSource( tenantIdentifierForAny );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DataSource selectDataSource(String tenantIdentifier) {
|
||||
DataSource dataSource = dataSourceMap().get( tenantIdentifier );
|
||||
if ( dataSource == null ) {
|
||||
dataSource = (DataSource) jndiService.locate( baseJndiNamespace + '/' + tenantIdentifier );
|
||||
dataSourceMap().put( tenantIdentifier, dataSource );
|
||||
}
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
private Map<String,DataSource> dataSourceMap() {
|
||||
if ( dataSourceMap == null ) {
|
||||
dataSourceMap = new ConcurrentHashMap<String, DataSource>();
|
||||
}
|
||||
return dataSourceMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectServices(ServiceRegistryImplementor serviceRegistry) {
|
||||
final Object dataSourceConfigValue = serviceRegistry.getService( ConfigurationService.class )
|
||||
.getSettings()
|
||||
.get( AvailableSettings.DATASOURCE );
|
||||
if ( dataSourceConfigValue == null || ! String.class.isInstance( dataSourceConfigValue ) ) {
|
||||
throw new HibernateException( "Improper set up of DataSourceBasedMultiTenantConnectionProviderImpl" );
|
||||
}
|
||||
final String jndiName = (String) dataSourceConfigValue;
|
||||
|
||||
jndiService = serviceRegistry.getService( JndiService.class );
|
||||
if ( jndiService == null ) {
|
||||
throw new HibernateException( "Could not locate JndiService from DataSourceBasedMultiTenantConnectionProviderImpl" );
|
||||
}
|
||||
|
||||
Object namedObject = jndiService.locate( jndiName );
|
||||
if ( namedObject == null ) {
|
||||
throw new HibernateException( "JNDI name [" + jndiName + "] could not be resolved" );
|
||||
}
|
||||
|
||||
if ( DataSource.class.isInstance( namedObject ) ) {
|
||||
int loc = jndiName.lastIndexOf( "/" );
|
||||
this.baseJndiNamespace = jndiName.substring( 0, loc );
|
||||
this.tenantIdentifierForAny = jndiName.substring( loc + 1 );
|
||||
dataSourceMap().put( tenantIdentifierForAny, (DataSource) namedObject );
|
||||
}
|
||||
else if ( Context.class.isInstance( namedObject ) ) {
|
||||
this.baseJndiNamespace = jndiName;
|
||||
this.tenantIdentifierForAny = (String) serviceRegistry.getService( ConfigurationService.class )
|
||||
.getSettings()
|
||||
.get( TENANT_IDENTIFIER_TO_USE_FOR_ANY_KEY );
|
||||
if ( tenantIdentifierForAny == null ) {
|
||||
throw new HibernateException( "JNDI name named a Context, but tenant identifier to use for ANY was not specified" );
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new HibernateException(
|
||||
"Unknown object type [" + namedObject.getClass().getName() +
|
||||
"] found in JNDI location [" + jndiName + "]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if ( dataSourceMap != null ) {
|
||||
dataSourceMap.clear();
|
||||
dataSourceMap = null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue