動態資料來源路由 (Dynamic DataSource Routing)

工程 (Engineering) | Mark Fisher | 2007 年 1 月 23 日 | ...

Spring 2.0.1 引入了 AbstractRoutingDataSource。 我相信它值得關注,因為(根據客戶經常提出的問題)我猜測存在相當多的「自家製」解決方案來解決這個問題。 再加上它易於實作但容易被忽略的事實,現在我有很多理由來清理我的團隊部落格的一角。

總體思路是路由 DataSource 充當中間媒介 - 而「真實」的 DataSource 可以在運行時根據查找鍵動態確定。 一個潛在的用例是確保標準 JTA 不支持的特定於事務的隔離級別。 為此,Spring 提供了一個實現:IsolationLevelDataSourceRouter。 有關詳細描述(包括配置示例),請參閱其 JavaDoc。

另一個有趣的用例是根據當前用戶上下文的某些屬性確定 DataSource。 下面是一個相當牽強的例子來演示這個想法。

首先,我創建了一個擴展 Spring 2.0 的 SimpleJdbcDaoSupportCatalog。 該基底類僅需要 javax.sql.DataSource 的任何實現的實例,然後它會為您創建一個 SimpleJdbcTemplate。 由於它擴展了 JdbcDaoSupport,因此 JdbcTemplate 也可用。 但是,「簡單」版本提供了許多不錯的 Java 5 便利性。 您可以在 Ben Hale 的 這個部落格 中閱讀更多詳細信息。

無論如何,這是我的 Catalog 的程式碼

package blog.datasource;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;

public class Catalog extends SimpleJdbcDaoSupport {
	
   public List<Item> getItems() {
      String query = "select name, price from item";
      return getSimpleJdbcTemplate().query(query, new ParameterizedRowMapper<Item>() {
            public Item mapRow(ResultSet rs, int row) throws SQLException {
               String name = rs.getString(1);
               double price = rs.getDouble(2);
               return new Item(name, price);
            }
      });
   }
}

如您所見,Catalog 只是返回一個 item 物件列表。 Item 僅包含名稱和價格屬性

package blog.datasource;

public class Item {

   private String name;
   private double price;
	
   public Item(String name, double price) {
      this.name = name;
      this.price = price;
   }

   public String getName() {
      return name;
   }

   public double getPrice() {
      return price;
   }

   public String toString() {
      return name + " (" + price + ")";
   }

}

現在,為了演示多個 DataSources,我為不同的客戶類型創建了一個枚舉(表示我猜的會員資格「級別」),並且我創建了三個不同的資料庫 - 以便每種類型的客戶都會獲得一個不同的項目列表(我確實提到這將是一個牽強的例子,不是嗎?)。 重要的是每個資料庫在架構方面都是等效的。 這樣,Catalog 的查詢將適用於它們中的任何一個 - 只是返回不同的結果。 在這種情況下,它只是包含 2 個欄位的「item」表格:名稱和價格。 而且...這是枚舉

public enum CustomerType {
   BRONZE, 
   SILVER, 
   GOLD
}

現在是創建一些 bean 定義的時候了。 由於我有 3 個 datasources,其中所有內容都相同,除了連接埠號碼,所以我創建了一個父 bean,以便可以繼承共享屬性。 然後,我添加了 3 個 bean 定義來表示每個 CustomerType 的 DataSources

<bean id="parentDataSource"
         class="org.springframework.jdbc.datasource.DriverManagerDataSource"
         abstract="true">
   <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
   <property name="username" value="sa"/>
</bean>
		
<bean id="goldDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://localhost:${db.port.gold}/blog"/>
</bean>

<bean id="silverDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://localhost:${db.port.silver}/blog"/>
</bean>

<bean id="bronzeDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://localhost:${db.port.bronze}/blog"/>
</bean>

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="location" value="classpath:/blog/datasource/db.properties"/>
</bean>	

請注意,我添加了一個 PropertyPlaceholderConfigurer,以便我可以將連接埠號碼外部化到「db.properties」檔案中,如下所示

db.port.gold=9001
db.port.silver=9002
db.port.bronze=9003

現在事情開始變得有趣了。 我需要將「路由」DataSource 提供給我的 Catalog,以便它可以根據當前客戶的類型,在運行時從 3 個不同的資料庫動態獲取連線。 正如我所提到的,AbstractRoutingDataSource 的實作可能相當簡單。 這是我的實作

