HHH-13115 - Document how to define timezone per tenant when using Multitenant Database

This commit is contained in:
Vlad Mihalcea 2018-11-23 17:17:35 +02:00 committed by Guillaume Smet
parent 2823e98cd9
commit c9356ce9b4
3 changed files with 441 additions and 0 deletions

View File

@ -1,6 +1,7 @@
[[multitenacy]]
== Multitenancy
:sourcedir: ../../../../../test/java/org/hibernate/userguide/multitenancy
:extrasdir: extras
[[multitenacy-intro]]
=== What is multitenancy?
@ -180,3 +181,82 @@ Currently, schema export will not really work with multitenancy. That may not ch
The JPA expert group is in the process of defining multitenancy support for an upcoming version of the specification.
====
[[multitenacy-hibernate-session-configuration]]
==== Multitenancy Hibernate Session configuration
When using multitenancy, you might want to configure each tenant-specific `Session` differently.
For instance, each tenant could take a different time zone configuration.
[[multitenacy-hibernate-timezone-configuration-registerConnectionProvider-call-example]]
.Registering the tenant-specific time zone information
====
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-timezone-configuration-registerConnectionProvider-call-example]
----
====
The `registerConnectionProvider` method is used to define the tenant-specific context.
[[multitenacy-hibernate-timezone-configuration-registerConnectionProvider-example]]
.The `registerConnectionProvider` method used for defining the tenant-specific context
====
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-timezone-configuration-registerConnectionProvider-example]
----
====
For our example, the tenant-specific context is held in the `connectionProviderMap` and `timeZoneTenantMap`.
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-timezone-configuration-context-example]
----
Now, when building the Hibernate `Session`, aside from passing the tenant identifier,
we could also configure the `Session` to use the tenant-specific time zone.
[[multitenacy-hibernate-timezone-configuration-session-example]]
.The Hibernate `Session` can be configured using the tenant-specific context
====
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-timezone-configuration-session-example]
----
====
So, if we set the `useTenantTimeZone` parameter to `true`, Hibernate will persist the `Timestamp` properties using the
tenant-specific time zone. As you can see in the following example, the `Timestamp` is successfully retrieved
even if the currently running JVM uses a different time zone.
[[multitenacy-hibernate-applying-timezone-configuration-example]]
.The `useTenantTimeZone` allows you to persist a `Timestamp` in the provided time zone
====
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-applying-timezone-configuration-example]
----
====
However, behind the scenes, we can see that Hibernate has saved the `created_on` property in the tenant-specific time zone.
The following example shows you that the `Timestamp` was saved in the UTC time zone, hence the offset displayed in the
test output.
[[multitenacy-hibernate-not-applying-timezone-configuration-example]]
.With the `useTenantTimeZone` property set to `false`, the `Timestamp` in fetched in the tenant-specific time zone
====
[source, JAVA, indent=0]
----
include::{sourcedir}/DatabaseTimeZoneMultiTenancyTest.java[tags=multitenacy-hibernate-not-applying-timezone-configuration-example]
----
[source, SQL,indent=0]
----
include::{extrasdir}/multitenacy-hibernate-not-applying-timezone-configuration-example.sql[]
----
====
Notice that, for the `Eastern European Time` time zone, the time zone offset was 2 hours when the test was executed.

View File

@ -0,0 +1,12 @@
SELECT
p.created_on
FROM
Person p
WHERE
p.id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([CREATED_ON] : [TIMESTAMP]) - [2018-11-23 10:00:00.0]
-- The created_on timestamp value is: [2018-11-23 10:00:00.0]
-- For the current time zone: [Eastern European Time], the UTC time zone offset is: [7200000]

View File

