Link to home
Start Free TrialLog in
Avatar of alcani
alcani

asked on

Schema Multi-Tenancy

Hello Experts.

We are developing a project using Spring Roo with Spring Security, Spring Mvc for the view and JPA with Hibernate  (4.2.2) ORM. All with the last version available.

Our Development IDE is Spring STS Version: 3.3.0.RELEASE, Build Id: 201307091644 . Our application server is Tomcat 7.0.42

In fact, the project it's composed by two sub-projects: one that manages the clients, and another one where each client log in to work.

We need to find a way to separate each client data: Multy-Tenancy.

I know there are three ways to do it: a) in the same table with an ID,  b)with different tables with a prefix and c) changing the database schema.

We are using PostgreSql for the database. I'ts easy to manage each schema separately, for example backup/restore process, that's why we prefer to use the "c" option.

We want to prevent as much as possible an event in which one client could see other client's information. (Even by a programming mistake)

We found these two examples:

http://blog.trixi.cz/2012/01/multitenancy-using-spring-and-postgresql/
http://www.developer.com/java/ent/extend-spring-security-to-protect-multi-tenant-saas-applications.html 

The problem is they are incomplete and we are new to these technologies (Spring and it's sub-projects)

When a new client self-register's it would be great if all it's schema could be created automatically.

I wanted to upload an example project, but the site doesn't let me (files with ".classpath", for example)

Here is an example Spring Roo script for the client sub-project:

------------------------------- START -------------------------------
// Spring Roo 1.2.4.RELEASE [rev 75337cf] log opened at 2013-09-09 08:20:44
project --topLevelPackage com.i4b.TestMutliTenant --projectName TestMultiTenant --java 7 --packaging WAR
// Spring Roo 1.2.4.RELEASE [rev 75337cf] log closed at 2013-09-09 08:20:54
// Spring Roo 1.2.4.RELEASE [rev 75337cf] log opened at 2013-09-09 08:21:02
jpa setup --database HYPERSONIC_IN_MEMORY --provider HIBERNATE
entity jpa --class ~.domain.MetodoPago --testAutomatically --entityName METODOS_PAGO
field string    --fieldName idMetodoPago --notNull --sizeMin 2 --sizeMax 5 --column METODO_PAGO --unique
field string    --fieldName descripcion --notNull --sizeMin 2 --sizeMax 50 --column DESCRIPCION
web mvc setup
web mvc all --package ~.domain.web
web mvc language --code es
web mvc finder all
security setup
// Spring Roo 1.2.4.RELEASE [rev 75337cf] log closed at 2013-09-09 08:38:03
------------------------------- END -------------------------------


I hope someone can shed some light about it.

Thank you.
Avatar of mrcoffee365
mrcoffee365
Flag of United States of America image

It would help if you could ask a specific question.

What have you tried so far?  What error messages are you getting?  What about the created schemas is not working for you?
Avatar of alcani
alcani

ASKER

Hi.

I'm now facing some problems with this:

a) As the first link states, there is the need to have a "TenantContext" bean with a "session" scope. The problem is no matter what I do, I always get the exception I copy at the end of this message. Just like the comments in the same web page.

Here is the scope documentation: http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/beans.html#beans-factory-scopes-other

b) Didn't know how to implement the createDataSource and other methods. Then I decided to extend the BasicDatasource. Hope that's not a problem.

What do you think about this?


c) servletRequest.getPathInfo() is always null. And I understand why. The Javadoc says:


The extra path information follows the servlet path but precedes the query string and will start with a "/" character.

Open in new window


In case of Spring the servlet path is the full path hence if you call getServletPath() it will always return the full URI and getPathInfo() will return nothing.

So, I haven't been able to add the tenantId in the url. Well. Not sure exactly where or how to set it when the user logs in. The page says how to parse it, but not how to add some custom information in it.  

¿Any Ideas?


Thank you for your patience. There are still some things I don't fully understand. Hope I'm not asking stupid questions.



