HHH-13115 - Document how to define timezone per tenant when using Multitenant Database
This commit is contained in:
parent
2823e98cd9
commit
c9356ce9b4
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue