2016/06/27

康威定律 Conway's Law

Conway's law 是 1967 年一位 computer programmer Melvin Conway 說的一段話,在 1968 年的 National Symposium on Modular Programming 會議中,正式被命名。

康威定律 Conway's Law

Conway's Law 的內容如下:organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations

中文翻譯有人這樣寫:

人力組織的架構往往決定了設計的層次。

也有人這樣寫:

公司開發的產品和服務其實都是公司自身組織架構、溝通與工作方式的反映。

設計系統的組織,其產生的設計和架構等價於組織間的溝通結構。

OSI 的 co-founder Eric S Raymond 曾說,如果有四個小組在實作 compiler 的時候,最終我們會得到一個需要四個步驟的 compiler。

角色和職責 Role and Responsibility

每個架構師都應該研究下康威定律 這篇文章中,談到了作者對於架構設計以及演化的一些概念。

架構其實是發現利益相關者(stakeholder),然後解決他們的關注點(concerns)。架構師的首要任務是盡最大可能找出所有利益相關者,業務方,產品經理,客戶/用戶,開發經理,工程師,項目經理,測試人員,運維人員,產品運營人員等等都有可能是利益相關者。

  1. 每個系統都有一個架構
  2. 架構由架構元素以及相互之間的關係構成
  3. 系統是為了滿足利益相關者(stakeholder)的需求而構建的
  4. 利益相關者都有自己的關注點(concerns)
  5. 架構由架構文檔描述
  6. 架構文檔描述了一系列的架構視角
  7. 每個視角都解決並且對應到利益相關者的關注點。

架構主要關注非功能性需求(non-functional requirements),即所謂的-abilities。

  1. Easy to separate - Autonomy
  2. Easy to understand - Understandability
  3. Easy to extend - Extensibility
  4. Easy to change - Changeability
  5. Easy to replace - Replaceability
  6. Easy to scale - Scalability
  7. Easy to recover - Resilience
  8. Easy to connect - Uniform interface
  9. Easy to afford - Cost-efficiency

最小可用產品(Minimum Viable Product, MVP): 做出最小可用產品,盡快丟給用戶試用,快速獲取客戶反饋,在此基礎上不斷迭代和演化架構和產品。

Over Engineering: 產品架構和用戶之間沒有形成有效的反饋閉環,架構師想的和客戶想的不在一個方向上,通過最小可用產品,快速迭代反饋的策略,可以避免這種問題。

圖像化組織結構

Manu Cornet: Organizational Charts

Manu Cornet 畫了一組美國科技公司的組織結構圖,亞馬遜等級森嚴且有序;谷歌結構清晰,產品和部門之間卻相互交錯且混亂;Facebook架構分散,就像一張散開的網絡;微軟內部各自為政;蘋果一個人說了算,而那個人路人皆知;龐大的甲骨文,臃腫的法務部顯然要比工程部門更加重要。

接下來大陸跟進,畫了許多大陸 IT 公司的組織圖。

瘋狂的架構——國內六大著名科技公司組織結構圖一覽

如果康威定律真的成立,很難想像每個公司的系統有著跟組織圖一樣的關係,

好的架構源於不停地進化及演變,而非設計

宜人貸系統架構——高並發下的進化之路

58同城沈劍:好的架構源於不停地演變,而非設計

網站在不同的階段遇到的問題不一樣,而解決這些問題使用的技術也不一樣,流量小的時候,主要目的是提高開發效率。隨著流量變大,使用動靜分離、讀寫分離、主從同步、垂直拆分、CDN、MVC等方式不斷地提升網站穩定性。

系統架構缺少的東西

在系統的正向回饋循環中,最重要的是建立一個測試的程序,如果認定系統架構有著不斷演化的過程,系統會不斷地改變,那麼就必須要正視演化的步驟,最重要的就是測試。

但是系統在一開始開發時,不僅資料的變化大,系統的存取介面也會不斷地調整與修改,這時候,就會形成一個氣氛,那就是不撰寫測試的程式,而是直接在存取介面上進行測試,最終就是在使用者介面上進行測試。

但是使用者介面測試卻是一個最難以自動化的測試,尤其在做手機 APP 軟體更是如此,沒有一個方便的工具,可以自動化使用者介面測試,或者說,要達成自動化的 UI 測試,會需要使用更多的 programmer resource,公司能不能、要不要花費這樣的成本是個根本的大問題。

專案最需要的,是在一開始就建立好更新的流程,除了 Server 及 Client 的獨立更新,這個更新包含了 Server 跟 Client 的對應更新的方法,另外還有測試環境跟正式環境的切換方法,再加上現在的 ios APP 上架跟測試有時間差,這會造成更多更新的問題。

建立自動化測試、完整的更新程序,最後還有自動化部署跟監控系統的問題。這也是 DevOps 所關心的問題,有了完整的人力資源,隨著業務量的提升,就需要建立這些方面的處理方案。

References

