Making Document~ Make a Database Schema SQL from StarUML's Class Diagrams
日前我在公司和同事討論一個案子的 database schema,PM初期丟下來的 database schema 是寫在 Excel .xls 檔中。我拿到手後看了一會,覺得有些怪怪的,好像有些重覆。自然要討論內容。不過,用 Excel .xls 實在不方便討論,不時地切換試算表頁,常常中斷思考與討論節奏。所以,我乾脆打開 StarUML,建立一個類別圖 (Class diagram),把 database schema 畫了出來。在類別圖中清楚地呈現了表格與關聯性。一畫之下果然討論起來就順暢多了,也重新修正了不少地方。那接下來呢?再打開程式碼編輯器,把剛剛畫出來的 database schema 類別圖的內容,重新用 SQL 語法編寫一次嗎?
喔,不,老兄,那樣太不專業了。我們可是專業的資訊人員,重複輸入同樣的資訊,不是我們該做的事。身為專業資訊人員,就應該重複利用輸入過的資訊才對。不然我們做什麼資訊管理,搞什麼資料庫?
既然我們應該重用輸入過的資訊,哪要如何做呢?我看看 StarUML 的存檔格式。非常好,是用 XML 。再用 XML 節點瀏覽工具觀察文件內容後,我決定寫一個小工具,可以從 StarUML 的類別圖中,產出我要的 SQL 文件。
類別圖
首先,看看用來表示 database schema 的類別圖內容。