Exception:
SEVERE: Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in file [C:\Users\ely\Documents\Desarrollo\Spring\springsource\vfabric-tc-server-developer-2.9.2.RELEASE\base-instance\wtpwebapps\TestMultiTenant\WEB-INF\classes\META-INF\spring\applicationContext.xml]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: persistenceUnit] Unable to build EntityManagerFactory
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:529)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
	at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:741)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:464)
	at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:389)
	at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:294)
	at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:112)
	at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4887)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5381)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901)
	at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877)
	at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:633)
	at org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:657)
	at org.apache.catalina.startup.HostConfig$DeployDescriptor.run(HostConfig.java:1637)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
	at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
	at java.util.concurrent.FutureTask.run(FutureTask.java:166)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:724)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in file [C:\Users\ely\Documents\Desarrollo\Spring\springsource\vfabric-tc-server-developer-2.9.2.RELEASE\base-instance\wtpwebapps\TestMultiTenant\WEB-INF\classes\META-INF\spring\applicationContext.xml]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: persistenceUnit] Unable to build EntityManagerFactory
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1482)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:439)
	at org.springframework.beans.factory.BeanFactoryUtils.beansOfTypeIncludingAncestors(BeanFactoryUtils.java:277)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.detectPersistenceExceptionTranslators(PersistenceExceptionTranslationInterceptor.java:139)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.<init>(PersistenceExceptionTranslationInterceptor.java:79)
	at org.springframework.dao.annotation.PersistenceExceptionTranslationAdvisor.<init>(PersistenceExceptionTranslationAdvisor.java:71)
	at org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor.setBeanFactory(PersistenceExceptionTranslationPostProcessor.java:85)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeAwareMethods(AbstractAutowireCapableBeanFactory.java:1502)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1470)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
	... 24 more
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: persistenceUnit] Unable to build EntityManagerFactory
	at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:924)
	at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:899)
	at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:76)
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:288)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:310)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1541)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1479)
	... 39 more
Caused by: org.hibernate.AnnotationException: @OneToOne or @ManyToOne on com.i4b.test1.domain.UserRole.userEntry references an unknown entity: com.i4b.test1.domain.User
	at org.hibernate.cfg.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:109)
	at org.hibernate.cfg.Configuration.processEndOfQueue(Configuration.java:1536)
	at org.hibernate.cfg.Configuration.processFkSecondPassInOrder(Configuration.java:1457)
	at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1365)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1756)
	at org.hibernate.ejb.EntityManagerFactoryImpl.<init>(EntityManagerFactoryImpl.java:96)
	at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:914)
	... 45 more

Open in new window

Avatar of alcani

ASKER

Oh, I forgot another one:

d) I tried for a day or two to find a way to get the parameters defined in the applicationContext.xml for the bean.

For example, when Roo generated the code, this is the definition of the DataSource bean:

    <bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource">
        <property name="driverClassName" value="${database.driverClassName}"/>
        <property name="url" value="${database.url}"/>
        <property name="username" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
        <property name="testOnBorrow" value="true"/>
        <property name="testOnReturn" value="true"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="1800000"/>
        <property name="numTestsPerEvictionRun" value="3"/>
        <property name="minEvictableIdleTimeMillis" value="1800000"/>
        <property name="validationQuery" value="SELECT version();"/>
    </bean>

Open in new window

So, I changed it to use my "TenantAwareDatasource":

    <bean class="com.i4b.test1.core.TenantAwareDataSource" destroy-method="close" id="dataSource">
        <property name="driverClassName" value="${database.driverClassName}"/>
        <property name="url" value="${database.url}"/>
        <property name="username" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
        <property name="testOnBorrow" value="true"/>
        <property name="testOnReturn" value="true"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="1800000"/>
        <property name="numTestsPerEvictionRun" value="3"/>
        <property name="minEvictableIdleTimeMillis" value="1800000"/>
        <property name="validationQuery" value="SELECT version();"/>
    </bean>

Open in new window

I wasn't able to pass those parameters to the "BasicDataSource" I was creating in the class.

All the examples I could find showed how to get parameters from the ".properties" file. But couldn't find a way to get, for example, the minEvictableIdleTimeMillis property.

One example that at least didn't return "null" was to add an attribute with a @Value annotation like this:

	@Value("#{dataSource.url}") 
	private String databaseUrl2;

Open in new window


I also, just to say "well I at least tried", I created some attributes with the same names as the bean properties. But didn't work either.
Now that I'm writing this, I'm thinking maybe the problem was that I was already extending BasicDataSource, so those attributes where already defined in the super class, so I couldn't cleanly define them again.

