使用 Command 與 Strategy 產生 SQL 語法

作者:Kyle Brown
譯者:James_Sa 

 
 

我遭遇的問題

我在 KSC 當講師(Apprentice Master),最重要的工作就是教導學生建立連結關聯式資料庫的物件系統。對於這方面的主題,我也寫了不少東西([Brown 96a], [Brown 96b]),所以也不會在這裡贅述。不過,我的工作裡有部分作業,十分呆板,常常造成我的困擾。

在課程中,我們盡可能建立與客戶環境一樣的開發環境。如果客戶使用的是關聯式資料庫,就表示我們必須建立合適的資料庫系統,並且讓每個學生可以操作它。在我多年的課程裡,曾經教導學員使用 SQL Server、 Oracle、 DB2 還有其他你可以叫出名字的資料庫系統。在[Brown 95a]裡,我有寫到,在我們的開發流程中,會從物件的設計裡去導出關聯式資料庫的設計。這表示在我們設計物件的過程裡,我們必須同時決定資料庫的結構描述(schema)。也許你已經猜到了,在反覆式的設計過程中,結構描述常常會需要變更。

難在哪裡呢?

在不同的資料庫裡,用來建立資料庫或是查詢資料的 SQL 都很統一,但修改結構描述的 SQL 卻不太一樣。更正確的說,每個資料庫的廠商都有自己的標準,現在還沒有任何一個廠商可以說服大家用它的標準。像修改欄位名稱、增加或刪除欄位這樣簡單的工作,在 Oracle、 SQL Server 和 DB2 裡面的做法都不一樣。不僅指令不同,而且每個廠商又提供自己的維護工具,如(SQL*Plus、 SAF 等等),造成你必須熟悉這些工具本身的小差異。

解決方法

花了許多時間來鑽研手冊,就為了找到那些難以理解的指令,用來修改特定資料庫的欄位名稱,我最後決定為自己製作工具來處理這個問題。我發現使用一些設計樣式(design patterns),如 Command 與 Strategy,是相當明智的決定。它們讓我用幾個物件就可以建立一個非常有彈性的系統。希望對於這個設計的解釋可以讓你了解這些設計樣式的意義,並且讓你能洞悉如何運用它們。

我決定用 VisualAge for Java 來撰寫我的系統,因為 Java 對於處理不同的資料庫很強的能力和彈性。而且我也希望可以使用 VisualAge 所提供的功能,讓這個系統可以是獨立的應用程式,或是 three-tier 的形式。接下來,我會介紹  JDBC(Java Database Connectivity 規格)的功能,還有這些功能如何提供我們靈活的存取所有資料庫,而無需考慮特定資料庫的應用程式介面(API)。

超資料(Metadata)

設計這個工具的第一步,就是勾勒出系統主要的領域物件(primary domain objects)。既然我處理的是關聯式資料庫,DbTable 與 DbColumn 這兩個類別就會從我的設計裡脫穎而出。相信我,雖然這看起來很簡單,但這並不是必然的做法。我常常看到一些資料庫的工具,並沒有在實際的表格(tables) 與欄位(columns)之外,提供表達資料的物件。這樣把「事物的表象(map of the thing)」從「事物(the thing)」中抽離的方法,通常稱為超資料(Metadata)。

接下來,我這樣設計我的 DbTable 和 DbColumn 類別:

 

DbTable 包含零或多個 DbColumn,每個 DbTable 有唯一的名稱(name)。DbTable 有些方法可新增、刪除欄位,也有方法可尋找具有特定名稱的欄位。DbColumn 由三個項目簡單地組成,這三個項目包括名稱(name)、資料型別(datatype),與指示欄位是否必要(NOT NULL)的布林值。這兩個類別構成我們系統的核心。稍後你會看到,系統中其他的物件如何運用不同的方法,來使用這兩個類別。

Command 樣式

下面這一組類別,是我用來表示對於一個資料庫表可能做的動作。在這裡我專注在下面這幾個動作:加入新的欄位、刪除欄位和修改欄位名稱。也許,還有很多我實作過的動作,但是舉這三個,就足以涵蓋我所必須處理的大部分工作。現在問題來了,為什麼我需要實作這些類別呢?這些不過是表格和欄位很簡單的動作啊?實際上這些的確是 DbTable 及 DbColumn 類別所應該負責的行為,所以我需要離題一下,來說明為什麼我要將這些動作實作成類別。

我想像我自己和我的學生以下面的方法使用這個系統。首先,我從特定資料庫的表格名單裡,選出一個表格。接著我可以看到這個表格所有的欄位和相關資訊。我大概會想要選取任何一個欄位,決定去修改或刪除這個欄位,當然也能夠把新欄位加入名單內。像這一類互動式應用程式,大多數有復原與重複的能力。好處是可以避免無心之過,像是你實際上打算修改欄位名稱,卻不小心刪除了欄位。[Gamma 94] Command 樣式就可以應用在這種情況,Command 樣式的目的說明你能夠「把請求(request)封裝成物件,因此允許你用參數來表示客戶端不同的請求、請求佇列或請求紀錄,而且操作可以進行回復。」

這正是我設計裡想要有的功能。但還有一個與復原與重複的功能完全無關的原因,讓我想要使用 Command 樣式。SQL 資料庫改變資料庫結構描述的過程,牽扯到許多的小動作(deltas),也就是一連串的 SQL 指令。在資料表裡加入一個欄位,你會使用 ALTER 指令。例如你有一個 employees 資料表,而希望加入一個 salary 欄位,你會執行下面這個標準的 ANSI SQL 指令:

ALTER employees ADD salary money NOT NULL WITH DEFAULT