很威的康威定律 (Conway's Law)

Conway's Law 康威定律

不會帶團隊的領導者,只能自己幹到死

半個世紀過去了,康威定律依然適用

2016/06/20

如何使用 Apache POI 處理 Excel 檔案

Apache POI 是 Poor Obfuscation Implementation 的縮寫,其目的是建立與讀取 Office Open XML(OOXML)標準和微軟的OLE 2復合文檔格式(OLE2)的Java API。

主要的元件有:

  • Excel (SS=HSSF+XSSF)
  • Word (HWPF+XWPF)
  • PowerPoint (HSLF+XSLF)
  • OpenXML4J (OOXML)
  • OLE2 Filesystem (POIFS): OLE 2 Compound Document format 的Java Implementation
  • OLE2 Document Props (HPSF): Open Packaging Conventions (OPC) 的 Java Implementation
  • Outlook (HSMF)
  • Visio (HDGF+XDGF)
  • TNEF (HMEF): Microsoft's TNEF (Transport Neutral Encoding Format),也就是 winmail.dat,用在 Outlook 跟 Exchange -Publisher (HPBF): Publisher file format

怎麼會命名成 Poor Obfuscation Implementation

POI 套件從 2001 年就開始了初始專案,由於 MS 的封閉特性,Office 檔案格式並沒有開放,作者就戲稱這個檔案格式是非常難以被理解,很混亂的一種檔案,就用了 Poor Obfuscation 這兩個字,當然還是成功地被 reverse-engineered,成就了這個專案,除了 MS Office 軟體之外,我們現在也可以用程式產生 Office 檔案。

後來在 2008 年,MS終於向 Sourcesense 提交了 ISO/IEC 29500:2008 Office Open XML file formats,等於是開放了 OOXML 的 Office 檔案的標準,POI 就以這個規格,實作了接下來的函式庫,支援這個 OOXML 的標準規格。

HSSF, XSSF

Excel 分為兩種檔案格式,比較舊的是 HSSFWorkbook,檔案格式為 Excel 1997-2003 版的Excel,副檔名是 xls,XSSFWorkbook 是 Excel 2007-10 的版本,副檔名是 xlsx。

基本的階層概念為一個 xlsx 檔案 XSSFWorkbook,裡面有多個工作表 XSSFSheet,每一個 Sheet 下面有一個表格,裡面有多列資料 XSSFRow,每列資料中有多欄資料儲存格 XSSFCell。

文件的資料階層是

XSSFWorkbook -> XSSFSheet -> XSSFRow -> XSSFCell

Read/Write XLSX file in Java

以下的 Java 程式會產生一個 Test.xlsx 檔案,包含兩個工作表。如果要處理的是 xls 檔案,只要把所有 XSSF 開頭的 class 都換成 HSSF 就可以了。

import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

public class PoiTest {
    public static void readXLSXFile() throws IOException {
        InputStream ExcelFileToRead = new FileInputStream("Test.xlsx");
        XSSFWorkbook wb = new XSSFWorkbook(ExcelFileToRead);

        XSSFSheet sheet = wb.getSheetAt(0);
        XSSFRow row;
        XSSFCell cell;

        Iterator rows = sheet.rowIterator();

        while (rows.hasNext()) {
            row = (XSSFRow) rows.next();
            Iterator cells = row.cellIterator();
            while (cells.hasNext()) {
                cell = (XSSFCell) cells.next();

                if (cell.getCellType() == XSSFCell.CELL_TYPE_STRING) {
                    System.out.print(cell.getStringCellValue() + " ");
                } else if (cell.getCellType() == XSSFCell.CELL_TYPE_NUMERIC) {
                    System.out.print(cell.getNumericCellValue() + " ");
                } else {
                    //U Can Handel Boolean, Formula, Errors
                }
            }
            System.out.println();
        }

    }

    public static void writeXLSXFile() throws IOException {

        String excelFileName = "Test.xlsx";//name of excel file

        XSSFWorkbook wb = new XSSFWorkbook();

        Font titlefont = wb.createFont();
        titlefont.setColor(HSSFColor.BLACK.index);//顏色
        titlefont.setBoldweight(Font.BOLDWEIGHT_BOLD); //粗體

        CellStyle styleRow1 = wb.createCellStyle();
        styleRow1.setFillForegroundColor(HSSFColor.LIGHT_GREEN.index);//填滿顏色
        styleRow1.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
        styleRow1.setFont(titlefont);//設定字體
        styleRow1.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平置中
        styleRow1.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直置中

        //設定框線
        styleRow1.setBorderBottom((short)1);
        styleRow1.setBorderTop((short)1);
        styleRow1.setBorderLeft((short)1);
        styleRow1.setBorderRight((short)1);
        styleRow1.setWrapText(true);//自動換行

        CellStyle styleRow2 = wb.createCellStyle();
        styleRow2.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平置中
        styleRow2.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直置中
        styleRow2.setBorderBottom((short)1);
        styleRow2.setBorderTop((short)1);
        styleRow2.setBorderLeft((short)1);
        styleRow2.setBorderRight((short)1);
        styleRow2.setWrapText(true);//自動換行


        String sheetName2 = "工作表2";//name of sheet
        XSSFSheet sheet2 = wb.createSheet(sheetName2);

        XSSFRow titlerow = sheet2.createRow(0);
        for (int c = 0; c < 6; c++) {
            XSSFCell cell = titlerow.createCell(c);
            cell.setCellStyle(styleRow1);
            cell.setCellValue("標題 Cell 0 " + c);
            sheet2.autoSizeColumn(c);   //自動調整欄位寬度
        }

        for (int r = 1; r < 10; r++) {
            XSSFRow row = sheet2.createRow(r);

            for (int c = 0; c < 5; c++) {
                XSSFCell cell = row.createCell(c);
                cell.setCellStyle(styleRow2);

                cell.setCellValue("中文 Cell " + r + " " + c);

                sheet2.autoSizeColumn(c);   //自動調整欄位寬度
            }

            XSSFCell cell = row.createCell(5);
            cell.setCellValue(100);
            sheet2.autoSizeColumn(5);
        }

        String sheetName = "工作表";//name of sheet

        XSSFSheet sheet = wb.createSheet(sheetName);

        for (int r = 0; r < 5; r++) {
            XSSFRow row = sheet.createRow(r);

            for (int c = 0; c < 5; c++) {
                XSSFCell cell = row.createCell(c);

                if( r==0 ) {
                    cell.setCellStyle(styleRow1);
                } else {
                    cell.setCellStyle(styleRow2);
                }

                cell.setCellValue("中文 title " + r + " " + c);

                sheet.autoSizeColumn(c);   //自動調整欄位寬度
            }
        }

        FileOutputStream fileOut = new FileOutputStream(excelFileName);

        wb.write(fileOut);
        fileOut.flush();
        fileOut.close();
    }

    public static void main(String[] args) throws IOException {
        writeXLSXFile();
        readXLSXFile();
    }

}

Read/Write XLSX file in Scala

將上面的 Java 版本的程式翻譯成 Scala 的寫法,其中有個部分要注意,在 Java 程式中,有一段 XSSFCell.CELLTYPESTRING,使用了 XSSFCell 實作的 Cell 介面,在 Cell 介面中定義的常數 CELLTYPESTRING。

雖然 Scala 跟 Java 相容,但是在 Scala 沒辦法直接使用 Java 介面裡面定義的常數,所以我們只有再用 Scala object 再定義一次常數。

import java.io.{FileInputStream, FileOutputStream, IOException, InputStream}
import java.util.Iterator

import org.apache.poi.hssf.util.HSSFColor
import org.apache.poi.ss.usermodel.{CellStyle, Font}
import org.apache.poi.xssf.usermodel.{XSSFCell, XSSFRow, XSSFSheet, XSSFWorkbook}

object Cell {
  val CELL_TYPE_NUMERIC: Int = 0
  val CELL_TYPE_STRING: Int = 1
  val CELL_TYPE_FORMULA: Int = 2
  val CELL_TYPE_BLANK: Int = 3
  val CELL_TYPE_BOOLEAN: Int = 4
  val CELL_TYPE_ERROR: Int = 5
}

object CellStyle {
  val ALIGN_GENERAL:Short = 0
  val ALIGN_LEFT:Short = 1
  val ALIGN_CENTER:Short = 2
  val ALIGN_RIGHT:Short = 3
  val ALIGN_FILL:Short = 4
  val ALIGN_JUSTIFY:Short = 5
  val ALIGN_CENTER_SELECTION:Short = 6
  
  val VERTICAL_TOP:Short = 0
  val VERTICAL_CENTER:Short = 1
  val VERTICAL_BOTTOM:Short = 2
  val VERTICAL_JUSTIFY:Short = 3
  
  val BORDER_NONE:Short = 0
  val BORDER_THIN:Short = 1
  val BORDER_MEDIUM:Short = 2
  val BORDER_DASHED:Short = 3
  val BORDER_HAIR:Short = 7
  val BORDER_THICK:Short = 5
  val BORDER_DOUBLE:Short = 6
  val BORDER_DOTTED:Short = 4
  val BORDER_MEDIUM_DASHED:Short = 8
  val BORDER_DASH_DOT:Short = 9
  val BORDER_MEDIUM_DASH_DOT:Short = 10
  val BORDER_DASH_DOT_DOT:Short = 11
  val BORDER_MEDIUM_DASH_DOT_DOT:Short = 12
  val BORDER_SLANTED_DASH_DOT:Short = 13
  
  val NO_FILL:Short = 0
  val SOLID_FOREGROUND:Short = 1
  val FINE_DOTS:Short = 2
  val ALT_BARS:Short = 3
  val SPARSE_DOTS:Short = 4
  val THICK_HORZ_BANDS:Short = 5
  val THICK_VERT_BANDS:Short = 6
  val THICK_BACKWARD_DIAG:Short = 7
  val THICK_FORWARD_DIAG:Short = 8
  val BIG_SPOTS:Short = 9
  val BRICKS:Short = 10
  val THIN_HORZ_BANDS:Short = 11
  val THIN_VERT_BANDS:Short = 12
  val THIN_BACKWARD_DIAG:Short = 13
  val THIN_FORWARD_DIAG:Short = 14
  val SQUARES:Short = 15
  val DIAMONDS:Short = 16
  val LESS_DOTS:Short = 17
  val LEAST_DOTS:Short = 18
}

object ScalaPoiTest {

  @throws(classOf[IOException])
  def readXLSXFile {
    val ExcelFileToRead: InputStream = new FileInputStream("Test.xlsx")
    val wb: XSSFWorkbook = new XSSFWorkbook(ExcelFileToRead)

    val sheet: XSSFSheet = wb.getSheetAt(0)
    var row: XSSFRow = null
    var cell: XSSFCell = null

    val rows: Iterator[_] = sheet.rowIterator
    while (rows.hasNext) {
      row = rows.next.asInstanceOf[XSSFRow]
      val cells: Iterator[_] = row.cellIterator
      while (cells.hasNext) {
        cell = cells.next.asInstanceOf[XSSFCell]

        if (cell.getCellType == Cell.CELL_TYPE_STRING) {
          System.out.print(cell.getStringCellValue + " ")
        }
        else if (cell.getCellType == Cell.CELL_TYPE_NUMERIC) {
          System.out.print(cell.getNumericCellValue + " ")
        }
        else {
        }
      }
      System.out.println
    }
  }

  @throws(classOf[IOException])
  def writeXLSXFile {
    val excelFileName: String = "Test.xlsx"

    val wb: XSSFWorkbook = new XSSFWorkbook

    val titlefont: Font = wb.createFont
    titlefont.setColor(HSSFColor.BLACK.index)
    titlefont.setBoldweight(Font.BOLDWEIGHT_BOLD)

    val styleRow1: CellStyle = wb.createCellStyle
    styleRow1.setFillForegroundColor(HSSFColor.LIGHT_GREEN.index)
    styleRow1.setFillPattern(CellStyle.SOLID_FOREGROUND)
    styleRow1.setFont(titlefont)
    styleRow1.setAlignment(CellStyle.ALIGN_CENTER)
    styleRow1.setVerticalAlignment(CellStyle.VERTICAL_CENTER)
    styleRow1.setBorderBottom(1)
    styleRow1.setBorderTop(1)
    styleRow1.setBorderLeft(1)
    styleRow1.setBorderRight(1)
    styleRow1.setWrapText(true)

    val styleRow2: CellStyle = wb.createCellStyle
    styleRow2.setAlignment(CellStyle.ALIGN_CENTER)
    styleRow2.setVerticalAlignment(CellStyle.VERTICAL_CENTER)
    styleRow2.setBorderBottom(1)
    styleRow2.setBorderTop(1)
    styleRow2.setBorderLeft(1)
    styleRow2.setBorderRight(1)
    styleRow2.setWrapText(true)


    val sheetName2: String = "Sheet2"
    val sheet2: XSSFSheet = wb.createSheet(sheetName2)
    val titlerow: XSSFRow = sheet2.createRow(0)
      for (c <- 0 to 6) {
          val cell: XSSFCell = titlerow.createCell(c)
          cell.setCellStyle(styleRow1)
          cell.setCellValue("標題 Cell 0 " + c)
          sheet2.autoSizeColumn(c)
      }

    for (r <- 1 to 5) {
      val row: XSSFRow = sheet2.createRow(r)

      for (c <- 0 to 5) {
        val cell: XSSFCell = row.createCell(c)
        cell.setCellValue("中文 Cell " + r + " " + c)
        sheet2.autoSizeColumn(c)
      }

      val cell: XSSFCell = row.createCell(6)
      cell.setCellValue(100)
      sheet2.autoSizeColumn(6)
    }

    val sheetName: String = "Sheet"
    val sheet: XSSFSheet = wb.createSheet(sheetName)

    for (r <- 0 to 4) {
      val row: XSSFRow = sheet.createRow(r)

      for (c <- 0 to 4) {
        val cell: XSSFCell = row.createCell(c)
        cell.setCellValue("中文 Cell " + r + " " + c)
        sheet.autoSizeColumn(c)
      }
    }

    val fileOut: FileOutputStream = new FileOutputStream(excelFileName)
    wb.write(fileOut)
    fileOut.flush
    fileOut.close
  }

  @throws(classOf[IOException])
  def main(args: Array[String]) {
    writeXLSXFile
    readXLSXFile

  }
}

Scala 不能使用 Java Interface constant members

以下這個 stackoverflow 討論中,有談到剛剛在 Scala 版本中遇到的問題:Scala can not resolve inherited Java interface constant members

基本上沒有什麼特殊的解決方案,在 Scala 就是不支援這樣子的寫法。

References

madan712/ReadWriteExcelFile.java

HSSF and XSSF Examples

2016/06/13

MySQL driver for Erlang

MySQL/OTP 的作者提供了一份 MySQL Driver for Erlang 的比較表:Comparison of MySQL clients,除了使用最原始的 ODBC 連接 MySQL 之外,要在 erlang 連接 MySQL 目前有四個 driver:MySQL/OTPEmysql、erlang-mysql-driver、YXA。

Emysql#History 有說明後面三個 driver 的演進的過程,從最早的 YXA,接手的是 erlang-mysql-driver,再來是 Emysql,MySQL/OTP 的作者原本也是 Emysql 的貢獻者,但覺得這個 driver 為了跟舊版 MySQL 相容,而有一些限制,因此他又做了一個 MySQL/OTP 的 driver,另外該作者又利用 PoolBoy 製作了 Connection Pool 的功能,就是 mysql-otp-poolboy

MySQL-OTP

要直接使用 MySQL-OTP 並不困難,基本上依照 專案首頁 的方式,用 mysql:start_link 就可以連接資料庫,mysql:query 取得資料。

(web@cmbp)8> {ok, Pid} = mysql:start_link([{host, "localhost"}, {user, "root"},{password, "pwd"}, {database, "test"}]).
{ok,<0.146.0>}
(web@cmbp)9> {ok, ColumnNames, Rows} = mysql:query(Pid, <<"SELECT * FROM dbversion WHERE versiontxt = ?">>, ["test"]).
{ok,[<<"dbversionseq">>,<<"versionno">>,<<"versiontxt">>,
     <<"updatedate">>,<<"updateby">>,<<"createdate">>,
     <<"createby">>],
    [[1,1,<<"test">>,
      {{2016,3,25},{15,35,47}},
      0,
      {{2016,3,25},{15,35,47}},
      0]]}

MySQL-OTP Poolboy

MySQL/OTP Poolboy 的部分,依照 網頁 的說明,有兩種啟動 supervisor 的方式:一種是放在自己的 supervisor 裡面,一種是使用專案中內建的 supervisor。

兩種方式都試過可以用,以下紀錄怎麼用專案中內建的 supervisor。

首先要製作 OTP appcation config 檔案,通常是放在 sys.config 裡面。

[
  {mysql_poolboy, [
    {pool1,
      {
        [
          {size, 10}, {max_overflow, 20}
        ],
        [
          {host, "localhost"},
          {port, 3306},
          {user, "root"},
          {password, "password"},
          {database, "test"},
          {keep_alive, true},
          {prepare, [{foo, "SELECT sysdate()"}]}
        ]
      }
    }
  ]}
].

撰寫一個 otp server,在啟動 mysql_poolboy 以前,要先啟動 mysql 跟 poolboy。

-module(server).

-behaviour(application).

%% Application callbacks
-export([start/2, stop/1]).

%% ===================================================================
%% Application callbacks
%% ===================================================================

start(_StartType, _StartArgs) ->
  init_mysql()
  ok.
stop(_State) ->
  ok.

init_mysql() ->
  application:start(mysql),
  application:start(poolboy),
  application:start(mysql_poolboy),

  ok.

啟動 MySQL/OTP Pooboy 之後,就是要使用這個 mysql connection pool,用以下的方式,可以查詢出 table 的結果

{ok, L1, L2} = mysql_poolboy:query(pool1, <<"SELECT * FROM dbversion">>)

用以下的指令啟動 otp server

erl -pa ./deps/*/ebin ./ebin -config sys.config -eval "application:start(server)"

處理 Result Set

利用 query 查詢的結果,資料結構就像下面這樣,第一個是 ok,第二個是所有欄位名稱的 list,第三個部分是所有結果的 list of list。

mysql_poolboy:query(pool1, <<"SELECT * FROM dateserial">>).

{ok,[<<"dateserialseq">>,<<"datestr">>,<<"datenumber">>,
     <<"updatedate">>,<<"updateby">>,<<"createdate">>,
     <<"createby">>],
    [[8,<<"20160330">>,9,
      {{2016,3,30},{18,26,35}},
      0,
      {{2016,3,30},{18,26,28}},
      0],
     [9,<<"20160331">>,10,
      {{2016,3,31},{9,56,32}},
      0,
      {{2016,3,31},{9,53,29}},
      0]]}

處理日期

erlang 的日期格式是以下這個樣子,但對網頁資料處理來說,這個結構是沒辦法處理的。我們可以利用 erlware_commons 的 ec_date 作為日期轉換的 library。

{{2016,3,31},{9,56,32}}

利用 ec_date 就可以將日期轉換成字串 2016-03-31 09:56:32

ec_date:format("Y-m-d H:i:s", {{2016,3,31},{9,56,32}})

但剛剛的 MySQL Result Set 裡面的日期,並沒有固定的欄位位置,我們寫一個 function,用來將 list 的 Index 位置的元素,轉換成日期字串。

convertDateString(L,Index) ->
  {L1,[OldValue|L2]} = lists:split(Index-1,L),
  L1++[ list_to_binary(ec_date:format(?DATE_FORMATE, OldValue)) |L2].

以剛剛的 sql 查詢,就是第 4 以及第 6 個欄位位置是需要轉換的日期。

  %% convert date to bit string
        L3 = lists:map( fun(L2Map) -> convertDateString(L2Map, 4) end, L2 ),
        L4 = lists:map( fun(L2Map) -> convertDateString(L2Map, 6) end, L3 )

轉換 JSON

通常,我們會直接將資料庫取得資料的結果,以 JSON 的方式傳給前端網頁的 javascript 程式處理,所以需要設法將這些資料轉換為 JSON。

JSON 會以 jsx 這個套件來輔助處理,我們先看 encode 跟 decode 的部分,Erlang 在 17 版之後提供了 maps 的資料結構,在這裡我們有看到 jsx 有支援 maps,所以目標是先將 MySQL Result Set 轉換成 maps,再透過 jsx 轉換成 JSON。

因為是 Result Set 是 list of maps,對於 JSON 來說,前面需要再加上一個 key,才是一個完整的 JSON。

#{<<"res">> => L5 }

透過 lists:map 以及 lists:zip 就可以完成這項工作,最後再用 jsx:encode 轉換成 JSON。

%% convert two lists to  list of maps
        L5 = lists:map( fun(L4Map) -> maps:from_list(lists:zip(L1, L4Map)) end, L4 ),
%% add key
        L6= #{<<"res">> => L5 },
%% convert to json
        Json= jsx:encode(L6)

完整的範例

以下是處理 MySQL Result Set 的完整範例程式。

  Result = mysql_poolboy:transaction(pool1,
      fun(Pid) ->
        {ok, L1, L2} = mysql_poolboy:query(pool1, <<"SELECT * FROM dateserial">>),
        lager:debug("~n L1=~p.~n", [L1]),
        lager:debug("~n L2=~p.~n", [L2]),

        %% convert date to bit string
        L3 = lists:map( fun(L2Map) -> convertDateString(L2Map, 4) end, L2 ),
        L4 = lists:map( fun(L2Map) -> convertDateString(L2Map, 6) end, L3 ),

        %% convert two lists to  list of maps
        L5 = lists:map( fun(L4Map) -> maps:from_list(lists:zip(L1, L4Map)) end, L4 ),
        lager:debug("~n L5=~p.~n", [L5]),

        %% add key
        L6= #{<<"res">> => L5 },
        lager:debug("~n L6=~p.~n", [L6]),

        %% convert to json
        Json= jsx:encode(L6),

        lager:debug("~n Json=~p.~n", [Json]),

        %% decode json, json -> maps
        % JMap=jsx:decode(<<"{\"res\":[{\"dateserialseq\":8,\"datestr\":\"20160330\",\"datenumber\":10},{\"dateserialseq\":9,\"datestr\":\"20160331\",\"datenumber\":8}] }">>, [return_maps]).

        ok
      end
  ),

References

add transaction for emysql

emysql與erlang-mysql-driver的pool模型

Erlang Mysql: How to prevent SQL Injections

使用 prepared statements + escape characters

使用 Haproxy 搭建高可用與負載平衡叢集(一)MySQL DB 應用

MaxScale

High availability with asynchronous replication… and transparent R/W split

MySQL高可用性方案

Erlang pool management -- Emysql pool

Erlang pool management -- Emysql pool optimize

關於emysql的若干問題

2016/06/06

如何用 D3.js 繪製地圖

D3.js 是個強大熱門的網頁 svg 圖表工具,我們試著用 D3.js 在網頁上製作地圖,並在網頁上將座標點標記在上面,最後測試了如何更新標記的座標點。

利用 topojson 轉換行政區域圖 SHP 資料

台灣官方釋出了三種詳實等級不同的行政區域圖資:

  1. 縣(市)行政區界線
  2. 鄉(鎮、市、區)行政區域界線
  3. 全國村里界圖

釋出的格式均為 SHP Shapefile 檔格式,D3.js 的作者 Mike,在 Github 上提供topojson,讓我們可以讀取 SHP 檔並輸出成 GeoJSON 格式,讓我們可以直接由 SHP 檔產生 TopoJSON 檔。

要使用 topojson 必須先安裝 NodeJS 以及 npm,在 mac 可用 port 或是 homebrew 安裝。

sudo port install nodejs
sudo port install npm

接下來就用 npm 安裝 topojson

sudo npm install -g topojson

我們用了 縣(市)行政區界線 的資料,下載下來是一個壓縮檔: 直轄市、縣(市)界線檔1041215.zip,解壓縮後會有以下這些檔案:

County_MOI_1041215.dbf
County_MOI_1041215.prj
County_MOI_1041215.shp
County_MOI_1041215.shx

我們要用的是 CountyMOI1041215.shp。下一步是用 topojson 轉檔,由於原始檔的文字編碼是 big5,要指定 --shapfile-encoding ,taiwan.json 就是我們要用的 topojson 檔案。

topojson -s 0.0000001 -o taiwan.json -p --shapefile-encoding big5 County_MOI_1041215.shp

利用 D3 geo 繪製台灣地圖

先準備一個 web server,隨便一種 server 都可以,製作 index.html,include 需要的 js 檔案: d3.v3.min.js 以及 topojson.v1.min.js,還有 jquery,把剛剛的 taiwan.json 以及 index.html 放在 webserver 的同一個目錄中。。

<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>

增加兩個 css style,用在頁面底色以及縣市邊界的線段類型。

<style>
body {
    background-color: #cceeff;
}

.subunit-boundary {
    fill: none;
    stroke: #fff;
    stroke-dasharray: 5, 0;
    stroke-linejoin: round;
}
</style>

接下來是 javascipt 的部分,首先要定義 svg 的圖形尺寸

var width = 800,
    height = 600;

var svg = d3.select("body").append("svg")
    // .attr("class", "svgback")
    .attr("width", width)
    .attr("height", height);

初始化 d3.geo,projection 是 d3.geo 最重要的概念,他能將座標點投影至頁面的圖形區塊上。

var projection = d3.geo.mercator()
    .center([121,24])
    .scale(6000);

var path = d3.geo.path()
    .projection(projection);

再來是載入 taiwan.json 的路徑資料,並將他畫到 svg 上面,同時我們將縣市行政區界線畫出來。d3.json 是從 server 將 taiwan.json 載入到頁面,然後用 topology.objects["CountyMOI1041215"] 裡面描述的路徑畫到 svg 上,會寫成 "CountyMOI1041215" 的原因是因為產生出來的 taiwan.json 是由 CountyMOI1041215.shp 產生出來的。

d3.json("taiwan.json", function(error, topology) {
    var g = svg.append("g");
    
    // 縣市/行政區界線
    d3.select("svg").append("path").datum(
            topojson.mesh(topology,
                    topology.objects["County_MOI_1041215"], function(a,
                            b) {
                        return a !== b;
                    })).attr("d", path).attr("class","subunit-boundary");
    
    d3.select("g").selectAll("path")
          .data(topojson.feature(topology, topology.objects.County_MOI_1041215).features)
          .enter()
          .append("path")
          .attr("d", path)
          .attr({
                d : path,
                name : function(d) {
                    return d.properties["C_Name"];
                },
                fill : '#55AA00'
        });
});

taiwan.json 裡面的內容:

# taiwan.json
{"type":"Topology","objects":{"County_MOI_1041215":{"type":"GeometryCollection","bbox":[118.14367476000007,21.895599675000085,124.56114950000006,26.385278130000074],"geometries":[{"type":"MultiPolygon","properties":{"OBJECTID":1,"County_ID":"09007","Shape_Leng":1.55703852,"Shape_Area":0.00268483471071,"C_Name":"連江縣","
.....

目前為止,我們可以看到這樣的畫面:

在地圖上標記城市的點

準備一個 cities.csv 檔案,內容如下,放在同一個 web server 的目錄中,其實我們只有用到經緯度的資料,其他的欄位沒有用到。

code,city,country,lat,lon
KHH,KAOHSIUNG,TAIWAN,22.630865,120.310047
HUN,HUALIEN,TAIWAN,23.979337,121.595788
KNH,KINMEN,TAIWAN,24.449047,118.375968
MZG,MAKUNG,TAIWAN,23.567376,119.584375

在載入 taiwan.json 台灣地圖後,再利用 d3.csv 載入 cities.csv,將城市地點以一個紅點標記出來。

    var g = svg.append("g");
    
    // load and display the cities
    d3.csv("cities.csv", function(error, data) {
        g.selectAll("circle")
           .data(data)
           .enter()
           .append("circle")
           .attr("cx", function(d) {
                   return projection([d.lon, d.lat])[0];
           })
           .attr("cy", function(d) {
                   return projection([d.lon, d.lat])[1];
           })
           .attr("r", 5)
           .style("fill", "red");
    });

到目前為止,我們可以看到的畫面如下

因為未來我們希望能夠持續動態更換產生的圖點,我們先準備另一個檔案 cities2.csv,內容如下

code,city,country,lat,lon
TPE,TAIPEI,TAIWAN,25.034608,121.564801
TXG,TAICHUNG,TAIWAN,24.165392,120.662028

以 setInterval 定時每兩秒鐘更新一次城市的圖點,未來可以將 "cities2.csv" 換成會持續更換資料的 server 動態頁面,就可以看到地圖點持續地更新。

setInterval(function() {
    d3.csv("cities2.csv", function(error, data) {
        svg.select("g").selectAll("circle").remove();
        
        // new data joins old elements 'circle'
        var update = svg.append("g").selectAll("circle")
            .data(data)
            .enter()
            .append("circle")
            .attr("cx", function(d) {
                return projection([d.lon, d.lat])[0];
            })
            .attr("cy", function(d) {
                    return projection([d.lon, d.lat])[1];
            })
            .attr("r", 5)
            .style("fill", "red");
    });
}, 2000);

打開頁面,兩秒鐘後,就會看到畫面變成這樣

顯示縣市名稱

在 body 裡面先產生一個 panel 的 div

<div id='panel' style="display: none">
    <span id='title'></span><br />
</div>

用以下的程式,顯示 panel

    // 顯示滑鼠所指向的縣市/行政區
    d3.select("svg").selectAll("path").on("mouseenter", function() {
        // console.log(e);
        fill = $(this).attr("fill");
        $(this).attr("fill", '#00DD77');
    
        $('#title').html($(this).attr("name"));
        $('#panel').css({
            "height" : "20px",
            "width" : "60px"
        });
    }).on("mouseout", function() {
        $(this).attr("fill", fill);
    });
    
    // panel 隨滑鼠移動
    $("path").mouseover(function(e) {
        
        if($('#panel').is(':visible')){
            $('#panel').css({
                'top' : e.pageY,
                'left' : e.pageX
            });
        } else {
            $('#panel').fadeIn('slow').fadeOut();
        }
    });

完整的範例程式

以下是整個完整的範例程式

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="author" content="" />
<meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>test</title>

<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>

<style>
body {
    background-color: #cceeff;
}

.subunit-boundary {
    fill: none;
    stroke: #fff;
    stroke-dasharray: 5, 0;
    stroke-linejoin: round;
}

#panel {
    position: absolute;
    z-index: 99;
    height: 20px;
    width: 60px;
    background-color: #fff;
    -webkit-transition: all .1s;
    border-radius: 5px;
    background-color: #000;
    background-color: rgba(0, 0, 0, 0.3);
    color: #fff;
    padding: 10px;
}

.svgback {
    background-color: #ffffff;
}
</style>

</head>
<body>

<div id='panel' style="display: none">
    <span id='title'></span><br />
</div>

<script>

var width = 800,
    height = 600;
var svg = d3.select("body").append("svg")
    // .attr("class", "svgback")
    .attr("width", width)
    .attr("height", height);

var projection = d3.geo.mercator()
    .center([121,24])
    .scale(6000);

var path = d3.geo.path()
    .projection(projection);

// load and display the World
d3.json("taiwan.json", function(error, topology) {
    var g = svg.append("g");
    
    // load and display the cities
    d3.csv("cities.csv", function(error, data) {
        g.selectAll("circle")
           .data(data)
           .enter()
           .append("circle")
           .attr("cx", function(d) {
                   return projection([d.lon, d.lat])[0];
           })
           .attr("cy", function(d) {
                   return projection([d.lon, d.lat])[1];
           })
           .attr("r", 5)
           .style("fill", "red");
    });
    
    // 縣市/行政區界線
    d3.select("svg").append("path").datum(
            topojson.mesh(topology,
                    topology.objects["County_MOI_1041215"], function(a,
                            b) {
                        return a !== b;
                    })).attr("d", path).attr("class","subunit-boundary");
    
    d3.select("g").selectAll("path")
          .data(topojson.feature(topology, topology.objects.County_MOI_1041215).features)
          .enter()
          .append("path")
          .attr("d", path)
          .attr({
                d : path,
                name : function(d) {
                    return d.properties["C_Name"];
                },
                fill : '#55AA00'
        });
    
    // 顯示滑鼠所指向的縣市/行政區
    d3.select("svg").selectAll("path").on("mouseenter", function() {
        // console.log(e);
        fill = $(this).attr("fill");
        $(this).attr("fill", '#00DD77');
    
        $('#title').html($(this).attr("name"));
        $('#panel').css({
            "height" : "20px",
            "width" : "60px"
        });
    }).on("mouseout", function() {
        $(this).attr("fill", fill);
    });
    
    // panel 隨滑鼠移動
    $("path").mouseover(function(e) {
        if($('#panel').is(':visible')){
            $('#panel').css({
                'top' : e.pageY,
                'left' : e.pageX
            });
        } else {
            $('#panel').fadeIn('slow').fadeOut();
        }
    });

});

setInterval(function() {
    d3.csv("cities2.csv", function(error, data) {
        svg.select("g").selectAll("circle").remove();
        
        // new data joins old elements 'circle'
        var update = svg.append("g").selectAll("circle")
            .data(data)
            .enter()
            .append("circle")
            .attr("cx", function(d) {
                return projection([d.lon, d.lat])[0];
            })
            .attr("cy", function(d) {
                    return projection([d.lon, d.lat])[1];
            })
            .attr("r", 5)
            .style("fill", "red");
    });
}, 2000);
</script>

</body>
</html>

換成世界地圖

topojson/examples 下載 world-110m.json

projection 的地方要改成以 [0,5] 為中心,至於旋轉的原因,是為了搭配原始資料的關係,這樣才能正確地用經緯度標記城市的點。

var projection = d3.geo.mercator()
    .center([0,5])
    .rotate([-180,0]);

把 taiwan.json 改成 world-110m.json。

// load and display the World
d3.json("world-110m.json", function(error, topology) {
    var g = svg.append("g");

因為 world-110m.json 裡面的 objects 的名稱為 countries,所以把 CountyMOI1041215 改成 countries。

// 縣市/行政區界線
    d3.select("svg").append("path").datum(
            topojson.mesh(topology,
                    topology.objects["countries"], function(a,
                            b) {
                        return a !== b;
                    })).attr("d", path).attr("class","subunit-boundary");
    
    d3.select("g").selectAll("path")
          .data(topojson.feature(topology, topology.objects.countries).features)
          .enter()
          .append("path")
          .attr("d", path)
          .attr({
                d : path,
                name : function(d) {
                    return d.properties["name"];
                },
                fill : '#55AA00'
        });

調整一小部分就可以換成世界地圖

Reference

視覺化實戰 - D3.js 地理區塊視覺化

Taiwan Disaster in Real Time Display

抄程式學 d3.js

Let’s Make a Map

【 D3.js 入門系列 — 10 】 地圖的製作

【 地圖系列 】 世界地圖和主要國家的 JSON 文件

Try D3 Now

Spherical Mercator

Center a map in d3 given a geoJSON object

DavaViz for Everyone: Responsive Maps With D3