動態資料來源路由

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

Spring 2.0.1 引入了 AbstractRoutingDataSource。我相信它值得關注,因為(根據客戶的頻繁提問)我預感有很多“自制”的解決方案在解決這個問題。再加上它實現起來很簡單但又容易被忽視的事實,現在我有幾個理由來撣去團隊部落格我這個角落的灰塵了。

總體的想法是,一個路由DataSource充當一箇中介——而“真實”的DataSource可以在執行時根據查詢鍵動態確定。一個潛在的用例是確保標準JTA不支援的事務特定隔離級別。為此,Spring提供了一個實現:IsolationLevelDataSourceRouter。有關詳細描述(包括配置示例),請參閱其JavaDoc。

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

首先,我建立了一個Catalog,它擴充套件了Spring 2.0的SimpleJdbcDaoSupport。該基類只需要一個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只包含name和price屬性。

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 + ")";
   }

}

現在,為了演示多個DataSource,我為不同的Customer型別建立了一個列舉(我猜代表了會員“級別”),並建立了三個不同的資料庫——以便每種型別的客戶都能獲得一個獨特的專案列表(我之前提到過這會是一個牽強的例子,不是嗎?)。重要的是,每個資料庫在模式方面都是等效的。這樣Catalog的查詢就可以針對其中任何一個執行——只是返回不同的結果。在這種情況下,它只是一個包含2列的“item”表:name和price。還有……這是列舉。

public enum CustomerType {
   BRONZE, 
   SILVER, 
   GOLD
}

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

<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://:${db.port.gold}/blog"/>
</bean>

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

<bean id="bronzeDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://:${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

現在事情開始變得有趣了。我需要為我的Catalog提供“路由”DataSource,以便它可以在執行時根據當前客戶的型別動態地從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和routing DataSource bean。正如你所見,“真實”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)]

正如你所見,配置很簡單。更好的是,資料訪問程式碼不必關心查詢不同的DataSource。有關更多資訊,請參閱AbstractRoutingDataSource的JavaDoc。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有