在我的作法是:
- 使用類別圖 (Class Diagram)。
- 一個類別(Class)對應一張資料表(Table)。
- 以屬性(Class's attribute)表示資料欄位,可見性皆為 public 。
- 屬性應加上資料型態,若省略則視為 int 。使用 ANSI SQL 標準資料型態。
- 以關聯 (association) 表示表格關聯性。這關聯性僅用於標明一對一、一對多此類關聯。
- 鍵值外部關聯鍵值,於資料型態中表示。
- 沒有行為。
- 以註解(Note)說明屬性的意義與用途。
- 一份註解關聯一張表,並加上註解連結(NoteLink)。
- 以 “空行” 或 “非空格開頭” 為分段。開頭為欄位名稱的註解段落,該段落皆為對應欄位的註解內容。
我要說明一點,用類別圖表示 database schema 的方式很多,OMG 並沒有給我們"標準答案"。你要怎麼畫都行。只要有脈絡可循,能夠抓出資訊就可以。
此外,上述作法並不是我一開始就這麼做的。我一開始畫的很簡單,類別、屬性,再加上幾條關聯線。是跟著我後來寫的小工具,才開始形成上述編寫慣例。慣例、慣例,請把這件事放在心中: Agile method 以慣例取代教條、規定。請隨時留意團隊成員在編程時的小習慣,也許那就是提升軟體開發效率的關鍵。
產生 SQL 文件
接著,我用 PHP 寫了一個小工具解析 StarUML 的文件內容,將 Database schema 的內容從類別圖中取出,轉換成 SQL 語法後輸出。這樣就產生了我要的 SQL 文件了。喔對,這動作就是 UML 用法中的正向工程 (See also: UML精華第三版,Martin Fowler著)。
<?php
// input data.
if (PHP_SAPI == 'cli') {
if ($argc == 2 and $argv[1] == 'help') {
echo "$argv[0] input_start_uml_file db_class_diagram_name\n";
exit(1);
}
$starUmlFile = isset($argv[1]) ? $argv[1] : 'project.uml';
$dbSchemaDiagramName = isset($argv[2]) ? $argv[2] : 'DB';
}
else {
$argv = explode('/', $_SERVER['PATH_INFO']);
$starUmlFile = '../'
. preg_replace('/\\\\/', '', $argv[1])
. '/doc/'
. preg_replace('/\\\\/', '', $argv[2]);
$dbSchemaDiagramName= $argv[3];
}
/**
* CODE START
*/
if ( !is_readable($starUmlFile) ) {
echo $starUmlFile, " is not readable!\n";
exit(1);
}
// Get the class diagram of database schema
foreach (simplexml_load_file($starUmlFile)->xpath('//XPD:OBJ[@type="UMLModel"]/XPD:OBJ[@type="UMLClassDiagram"]')
as $classDiagram)
{
$name = (string) current($classDiagram->xpath('XPD:ATTR[@name="Name"]'));
if ($name == $dbSchemaDiagramName) {
$dbSchemaXml = $classDiagram;
break;
}
}
if ( !isset($dbSchemaXml) ) {
echo $dbSchemaDiagramName, " is not found!\n";
exit(1);
}
$viewTypeSet = array(
'UMLClassView',
'UMLNoteView',
'UMLNoteLinkView'
);
foreach ($viewTypeSet as $viewName) {
$viewSet = $viewName . 'Set'; // like 'UMLClassViewSet'
$$viewSet = array(); // $UMLClassViewSet = array();
foreach ($dbSchemaXml->xpath("XPD:OBJ[@type='UMLClassDiagramView']/XPD:OBJ[@type='{$viewName}']")
as $viewXmlNode)
{
${$viewSet}[(string)$viewXmlNode['guid']] = $viewXmlNode;
}
}
/**
* Fetch each note text associatied to class.
*/
$classNoteSet = array();
foreach ($UMLNoteLinkViewSet as $guid => $noteLinkView) {
if ($refSet = $noteLinkView->xpath('XPD:REF')) { // if not false
$headGuid = (string)$refSet[0];
$tailGuid = (string)$refSet[1];
if (isset($UMLClassViewSet[$headGuid])) {
$classGuid = (string)current($UMLClassViewSet[$headGuid]->xpath('XPD:REF'));
// xpath('XPD:REF[1]')
$classNote = $UMLNoteViewSet[$tailGuid];
}
else if (isset($UMLClassViewSet[$tailGuid])) {
$classGuid = (string)current($UMLClassViewSet[$tailGuid]->xpath('XPD:REF'));
// xpath('XPD:REF[1]')
$classNote = $UMLNoteViewSet[$headGuid];
}
$classNoteSet[$classGuid] = (string)current($classNote->xpath('XPD:ATTR[@name="Text"]'));
}
} // endforeach ($UMLNoteLinkViewSet as $guid => $noteLinkView)
/**
* Fetch table's information
*/
$tableSet = array();
foreach ($UMLClassViewSet as $guid => $node) {
$tableGuid = (string)current($node->xpath('XPD:REF[@name="Model"]'));
$tableNode = current($dbSchemaXml->xpath('../XPD:OBJ[@guid="' . $tableGuid . '"]'));
$tableName = (string)current($tableNode->xpath('XPD:ATTR[@name="Name"]'));
$tableSet[$tableGuid] = array();
$tableSet[$tableGuid]['name'] = $tableName;
$currentTable =& $tableSet[$tableGuid];
if (isset($classNoteSet[$tableGuid]))
$tableNotes = explode("\n", $classNoteSet[$tableGuid]);
else
$tableNotes = array(); //There is no note that link to this class.
$tableFields = $tableNode->xpath('XPD:OBJ[@type="UMLAttribute"]');
foreach ($tableFields as $field) {
$fieldName = (string)current($field->xpath('XPD:ATTR[@name="Name"]'));
$fieldDataType = current($field->xpath('XPD:ATTR[@name="TypeExpression"]'));
if ($fieldDataType != false) {
$fieldDataType = (string)$fieldDataType;
}
else {
$fieldDataType = 'int'; //default type
}
$currentTable['fields'][$fieldName] = array();
$currentTable['fields'][$fieldName]['dataType'] = $fieldDataType;
$currentTable['fields'][$fieldName]['note'] = fetchNoteOfField($fieldName, &$tableNotes);
} // endforeach ($tableFields as $field)
} // endforeach ($dbSchemaXml->xpath('../XPD:OBJ[@type="UMLClass"]') as $tableNode)
/**
* Fetch note of field from tableNotes.
*
* 以 "空行" 或 "非空格開頭" 為分段。
* 找尋開頭為 欄位名稱 的註解段落,該段落皆為此欄位的註解內容。
*/
function fetchNoteOfField($fieldName, $tableNotes) {
$fetching = false;
$noteText = '';
foreach ($tableNotes as $noteLine) {
//找尋開頭為 欄位名稱 的註解段落
if ((stripos($noteLine, $fieldName) === 0) and !$fetching) {
$fetching = true;
$noteText = trim(substr($noteLine, strlen($fieldName)), ' :');
}
else if ($fetching) {
//"空行" 或 "非空格開頭" 分段。
if (empty($noteLine) or (strpos($noteLine, ' ') !== 0))
break;
//空格開頭,視為同一段落,接在先前的內容之後。
$noteText .= trim($noteLine);
}
}
return $noteText;
}
function makeSQLCreateTableString($table) {
$fields = array();
$n = count($table['fields']);
//foreach ($table['fields'] as $name => $field) {
for ($i = 1; $i <= $n; ++$i) {
list($name, $field) = each($table['fields']);
$fields[] = " "{$name}" {$field['dataType']}"
. ($i < $n ? ', ' : '')
. (empty($field['note']) ? '' : " -- {$field['note']}")
. "\n";
}
$SQLString = "CREATE TABLE "{$table['name']}" (\n"
. implode('', $fields)
. ");\n";
return $SQLString;
}
/**
* Output
*/
if (PHP_SAPI != 'cli')
header('Content-type: text/plain; charset="utf-8"');
foreach ($tableSet as $table) {
echo makeSQLCreateTableString(&$table);
}
?>
底下是用這個小工具,針對上面的類別圖所產出的 SQL 文件內容。一份 UML 名件中會有很多張類別圖,我習慣上把表示 Database schema 的類別圖命名為 'DB'。記得告訴這個小工具要從哪張類別圖中抓出資料。
# php makeDbSchemaFromUml example.uml DB
CREATE TABLE "Product" (
"id" int primary key,
"name" varchar(255), -- 商品名稱
"cost" numeric(10,2) not null -- 成本
);
CREATE TABLE "Customer" (
"id" int primary key,
"name" varchar(255)
);
CREATE TABLE "Order" (
"id" int primary key,
"customerId" int references "Customer"(id), -- 顧客編號
"orderDate" timestamp, -- 訂購時間
"deliveryAddress" varchar(255), -- 送貨地址
"deliveryDate" timestamp, -- 送貨時間
"type" char -- 訂單類型
);
CREATE TABLE "OrderItem" (
"id" int primary key,
"orderId" int references "Order"(id),
"productId" int references "Product"(id),
"price" numeric(10,2) not null, -- 售價
"qty" int not null -- 購買數量
);
結語
If you can make, do not write again.
一般人在初次接觸 Agile method 時,會有 Agile method 不寫文件的印象。但我可以肯定的說: 這不是正確的印象。Agile method 一樣產出文件(注意我用"產出"這個字,而非"編寫")。但是我們用得是具有重用性、結構性的表達方式記述文件。能用純文字檔格式記錄的,我們就不用 Word .doc (重用性較純文字檔差);能用程式語言寫下,我們就不用英文、中文等日常語言寫 (結構性較程式語言差)。所以我們說: 源碼即文件
。
在本文中,我也是遵循著這個原則。PM原來用 Excel .xls 編寫,可惜重用性不高,也不方便表達關聯性。所以我改用 UML 畫出來。同時,StarUML 也將我畫出來的資訊,再用 XML 型式儲存,這就提供了一份重用性與結構性兼具的源碼。於是,我可以根據我的需求寫一個小工具,產出(make)我要的 SQL 文件,不而是重複編寫(write)。
樂多舊回應