@ -0,0 +1,349 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.userguide.multitenancy;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.function.Consumer;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.MultiTenancyStrategy;
import org.hibernate.Session;
import org.hibernate.SessionBuilder;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.service.spi.Stoppable;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.internal.SchemaCreatorImpl;
import org.hibernate.tool.schema.internal.SchemaDropperImpl;
import org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase;
import org.hibernate.testing.AfterClassOnce;
import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.hibernate.test.util.DdlTransactionIsolatorTestingImpl;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(H2Dialect.class)
public class DatabaseTimeZoneMultiTenancyTest extends BaseUnitTestCase {
protected static final String FRONT_END_TENANT = "front_end";
protected static final String BACK_END_TENANT = "back_end";
//tag::multitenacy-hibernate-timezone-configuration-context-example[]
private Map<String, ConnectionProvider> connectionProviderMap = new HashMap<>();
private Map<String, TimeZone> timeZoneTenantMap = new HashMap<>();
//end::multitenacy-hibernate-timezone-configuration-context-example[]
private SessionFactory sessionFactory;
public DatabaseTimeZoneMultiTenancyTest() {
init();
}
private void init() {
//tag::multitenacy-hibernate-timezone-configuration-registerConnectionProvider-call-example[]
registerConnectionProvider( FRONT_END_TENANT, TimeZone.getTimeZone( "UTC" ) );
registerConnectionProvider( BACK_END_TENANT, TimeZone.getTimeZone( "CST" ) );
//end::multitenacy-hibernate-timezone-configuration-registerConnectionProvider-call-example[]
Map<String, Object> settings = new HashMap<>();
settings.put( AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.DATABASE );
settings.put(
AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER,
new ConfigurableMultiTenantConnectionProvider( connectionProviderMap )
);
sessionFactory = sessionFactory( settings );
}
@AfterClassOnce
public void destroy() {
sessionFactory.close();
for ( ConnectionProvider connectionProvider : connectionProviderMap.values() ) {
if ( connectionProvider instanceof Stoppable ) {
( (Stoppable) connectionProvider ).stop();
}
}
}
//tag::multitenacy-hibernate-timezone-configuration-registerConnectionProvider-example[]
protected void registerConnectionProvider(String tenantIdentifier, TimeZone timeZone) {
Properties properties = properties();
properties.put(
Environment.URL,
tenantUrl( properties.getProperty( Environment.URL ), tenantIdentifier )
);
DriverManagerConnectionProviderImpl connectionProvider =
new DriverManagerConnectionProviderImpl();
connectionProvider.configure( properties );
connectionProviderMap.put( tenantIdentifier, connectionProvider );
timeZoneTenantMap.put( tenantIdentifier, timeZone );
}
//end::multitenacy-hibernate-timezone-configuration-registerConnectionProvider-example[]
@Test
public void testBasicExpectedBehavior() {
//tag::multitenacy-hibernate-applying-timezone-configuration-example[]
doInSession( FRONT_END_TENANT, session -> {
Person person = new Person();
person.setId( 1L );
person.setName( "John Doe" );
person.setCreatedOn( LocalDateTime.of( 2018, 11, 23, 12, 0, 0 ) );
session.persist( person );
}, true );
doInSession( BACK_END_TENANT, session -> {
Person person = new Person();
person.setId( 1L );
person.setName( "John Doe" );
person.setCreatedOn( LocalDateTime.of( 2018, 11, 23, 12, 0, 0 ) );
session.persist( person );
}, true );
doInSession( FRONT_END_TENANT, session -> {
Timestamp personCreationTimestamp = (Timestamp) session
.createNativeQuery(
"select p.created_on " +
"from Person p " +
"where p.id = :personId" )
.setParameter( "personId", 1L )
.getSingleResult();
assertEquals(
Timestamp.valueOf( LocalDateTime.of( 2018, 11, 23, 12, 0, 0 ) ),
personCreationTimestamp
);
}, true );
doInSession( BACK_END_TENANT, session -> {
Timestamp personCreationTimestamp = (Timestamp) session
.createNativeQuery(
"select p.created_on " +
"from Person p " +
"where p.id = :personId" )
.setParameter( "personId", 1L )
.getSingleResult();
assertEquals(
Timestamp.valueOf( LocalDateTime.of( 2018, 11, 23, 12, 0, 0 ) ),
personCreationTimestamp
);
}, true );
//end::multitenacy-hibernate-applying-timezone-configuration-example[]
//tag::multitenacy-hibernate-not-applying-timezone-configuration-example[]
doInSession( FRONT_END_TENANT, session -> {
Timestamp personCreationTimestamp = (Timestamp) session
.createNativeQuery(
"select p.created_on " +
"from Person p " +
"where p.id = :personId" )
.setParameter( "personId", 1L )
.getSingleResult();
log.infof(
"The created_on timestamp value is: [%s]",
personCreationTimestamp
);
long timeZoneOffsetMillis =
Timestamp.valueOf( LocalDateTime.of( 2018, 11, 23, 12, 0, 0 ) ).getTime() -
personCreationTimestamp.getTime();
assertEquals(
TimeZone.getTimeZone(ZoneId.systemDefault()).getRawOffset(),
timeZoneOffsetMillis
);
log.infof(
"For the current time zone: [%s], the UTC time zone offset is: [%d]",
TimeZone.getDefault().getDisplayName(), timeZoneOffsetMillis
);
}, false );
//end::multitenacy-hibernate-not-applying-timezone-configuration-example[]
}
protected Properties properties() {
Properties properties = new Properties();
URL propertiesURL = Thread.currentThread().getContextClassLoader().getResource( "hibernate.properties" );
try (FileInputStream inputStream = new FileInputStream( propertiesURL.getFile() )) {
properties.load( inputStream );
}
catch (IOException e) {
throw new IllegalArgumentException( e );
}
return properties;
}
protected String tenantUrl(String originalUrl, String tenantIdentifier) {
return originalUrl.replace( "db1", tenantIdentifier );
}
protected SessionFactory sessionFactory(Map<String, Object> settings) {
ServiceRegistryImplementor serviceRegistry = (ServiceRegistryImplementor) new StandardServiceRegistryBuilder()
.applySettings( settings )
.build();
MetadataSources metadataSources = new MetadataSources( serviceRegistry );
for ( Class annotatedClasses : getAnnotatedClasses() ) {
metadataSources.addAnnotatedClass( annotatedClasses );
}
Metadata metadata = metadataSources.buildMetadata();
HibernateSchemaManagementTool tool = new HibernateSchemaManagementTool();
tool.injectServices( serviceRegistry );
final GenerationTargetToDatabase frontEndSchemaGenerator = new GenerationTargetToDatabase(
new DdlTransactionIsolatorTestingImpl(
serviceRegistry,
connectionProviderMap.get( FRONT_END_TENANT )
)
);
final GenerationTargetToDatabase backEndSchemaGenerator = new GenerationTargetToDatabase(
new DdlTransactionIsolatorTestingImpl(
serviceRegistry,
connectionProviderMap.get( BACK_END_TENANT )
)
);
new SchemaDropperImpl( serviceRegistry ).doDrop(
metadata,
serviceRegistry,
settings,
true,
frontEndSchemaGenerator,
backEndSchemaGenerator
);
new SchemaCreatorImpl( serviceRegistry ).doCreation(
metadata,
serviceRegistry,
settings,
true,
frontEndSchemaGenerator,
backEndSchemaGenerator
);
final SessionFactoryBuilder sessionFactoryBuilder = metadata.getSessionFactoryBuilder();
return sessionFactoryBuilder.build();
}
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
Person.class
};
}
//tag::multitenacy-hibernate-timezone-configuration-session-example[]
private void doInSession(String tenant, Consumer<Session> function, boolean useTenantTimeZone) {
Session session = null;
Transaction txn = null;
try {
SessionBuilder sessionBuilder = sessionFactory
.withOptions()
.tenantIdentifier( tenant );
if ( useTenantTimeZone ) {
sessionBuilder.jdbcTimeZone( timeZoneTenantMap.get( tenant ) );
}
session = sessionBuilder.openSession();
txn = session.getTransaction();
txn.begin();
function.accept( session );
txn.commit();
}
catch (Throwable e) {
if ( txn != null ) {
txn.rollback();
}
throw e;
}
finally {
if ( session != null ) {
session.close();
}
}
}
//end::multitenacy-hibernate-timezone-configuration-session-example[]
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@Column(name = "created_on")
private LocalDateTime createdOn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDateTime getCreatedOn() {
return createdOn;
}
public void setCreatedOn(LocalDateTime createdOn) {
this.createdOn = createdOn;
}
}
}