Could it be the reason it didn't work?

Can't believe there's no easy way to grab them. They are right there in the bean definition. I'm sure I'll be ashamed when someone tells me how to do it. Just hope not too much.

Does anybody know how to get them?

Finally, when I decided to extend BasicDataSource, I found a way to get those values:

private BasicDataSource createDataSourceForTenantAndSchema() throws SQLException {
...
    result = new BasicDataSource();
    
    result.setDriverClassName(                 this.driverClassName );
    result.setUrl(                             this.url );
    result.setUsername(                        this.username );
    result.setPassword(                        this.password );
    result.setTestOnBorrow(                    this.testOnBorrow );
    result.setTestOnReturn(                    this.testOnReturn );
    result.setTestWhileIdle(                   this.testWhileIdle );
    result.setTimeBetweenEvictionRunsMillis(   this.timeBetweenEvictionRunsMillis );
    result.setNumTestsPerEvictionRun(          this.numTestsPerEvictionRun );
    result.setMinEvictableIdleTimeMillis(      this.minEvictableIdleTimeMillis );
    result.setValidationQuery(                 this.validationQuery );
    
    return result;
...
}

Open in new window

But I'm not sure it's the best way to do it.

Is this ok?
Avatar of alcani

ASKER

Ups. sorry.

Just realized I pasted the wrong error today at "2013-10-06 at 13:59:23 with ID: 39551017"

This is the real one:

oct 06, 2013 8:09:52 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [test1] in context with path [/test1] threw exception
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:343)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
	at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:34)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.getTarget(CglibAopProxy.java:663)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:614)
	at com.i4b.test1.core.TenantContext$$EnhancerByCGLIB$$e5eb3b12.getTenant(<generated>)
	at com.i4b.test1.core.TenantContext_Roo_JavaBean.ajc$interMethodDispatch1$com_i4b_test1_core_TenantContext_Roo_JavaBean$com_i4b_test1_core_TenantContext$getTenant(TenantContext_Roo_JavaBean.aj)
	at com.i4b.test1.core.TenantFilter.doFilter(TenantFilter.java:50)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:173)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:259)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
	at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:947)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
	at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1009)
	at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589)
	at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:724)
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
	at org.springframework.web.context.request.SessionScope.get(SessionScope.java:90)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:329)
	... 35 more

Open in new window

Apparently spring instantiates all beans at web context initialization, so if a bean refers to the session -- which doesn't exist yet -- then it fails.

tenantContext is likely to be referring to the session in some way.  Take out that reference so that it can be loaded by spring before there is a session.
Avatar of alcani

ASKER

I'm realizing I never posted the software versions I'm using.

Here they go:

From the pom.xml:
	<properties>
		<aspectj.version>1.7.2</aspectj.version>
		<java.version>7</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<roo.version>1.2.4.RELEASE</roo.version>
		<slf4j.version>1.7.5</slf4j.version>
		<spring.version>3.2.3.RELEASE</spring.version>
		<spring-security.version>3.1.0.RELEASE</spring-security.version>
	</properties>
...
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

Open in new window



I didn't know how to implement the exception suggestions and kept looking for a way to do it.

The first part of the message says: "consider defining a scoped proxy for this bean if you intend to refer to it from a singleton"

Not sure, but I tried to add a proxy like this:

@Component
@RooJavaBean
@RooToString
//@RooJpaEntity(entityName = "TENANTS_CONTEXT")
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {

Open in new window

But didn't solve the problem.

The documentation talks about a special listener in order to have a "session" scoped bean.

I don't know how I missed that before.

The Roo generated web.xml has:
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

Open in new window


So I added:
	<listener>
	  <listener-class>
	      org.springframework.web.context.request.RequestContextListener
	  </listener-class>
	</listener>

Open in new window


I have both now.
Is it ok?

It doesn't give me that exception anymore.


Well... I still need help with the other questions.

Thank you.
ASKER CERTIFIED SOLUTION
Avatar of mrcoffee365
mrcoffee365
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Wow -- a C grade for help and the right answer.  You might not be aware that higher grades help the experts who help you and don't cost you anything.  In any case, for the next person who helps you, you might want to bear that in mind.