package blog.datasource;

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

public class CustomerRoutingDataSource extends AbstractRoutingDataSource {

   @Override
   protected Object determineCurrentLookupKey() {
      return CustomerContextHolder.getCustomerType();
   }
}

...並且 CustomerContextHolder 只是提供對線程綁定的 CustomerType 的存取。 實際上,「上下文」可能包含有關客戶的更多信息。 另請注意,如果您使用 Spring Security,則可以從 userDetails 檢索一些信息。 在這個例子中,它只是客戶的「類型」

public class CustomerContextHolder {

   private static final ThreadLocal<CustomerType> contextHolder = 
            new ThreadLocal<CustomerType>();
	
   public static void setCustomerType(CustomerType customerType) {
      Assert.notNull(customerType, "customerType cannot be null");
      contextHolder.set(customerType);
   }

   public static CustomerType getCustomerType() {
      return (CustomerType) contextHolder.get();
   }

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

最後,我只需要配置 catalog 和路由 DataSource beans。 如您所見,「真實」的 DataSource 參考在 Map 中提供。 如果您提供字串,它們可以解析為 JNDI 名稱(或可以提供任何自定義解析策略 - 請參閱 JavaDoc)。 此外,我只是將 'bronzeDataSource' 設定為預設值

<bean id="catalog" class="blog.datasource.Catalog">
   <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="dataSource" class="blog.datasource.CustomerRoutingDataSource">
   <property name="targetDataSources">
      <map key-type="blog.datasource.CustomerType">
         <entry key="GOLD" value-ref="goldDataSource"/>
         <entry key="SILVER" value-ref="silverDataSource"/>
      </map>
   </property>
   <property name="defaultTargetDataSource" ref="bronzeDataSource"/>
</bean>

當然,我想看到這個工作,所以我創建了一個簡單的測試(擴展了 Spring 的一個整合測試支持類別)。 我向「gold」資料庫添加了 3 個項目,向「silver」資料庫添加了 2 個項目,並且僅向「bronze」資料庫添加了 1 個項目。 這是測試

public class CatalogTests extends AbstractDependencyInjectionSpringContextTests {

   private Catalog catalog;

   public void setCatalog(Catalog catalog) {
      this.catalog = catalog;
   }

   public void testDataSourceRouting() {
      CustomerContextHolder.setCustomerType(CustomerType.GOLD);
      List<Item> goldItems = catalog.getItems();
      assertEquals(3, goldItems.size());
      System.out.println("gold items: " + goldItems);

      CustomerContextHolder.setCustomerType(CustomerType.SILVER);
      List<Item> silverItems = catalog.getItems();
      assertEquals(2, silverItems.size());
      System.out.println("silver items: " + silverItems);
	
      CustomerContextHolder.clearCustomerType();
      List<Item> bronzeItems = catalog.getItems();
      assertEquals(1, bronzeItems.size());
      System.out.println("bronze items: " + bronzeItems);		
   }

   protected String[] getConfigLocations() {
      return new String[] {"/blog/datasource/beans.xml"};
   }	
}

...並且不是簡單地截取綠色條的螢幕截圖,您會注意到我提供了一些控制台輸出 - 結果!

gold items: [gold item #1 (250.0), gold item #2 (325.45), gold item #3 (55.6)]
silver items: [silver item #1 (25.0), silver item #2 (15.3)]
bronze items: [bronze item #1 (23.75)]

如您所見,配置很簡單。 更好的是,資料存取程式碼不關心查找不同的 DataSources。 有關更多信息,請參閱 AbstractRoutingDataSource 的 JavaDoc。

取得 Spring 電子報 (Get the Spring newsletter)

隨時關注 Spring 電子報 (Stay connected with the Spring newsletter)

訂閱 (Subscribe)

領先一步 (Get ahead)

VMware 提供培訓和認證,以加速您的進度。 (VMware offers training and certification to turbo-charge your progress.)

了解更多 (Learn more)

取得支援 (Get support)

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。 (Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.)

了解更多 (Learn more)

即將舉行的活動 (Upcoming events)

查看 Spring 社群中所有即將舉行的活動。 (Check out all the upcoming events in the Spring community.)

檢視全部 (View all)