不同的動作(新增、刪除與修改)需要一組不同的 SQL 指令,既然表格的描述有益於展示出您所決定的資料表必須看起來像什麼,因此這些指令必須能實際達成您想要描述的結果。所以我發展出下面這樣的結構:

 

applyTo(DbTable aTable) 是負責把特定的改變放入 DbTable,而 backoutFrom(DbTable aTable) 則可以還原這個改變。generateSQLWith(SQLGenerator gen, DbTable table) 負責建立與執行把資料庫中資料表正確地變成新樣子的 SQL 語句。

現在讓我們來看看,如何在 AddColumn 類別中實作 applyTo():

public void applyTo(DbTable table) {
 table.addColumn(column);
}

相當直截了當吧?AddColumn 的 backoutFrom() 也相當簡單,是透過 removeColumn() 來實作。你應該猜到了,DeleteColumn 類別是把 applyTo()、 backoutFrom() 方法裡的訊息,addColumn() 與 removeColumn() 對調過來。RenameColumn 類別也是很直接,它同時存著舊的欄位名稱和新的欄位名稱。至於 applyTo() 就是找到舊欄位,然後改成新欄位名稱。backoutFrom() 就做完全相反的動作。

在這三個 command 類別裡,比較難了解的就是 generateSQLWith()。要了解它們要如何實作,就需要再更深入的了解我們的問題,和另外一個設計樣式:Strategy。

Strategy 樣式

就像我前面所提到的,維護結構描述最麻煩的地方,就是不同資料庫用來新增、刪除、修改的 SQL 指令都不一樣。例如在 SQL Server 裡面要改一個欄位的名稱是很簡單的:

sp_rename tableName.oldName, tablename.newName
 

另一方面,在 Oracle 的資料庫裡要完成一樣的工作,就顯得複雜些:

CREATE TABLE temporary ( newColumnName, col1, col2, …)
AS SELECT oldName, col1, col2, … FROM tableName
DROP TABLE tableName
RENAME temporary to tableName
 

為了能正確的操作資料庫,我們必須把依照資料庫而定的部分,用我們的設計封裝起來。 一個可能的方法就是繼承,為不同的資料庫產生子類別,例如 OracleAddColumn 與 SybaseAddColumn 類別,都會改寫 generateSQLWith() 方法。不過這會讓增加新資料庫很麻煩。每次增加新資料庫時,都需要為每個 command 類別撰寫子類別。這是個事倍功半的方法。

比較好的解法是使用 Strategy 樣式[Gamma 94]。Strategy 的目的是「定義一系列相關的演算法,把每個演算法封裝起來,而且可以互換使用。以不影響客戶端使用為前提,可以變換引用這些演算法」後面這一點,讓演算法的轉換與客戶端無關,正是我們所需要的。下圖就可以說明,如何把使用者所產生的特定指令,與資料庫的關聯性分離開來。

 

我們有一個抽象父類別 SQLGenerator 與不同資料庫的子類別。要注意的是,這個做法只會針對不同資料庫新增一個類別,其他的做法則會新增許多類別。每個類別需要為該種資料庫實作產生相對應 SQL 語法的方法。例如 SqlServerSQLGenerator 類別會產生 sp_rename 的 SQL 語法。而 OracleSQLGenertator 類別就會建立像之前範例所看到的複雜語法。現在讓我們看看組合起來的實例。

在 RenameCommand 中的 generateSQLWith() 方法如下:

public void generateSQLWith(SQLGenerator gen, DbTable table) {
 gen.buildRenameSqlFor(this,table);
}

 

如果第一個參數是 SqlServerSQLGenerator 的實體,就會呼叫下面這個方法:

public void buildRenameSqlFor(RenameColumn renameCommand, DbTable sourceTable) { StringBuffer spRenameCommand = new StringBuffer(); spRenameCommand.append("sp_rename "); spRenameCommand.append(renameCommand.getOldName()); spRenameCommand.append(','); spRenameCommand.append(renameCommand.getNewName()); execute(spRenameCommand.toString());}
 

OracleSQLGenerator 的實作看起來也很像,只不過是產生三個 StringBuffer,存放不同的 SQL 語法,然後依序執行。

 

最後的結果

把上述各個部分拼湊起來,最後的設計就像下面這樣:

 

最後的設計裡面,有一個 TableBuilder 類別,我們並沒有討論到。TableBuilder 有一個指令堆疊,簡單地自堆疊取出最上面的指令,再對著目前的 DbTable 取消這個指令,使用者可以執行回復的動作。或許你已經猜到了,沒錯,這就是 Builder 樣式[Gamma 94]。

後記

文章至此,希望你可以對於如何使用 Command 與 Strategy 來產生 SQL 語法(或其他的程式碼),有更多的了解。同時你也看到,個別實體的超資料描述方法,是如何簡化作業。像我前面所說的,下一篇文章,我們會回到這個範例,然後看看 JDBC 的超資料可以如何的利用。我們將會看到如何使用現有的資料來建立 DbTable,並且執行我們所產生的 SQL 指令,而不必去在乎我們實際使用的是什麼資料庫。如果你有任何的問題,請儘管問 kyle.brown@acm.org。
 

參考文獻

[Brown 96a] Brown and Whitenack, "Crossing Chasms: A pattern language for Object-Relational Integration",Pattern Languages of Program Design, Vol II, John Vlissides, Jim Coplien, and Norman Kerth Eds., Addison-Wesley, 1996

[Brown 96b] Brown and Whitenack, "A Pattern Language for Relational Databases and Smalltalk", Object Magazine, October 1996

[Gamma 94] Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Reusable Elements of Object Oriented Software, Addison-Wesley Publishing Company, Reading MA. ( http://st-www.cs.uiuc.edu/patterns/DPBook/DPBook.html )