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

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

问题描述

使用 Spring 和 Hibernate,我想写入一个 MySQL 主数据库,并从基于云的 Java Web 应用程序中的另一个复制从数据库读取.

我找不到对应用程序代码透明的解决方案.我真的不想更改我的 DAO 来管理不同的 SessionFactories,因为这看起来非常混乱并且将代码与特定的服务器架构耦合.

有什么方法可以告诉 Hibernate 自动将 CREATE/UPDATE 查询路由到一个数据源,并将 SELECT 路由到另一个数据源?我不想根据对象类型进行任何分片或任何操作 - 只需将不同类型的查询路由到不同的数据源.

解决方案

可以在此处找到示例:

Spring 提供了 DataSource 的变体,称为 AbstractRoutingDatasource.它可以用来代替标准的 DataSource 实现,并启用一种机制来确定在运行时为每个操作使用哪个具体的 DataSource.您需要做的就是扩展它并提供抽象 determineCurrentLookupKey 方法的实现.这是实现您的自定义逻辑以确定具体数据源的地方.返回的对象用作查找键.它通常是一个 String 或 en Enum,在 Spring 配置中用作限定符(详细信息将在后面).

包 website.fedulov.routing.RoutingDataSource导入 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;公共类 RoutingDataSource 扩展 AbstractRoutingDataSource {@覆盖受保护的对象确定CurrentLookupKey() {返回 DbContextHolder.getDbType();}}

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

public enum DbType {掌握,副本1,}公共类 DbContextHolder {私有静态最终 ThreadLocalcontextHolder = new ThreadLocal();公共静态无效 setDbType(DbType dbType) {如果(dbType == null){抛出新的 NullPointerException();}contextHolder.set(dbType);}公共静态 DbType getDbType() {返回(DbType)contextHolder.get();}公共静态无效 clearDbType() {contextHolder.remove();}}

如您所见,您还可以使用枚举作为键,Spring 会根据名称正确解析它.关联的数据源配置和键可能如下所示:

<代码> ....<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource"><属性名称="targetDataSources"><map key-type="com.sabienzia.routing.DbType"><entry key="MASTER" value-ref="dataSourceMaster"/><entry key="REPLICA1" value-ref="dataSourceReplica"/></地图></属性><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>

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

@Service公共类图书服务{私人最终 BookRepository bookRepository;私人最终 Mapper 映射器;@注入公共图书服务(BookRepository bookRepository,Mapper 映射器){this.bookRepository = bookRepository;this.mapper = 映射器;}@Transactional(readOnly = true)公共页面getBooks(Pageable p) {DbContextHolder.setDbType(DbType.REPLICA1);//<----- 设置 ThreadLocal 数据源查找键//从这里开始的所有连接都将转到 REPLICA1页<书>bookPage = callActionRepo.findAll(p);列表pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);DbContextHolder.clearDbType();//<----- 清除 ThreadLocal 设置return new PageImpl(pContent, p, callActionsPage.getTotalElements());}...//其他方法

现在我们可以控制使用哪个数据源并根据需要转发请求.看起来挺好的!

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

 @Transactional(readOnly = true)公共页面getBooks(Pageable p) {尝试{DbContextHolder.setDbType(DbType.REPLICA1);//<----- 设置 ThreadLocal 数据源查找键//从这里开始的所有连接都将转到 REPLICA1页<书>bookPage = callActionRepo.findAll(p);列表pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);DbContextHolder.clearDbType();//<----- 清除 ThreadLocal 设置} 捕获(异常 e){抛出新的运行时异常(e);} 最后 {DbContextHolder.clearDbType();//<----- 确保 ThreadLocal 设置被清除}return new PageImpl(pContent, p, callActionsPage.getTotalElements());}

哎呀>_<!这绝对不像我想放入每个只读方法中的东西.我们能做得更好吗?当然!这种在方法开始时做某事,然后在最后做某事"的模式应该敲响警钟.救援方面!

不幸的是,这篇文章已经太长了,无法涵盖自定义方面的主题.您可以使用此链接.

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天全站免登陆