最近更新: 2008-03-18

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 的類別圖內容。

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)

樂多舊網址: http://blog.roodo.com/rocksaying/archives/5717961.html

樂多舊回應
未留名 (#comment-16089539)
Thu, 27 Mar 2008 17:25:42 +0800
老弟,好久不見!
這篇文章不錯喔!很好用!
但是StartUML是for win,
你php在web上跑,這樣用起來粉麻煩!
ps:我是duba啦!
未留名 (#comment-16177687)
Tue, 08 Apr 2008 11:40:28 +0800
PHP 跨平台啊,我是在 Win32 平台下 working ,在 linux 平台下 testing 。

duba? 那位我在淡水工商學院唸書時的 duba嗎?
還真是好久不見,七、八年有了吧。

近來在哪高就?看你的回應,難不成在軟體開發產業?

ps.怎麼沒有留下 email 啊?
solman@pchome.com.tw(duba) (#comment-18190161)
Fri, 19 Dec 2008 23:54:32 +0800
沒錯啊~~我就是淡水duba
我留下Email& MSN了,
我現在作DotNet軟體平台開發,
PHP很久沒摸了!