如何设置Hibernate读/写不同的数据源? [英] How to setup Hibernate to read/write to different datasources?

查看:189
本文介绍了如何设置Hibernate读/写不同的数据源?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用Spring和Hibernate,我想写入一个MySQL主数据库,并从基于云的Java webapp中的另一个更多复制的从服务器读取数据。

我可以找不到对应用程序代码透明的解决方案。我真的不想改变我的DAO来管理不同的SessionFactory,因为这看起来很麻烦,并且将代码与特定的服务器架构耦合在一起。



有没有告诉Hibernate自动将CREATE / UPDATE查询路由到一个数据源,并将SELECT选择到另一个数据源的方法?我不想做任何基于对象类型的分片或任何分类 - 只是将不同类型的查询路由到不同的数据源。 解决方案

可以在这里找到一个示例:



Spring提供了一个变体的DataSource,名为 AbstractRoutingDatasource 。它可以用来代替标准的DataSource实现,并且使机制能够确定在运行时为每个操作使用哪个具体的DataSource。所有你需要做的就是扩展它,并提供一个抽象 determineCurrentLookupKey 方法的实现。这是实现自定义逻辑以确定具体数据源的地方。返回对象用作查找键。它通常是一个String或En Enum,在Spring配置中用作限定符(详情如下)。

  package website.fedulov .routing.RoutingDataSource 

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey(){
return DbContextHolder.getDbType();




$ b你可能想知道DbContextHolder对象是什么,它知道要返回哪个DataSource标识符吗?请记住,只要TransactionsManager请求连接,将调用 determineCurrentLookupKey 方法。记住每个事务与一个单独的线程关联是很重要的。更确切地说,TransactionsManager将Connection绑定到当前线程。因此,为了将不同的事务分派到不同的目标数据源,我们必须确保每个线程都可以可靠地识别哪个数据源将被使用。这使得使用ThreadLocal变量将特定的DataSource绑定到线程以及因此到事务处理变得很自然。这是如何完成的:

  public enum DbType {
MASTER,
REPLICA1,
}

public class DbContextHolder {

private static final ThreadLocal< DbType> contextHolder = new ThreadLocal< DbType>();

public static void setDbType(DbType dbType){
if(dbType == null){
throw new NullPointerException();
}
contextHolder.set(dbType);
}

public static DbType getDbType(){
return(DbType)contextHolder.get();


public static void clearDbType(){
contextHolder.remove();


code


$ b

正如你所看到的,你也可以使用enum作为关键字和Spring会根据名称正确解析它。关联的DataSource配置和键可能如下所示:

  .... 
< property name =targetDataSources>
< map key-type =com.sabienzia.routing.DbType>
< entry key =MASTERvalue-ref =dataSourceMaster/>
< entry key =REPLICA1value-ref =dataSourceReplica/>
< / map>
< / property>
< property name =defaultTargetDataSourceref =dataSourceMaster/>
< / bean>

< bean id =dataSourceMasterclass =org.apache.commons.dbcp.BasicDataSource>
< property name =driverClassNamevalue =com.mysql.jdbc.Driver/>
< property name =urlvalue =$ {db.master.url}/>
< property name =usernamevalue =$ {db.username}/>
< property name =passwordvalue =$ {db.password}/>
< / bean>
< bean id =dataSourceReplicaclass =org.apache.commons.dbcp.BasicDataSource>
< property name =driverClassNamevalue =com.mysql.jdbc.Driver/>
< property name =urlvalue =$ {db.replica.url}/>
< property name =usernamevalue =$ {db.username}/>
< property name =passwordvalue =$ {db.password}/>
< / bean>

此时您可能会发现自己在做这样的事情:

  @Service 
public class BookService {

private final BookRepository bookRepository;
private final Mapper映射器;

@Inject
public BookService(BookRepository bookRepository,Mapper mapper){
this.bookRepository = bookRepository;
this.mapper = mapper;
}

@Transactional(readOnly = true)
public Page< BookDTO> getBooks(Pageable p){
DbContextHolder.setDbType(DbType.REPLICA1); //< ----- set ThreadLocal DataSource lookup key
//从这里开始的所有连接将转到REPLICA1
页面< Book> booksPage = callActionRepo.findAll(p);
列表< BookDTO> pContent = CollectionMapper.map(mapper,callActionsPage.getContent(),BookDTO.class);
DbContextHolder.clearDbType(); //< -----清除ThreadLocal设置
返回新的PageImpl< BookDTO>(pContent,p,callActionsPage.getTotalElements());
}

... //其他方法

现在我们可以控制使用哪个DataSource并根据需要转发请求。看起来不错!



...或者是吗?首先,那些对神奇的DbContextHolder的静态方法调用真的很突出。他们看起来不属于商业逻辑。而他们没有。他们不仅没有传达目的,而且看起来很脆弱,容易出错(忘记清理dbType)。那么如果在setDbType和cleanDbType之间抛出异常呢?我们不能忽略它。我们需要绝对确定我们重置了dbType,否则返回到ThreadPool的Thread可能处于断开状态,试图在下一次调用中写入副本。所以我们需要:

  @Transactional(readOnly = true)
public Page< BookDTO> getBooks(Pageable p){
尝试{
DbContextHolder.setDbType(DbType.REPLICA1); //< ----- set ThreadLocal DataSource lookup key
//从这里开始的所有连接将转到REPLICA1
页面< Book> booksPage = callActionRepo.findAll(p);
列表< BookDTO> pContent = CollectionMapper.map(mapper,callActionsPage.getContent(),BookDTO.class);
DbContextHolder.clearDbType(); //< -----清除ThreadLocal设置
} catch(Exception e){
throw new RuntimeException(e);
} finally {
DbContextHolder.clearDbType(); //< -----确保清除ThreadLocal设置

返回新的PageImpl< BookDTO>(pContent,p,callActionsPage.getTotalElements());
}

Yikes > _< !这绝对不是我想要放入每个只读方法的东西。我们可以做得更好吗?当然!这种在方法开始时做些事情,然后在最后做点事情的模式应该会引起轰动。救援方面!

不幸的是,这篇文章已经过了很长时间以涵盖自定义方面的话题。您可以使用此链接


Using Spring and Hibernate, I want to write to one MySQL master database, and read from one more more replicated slaves in cloud-based Java webapp.

I can't find a solution that is transparent to the application code. I don't really want to have to change my DAOs to manage different SessionFactories, as that seems really messy and couples the code with a specific server architecture.

Is there any way of telling Hibernate to automatically route CREATE/UPDATE queries to one datasource, and SELECT to another? I don't want to do any sharding or anything based on object type - just route different types of queries to different datasources.

解决方案

An example can be found here: https://github.com/afedulov/routing-data-source.

Spring provides a variation of DataSource, called AbstractRoutingDatasource. It can be used in place of standard DataSource implementations and enables a mechanism to determine which concrete DataSource to use for each operation at runtime. All you need to do is to extend it and to provide an implementation of an abstract determineCurrentLookupKey method. This is the place to implement your custom logic to determine the concrete DataSource. Returned Object serves as a lookup key. It is typically a String or en Enum, used as a qualifier in Spring configuration (details will follow).

package website.fedulov.routing.RoutingDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }
}

You might be wondering what is that DbContextHolder object and how does it know which DataSource identifier to return? Keep in mind that determineCurrentLookupKey method will be called whenever TransactionsManager requests a connection. It is important to remember that each transaction is "associated" with a separate thread. More precisely, TransactionsManager binds Connection to the current thread. Therefore in order to dispatch different transactions to different target DataSources we have to make sure that every thread can reliably identify which DataSource is destined for it to be used. This makes it natural to utilize ThreadLocal variables for binding specific DataSource to a Thread and hence to a Transaction. This is how it is done:

public enum DbType {
   MASTER,
   REPLICA1,
}

public class DbContextHolder {

   private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();

   public static void setDbType(DbType dbType) {
       if(dbType == null){
           throw new NullPointerException();
       }
      contextHolder.set(dbType);
   }

   public static DbType getDbType() {
      return (DbType) contextHolder.get();
   }

   public static void clearDbType() {
      contextHolder.remove();
   }
}

As you see, you can also use an enum as the key and Spring will take care of resolving it correctly based on the name. Associated DataSource configuration and keys might look like this:

  ....
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource">
 <property name="targetDataSources">
   <map key-type="com.sabienzia.routing.DbType">
     <entry key="MASTER" value-ref="dataSourceMaster"/>
     <entry key="REPLICA1" value-ref="dataSourceReplica"/>
   </map>
 </property>
 <property name="defaultTargetDataSource" ref="dataSourceMaster"/>
</bean>

<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.master.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.replica.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>

At this point you might find yourself doing something like this:

@Service
public class BookService {

  private final BookRepository bookRepository;
  private final Mapper               mapper;

  @Inject
  public BookService(BookRepository bookRepository, Mapper mapper) {
    this.bookRepository = bookRepository;
    this.mapper = mapper;
  }

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                  // all connection from here will go to REPLICA1
    Page<Book> booksPage = callActionRepo.findAll(p);
    List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
    DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

  ...//other methods

Now we can control which DataSource will be used and forward requests as we please. Looks good!

...Or does it? First of all, those static method calls to a magical DbContextHolder really stick out. They look like they do not belong the business logic. And they don't. Not only do they not communicate the purpose, but they seem fragile and error-prone (how about forgetting to clean the dbType). And what if an exception is thrown between the setDbType and cleanDbType? We cannot just ignore it. We need to be absolutely sure that we reset the dbType, otherwise Thread returned to the ThreadPool might be in a "broken" state, trying to write to a replica in the next call. So we need this:

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    try{
      DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                    // all connection from here will go to REPLICA1
      Page<Book> booksPage = callActionRepo.findAll(p);
      List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
       DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    } catch (Exception e){
      throw new RuntimeException(e);
    } finally {
       DbContextHolder.clearDbType();               // <----- make sure ThreadLocal setting is cleared         
    }
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

Yikes >_< ! This definitely does not look like something I would like to put into every read only method. Can we do better? Of course! This pattern of "do something at the beginning of a method, then do something at the end" should ring a bell. Aspects to the rescue!

Unfortunately this post has already gotten too long to cover the topic of custom aspects. You can follow up on the details of using aspects using this link.

这篇关于如何设置Hibernate读/写不同的数据源?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