[英]Multi-tenant Spring JPA: Dynamic dialects resolution for dynamic datasources
[英]Dynamic Multi-tenant WebApp (Spring Hibernate)
我提出了一个使用以下内容的动态多租户应用程序:
这是我第一次必须自己实例化Spring对象,因此我想知道我是否正确完成了所有操作,或者该应用程序是否会在生产期间未指定的将来出现在我的面前。
基本上,单个DataBase模式是已知的,但是数据库详细信息将在运行时由用户指定。 他们可以自由指定任何主机名/端口/数据库名称/用户名/密码。
这是工作流程:
SessionFactory
(或在缓存中找到),则使用SourceContext#setSourceId(SourceId)
将其保存在用户会话中,然后用户可以使用此数据库。 AnnotationSessionFactoryBean
AnnotationSessionFactoryBean
被删除/销毁。 那么以下工作将按预期进行吗? 帮助和指针是最欢迎的。
web.xml
<web-app version="3.1" ...>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener> <!-- Needed for SourceContext -->
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<web-app>
applicationContext.xml
<beans ...>
<tx:annotation-driven />
<util:properties id="db" location="classpath:db.properties" /> <!-- driver/url prefix -->
<context:component-scan base-package="com.example.basepackage" />
</beans>
UserDao.java
@Service
public class UserDao implements UserDaoImpl {
@Autowired
private TemplateFactory templateFactory;
@Override
public void addTask() {
final HibernateTemplate template = templateFactory.getHibernateTemplate();
final User user = (User) DataAccessUtils.uniqueResult(
template.find("select distinct u from User u left join fetch u.tasks where u.id = ?", 1)
);
final Task task = new Task("Do something");
user.getTasks().add(task);
TransactionTemplate txTemplate = templateFactory.getTxTemplate(template);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.save(task);
template.update(user);
}
});
}
}
TemplateFactory.java
@Service
public class TemplateFactory {
@Autowired
private SourceSessionFactory factory;
@Resource(name = "SourceContext")
private SourceContext srcCtx; // session scope, proxied bean
@Override
public HibernateTemplate getHibernateTemplate() {
LocalSessionFactoryBean sessionFactory = factory.getSessionFactory(srcCtx.getSourceId());
return new HibernateTemplate(sessionFactory.getObject());
}
@Override
public TransactionTemplate getTxTemplate(HibernateTemplate template) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(template.getSessionFactory());
return new TransactionTemplate(txManager);
}
}
SourceContext.java
@Component("SourceContext")
@Scope(value="session", proxyMode = ScopedProxyMode.INTERFACES)
public class SourceContext {
private static final long serialVersionUID = -124875L;
private SourceId id;
@Override
public SourceId getSourceId() {
return id;
}
@Override
public void setSourceId(SourceId id) {
this.id = id;
}
}
SourceId.java
public interface SourceId {
String getHostname();
int getPort();
String getSID();
String getUsername();
String getPassword();
// concrete class has proper hashCode/equals/toString methods
// which use all of the SourceIds properties above
}
SourceSessionFactory.java
@Service
public class SourceSessionFactory {
private static Map<SourceId, AnnotationSessionFactoryBean> cache = new HashMap<SourceId, AnnotationSessionFactoryBean>();
@Resource(name = "db")
private Properties db;
@Override
public LocalSessionFactoryBean getSessionFactory(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.get(id);
if (sessionFactory == null) {
return createSessionFactory(id);
}
else {
return sessionFactory;
}
}
}
private AnnotationSessionFactoryBean createSessionFactory(SourceId id) {
AnnotationSessionFactoryBean sessionFactory = new AnnotationSessionFactoryBean();
sessionFactory.setDataSource(new CutomDataSource(id, db));
sessionFactory.setPackagesToScan(new String[] { "com.example.basepackage" });
try {
sessionFactory.afterPropertiesSet();
}
catch (Exception e) {
throw new SourceException("Unable to build SessionFactory for:" + id, e);
}
cache.put(id, sessionFactory);
return sessionFactory;
}
public void destroy(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.remove(id);
if (sessionFactory != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Releasing SessionFactory for: " + id);
}
try {
sessionFactory.destroy();
}
catch (HibernateException e) {
LOG.error("Unable to destroy SessionFactory for: " + id);
e.printStackTrace(System.err);
}
}
}
}
}
CustomDataSource.java
public class CutomDataSource extends BasicDataSource { // commons-dbcp2
public CutomDataSource(SourceId id, Properties db) {
setDriverClassName(db.getProperty("driverClassName"));
setUrl(db.getProperty("url") + id.getHostname() + ":" + id.getPort() + ":" + id.getSID());
setUsername(id.getUsername());
setPassword(id.getPassword());
}
}
最后,我扩展了Spring的AbstractRoutingDataSource
,使其能够动态地动态创建数据源。 一切正常后,我将使用完整代码更新此答案。 我还有两点要解决,但关键是:
@Service
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// this is pretty much the same as the above SourceSessionFactory
// but with a map of CustomDataSources instead of
// AnnotationSessionFactoryBeans
@Autowired
private DynamicDataSourceFactory dataSourceFactory;
// This is the sticky part. I currently have a workaround instead.
// Hibernate needs an actual connection upon spring startup & there's
// also no session in place during spring initialization. TBC.
// @Resource(name = "UserContext") // scope session, proxy bean
private UserContext userCtx; // something that returns the DB config
@Override
protected SourceId determineCurrentLookupKey() {
return userCtx.getSourceId();
}
@Override
protected CustomDataSource determineTargetDataSource() {
SourceId id = determineCurrentLookupKey();
return dataSourceFactory.getDataSource(id);
}
@Override
public void afterPropertiesSet() {
// we don't need to resolve any data sources
}
// Inherited methods copied here to show what's going on
// @Override
// public Connection getConnection() throws SQLException {
// return determineTargetDataSource().getConnection();
// }
//
// @Override
// public Connection getConnection(String username, String password)
// throws SQLException {
// return determineTargetDataSource().getConnection(username, password);
// }
}
因此,我只是将DynamicRoutingDataSource作为Spring的SessionFactoryBean的数据源与TransactionManager一起进行连接,其余的一切都照常进行。 正如我所说,还有更多代码需要遵循。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.