2011/12/29

Apache POI によるエクセルファイルの出力 その1 (Export to Excel file via Apache POI, Part 1)

以前、C#でエクセル形式のファイルへの出力を検証しました。今回はJava環境での検証を行います。(「エクセルファイルのバッチ出力 その1」を併せて参照してください)。

【概要】
全体の概要は以前の検証と変更ありません。参考まで、概要図を再掲します。
Figure 1: Overall image

【環境】
サンプルはJavaで実装します。そのほかの環境は以下の通りです。
  • 使用ライブラリ: Apache POI 3.7 (XSSF)
  • Excel: Excel2010
  • データベース:Oracle Database 11gR2 (11.2.0.1)
  • DB接続:Oracle JDBC Thinドライバ

【ソース(本体部)】
ソースの例を以下に示します。データの抽出、およびエクセルへの出力を定義します。
この例では、レコードセットをnextで移動する毎に、テンプレートの行をコピーおよび挿入し、セル毎にデータを挿入します。

List 1: Main logic


package util;

import java.io.*;
import java.util.Iterator;
import java.sql.*;
import org.apache.poi.xssf.usermodel.*;
import org.apache.poi.ss.usermodel.*;


public class Ora2Excel {

  // db objects
  private Connection con;
  private Statement  stm;
  
  // poi objects
  private Workbook wbk;
  
  public Ora2Excel() throws SQLException {
    DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
  }

  public void openDb(String userId, String password, String connString) throws SQLException {
    con = DriverManager.getConnection(connString, userId, password);
    stm = con.createStatement();
  }
  
  public void closeDb() throws SQLException {
    stm.close();
    con.close();
  }
  
  public void openBook(String fileName) throws IOException {
    wbk = new XSSFWorkbook(fileName);
  }
  
  public void saveBook(String fileName) throws IOException {
    FileOutputStream out = new FileOutputStream(fileName);
    wbk.write(out);
    out.close();
  }
  
  public void closeBook() {
    wbk = null;
  }
  
  public void extract(String sheetName, String sql) throws SQLException {
    Sheet wsh = wbk.getSheet(sheetName);
    ResultSet rst = stm.executeQuery(sql);
    int colCount = rst.getMetaData().getColumnCount();
    
    // determine the start position: search "$start"
    int rowStart = 0;
    int colStart = 0;
    Iterator<Row> iRow = wsh.rowIterator();
    while (iRow.hasNext() && rowStart + colStart == 0) {
      Row currentRow = (Row) iRow.next();
      Iterator<Cell> iCol = currentRow.cellIterator();
      while (iCol.hasNext()) {
        Cell currentCell = (Cell) iCol.next();
        if (currentCell.getCellType() == Cell.CELL_TYPE_STRING  && currentCell.getStringCellValue().trim().equalsIgnoreCase("$start")) {
          rowStart = currentCell.getRowIndex();
          colStart = currentCell.getColumnIndex();
          break;
        }
      }
    }
    
    // get "template row"
    Row templateRow = wsh.getRow(rowStart);
    
    // set cell values
    int idxRow = rowStart;
    while (rst.next()) {
      wsh.shiftRows(idxRow, wsh.getLastRowNum()+1, 1);
      
      Row r = wsh.createRow(idxRow);
      for (int idxCol = templateRow.getFirstCellNum(); idxCol < templateRow.getLastCellNum(); idxCol++) {
        Cell c = r.createCell(idxCol);
        
        if (idxCol >= colStart && idxCol - colStart < colCount) {
          int idxDbCol = idxCol-colStart + 1;
          switch(rst.getMetaData().getColumnType(idxDbCol)){
          case Types.NUMERIC:
            c.setCellValue(rst.getDouble(idxDbCol));
            break;
          case Types.DATE:
              c.setCellValue(rst.getDate(idxDbCol));
            break;
          case Types.TIMESTAMP:
              c.setCellValue(rst.getDate(idxDbCol));
            break;
          default:
            c.setCellValue(rst.getString(idxDbCol));
          }
        } else if (templateRow.getCell(idxCol).getCellType() == Cell.CELL_TYPE_FORMULA){
          c.setCellFormula(templateRow.getCell(idxCol).getCellFormula());
        } else if (templateRow.getCell(idxCol).getCellType() == Cell.CELL_TYPE_NUMERIC){
          c.setCellValue(templateRow.getCell(idxCol).getNumericCellValue());
        } else if (templateRow.getCell(idxCol).getCellType() == Cell.CELL_TYPE_STRING){
          c.setCellValue(templateRow.getCell(idxCol).getStringCellValue());
        } 
        c.setCellStyle(templateRow.getCell(idxCol).getCellStyle());
        
      }
      idxRow++;
    }
    rst.close();
    
    // remove the template row.
    wsh.removeRow(templateRow);
    wsh.shiftRows(idxRow,  wsh.getLastRowNum()+1, -1);
    
    // calculate formula cells
    XSSFFormulaEvaluator.evaluateAllFormulaCells((XSSFWorkbook) wbk);
  }
}

【テスト用ソース(呼び出し部)】
上記クラスを呼び出す側の例を以下に示します。


List 2: Calling Main Logic


import util.Ora2Excel;

public class Test1 {

  public static void main(String[] args) throws Exception {
    Ora2Excel o2x = new Ora2Excel();
    
    o2x.openDb("scott", "tiger", "jdbc:oracle:thin:@windows2003srv:1521:orcl");
    
    o2x.openBook("c:\\template.xlsx");
    o2x.extract("Sheet1", "select deptno, empno, ename, job, mgr, hiredate, sal, comm from emp order by deptno, empno");
    o2x.saveBook("c:\\result.xlsx");
    o2x.closeBook();
    
    o2x.closeDb();
  }
}


【テンプレートファイル】
テンプレートファイルは、前回使用したファイルからスパークラインを除去したものを使用しています(詳細はダウンロードしたファイルの内容を参照して下さい)。
※現状ではApache POIがスパークラインに対応していないため。

データを出力する開始点に"$start"を入力します。
Figure 2: Template file

出力結果のイメージは以下の通りです。
Figure 3: Result


次回からは性能検証、および性能改善の方策を検討します。

エクセルのテンプレートファイル、結果ファイルはここからダウンロードできます。
※上記のソースはサンプルです。例外処理などは除外しています。
※上記のソースおよびファイルを使用したことによる一切の結果について、作成者は責任を負いません。

[Summary]
The sample above shows how to export data onto Excel sheet via Apache POI (former Jakarta POI).
cf. Excel file processing, Part 1


[Outline]
Overall image is shown in Figure 1 - same as the past post.


[Environment]
Source is in Java. Environment are as follows:
  • External Library: Apache POI 3.7
  • Excel: Excel2010
  • Database: Oracle Database 11gR2 (11.2.0.1)
  • DB connection: Oracle JDBC Thin Driver

[Source (main part)]
Please see List 1.  The sample class retrieves rows from database, and set the data onto workbook cell by cell.


[Test source]
Please see List 2.


[Template file]
Please see Figure 2 (Template file) and 3 (Output).  In the template, you set the report header, number format, formula and so on.  The start position, the top-left position, of data area is defined by "$start".  Please refer to the sample file for details.

2011/12/26

ファンクション・ポイントにおける言語生産性 (Function Point: Language Productivity)

前回は Oracle BI Publisher の生産性について記載しました。
今回は参考まで、一般的な開発言語に関する比較的新しい情報が以下に公開されていましたのでご紹介します。

財団法人経済調査会
http://www.zai-keicho.com/

この中の「平成21年度 ソフトウェア開発に関する調査票(受託者向け) 集計結果その1」に、いくつかの言語の生産性について集計結果が掲載されています。


内容に関しては、サンプル数が十分ではないことに起因すると考えられるばらつきが散見されますが、言語生産性以外の集計の視点も参考になり、貴重な調査結果であると思います。「集計結果その2」にも、開発要員数と生産性の関連など、承認者の説得に困った際のピンポイントの材料として参照できる内容が記載されています。

※このブログと財団法人経済調査会とは無関係です。
※ファンクション・ポイント法を推奨する目的の記事(ブログ)ではありません。あくまでも参考指標としてご認識ください。


[Summary]
No English translation on this post.  please visit Google Translate.

2011/12/20

BI Publisherの生産性 (Productivity of Oracle BI Publisher)


Oracle BI Publisherに限らず、一般的な帳票ツールの導入が開発の効率を向上させることはよほどの間違いがない限り確実といってよいと思われます。しかしながら、どの程度の効果があるかという点については明確な答えがありません。
今回はファンクション・ポイント(FP: Function Point)の算出におけるOracle BI Publisherの言語生産性について考察します。
※記事の中の値および計算過程はいかなる効果も保証しません。また、VAF(調整係数)は考慮しません。

【帳票のファンクション・ポイント】
今回の記事では帳票を中程度の外部出力(EO: External Output)として平均化し、ファンクション・ポイントを5として見積もります。詳細は以下のURLを参照してください(英語)。
http://www.devdaily.com/FunctionPoints/


【開発工数】
開発工数の見積もりは、設計書やテストの粒度をどの程度想定するかに大きく依存します。
BI Publisherを採用した場合、標準的な集計表であれば実装(コーディング)そのものは1人日以下で対応可能と想定できます。
実装以外の設計やテスト、およびそれらのレビュー、オーバーヘッドに関わる工数はプロジェクトのポリシーに従ってゼロから4人日程度を想定します。


【言語生産性の算出】
1か月の稼働は20日ですので、1人月で開発可能な帳票の数は以下の様に見積もり算出されます。
  • 実装のみを見積もるパターン: 20days / (1day + 0day) = 20reports
  • 設計やテストに厚みを想定するパターン: 20days / (1day + 4days) = 4reports

これらの値を単純に掛け合わせ、言語生産性を算出します。
  • 実装のみを見積もるパターン: 20reports * 5points = 100points
  • 設計やテストに厚みを想定するパターン: 4reports * 5points = 20points
「詰め込めば何とか開発できる」という帳票ツールの性質が数値に現れたとも言えます(あまり良いことではありませんが)。


【補足】
ファンクション・ポイントに関わる係数は担当者の練度や対象システムの性質、組織の文化に大きく依存します。本記事で算出された値はあくまでもサンプルであり、実際の見積もりにおいては係数を個別に見直す必要があります。


[Summary]
Calculate the Productivity of Oracle BI Publisher.


[Function point of a report]
Consider all report as External Output (EO) with medium difficulty: Its FP is defined as 5 points.
(*) For more details, please refer to the following URL:
http://www.devdaily.com/FunctionPoints/


[Development Volume]
The Development Volume depends on how much you contribute supplemental tasks - such as documents, review and testing.
With BI Publisher, you can develop an ordinary report in a day or less.
For the supplemental takes, around zero to four days, depend on your project policy.


[Productivity]
The numbers of reports that you can implement in a month (20 days):
  • Maximum performance (without supplemental takes ): 20days / (1day + 0day) = 20reports
  • With supplemental takes: 20days / (1day + 4days) = 4reports
The productivity is:
  • Maximum performance (without  supplemental takes): 20reports * 5points = 100points
  • With supplemental takes: 4reports * 5points = 25points


[Note]
The values above do not guarantee accuracy.  VAF is excluded.  Please find your own values.

2011/12/07

SQLによるクロス集計 (SQL: Cross tab)

SQLで小計と合計を取得する (SQL: Subtotal and Grand Total)」では小計と合計を取得するSQLについて説明しました。
今回はクロス集計を行う方法について確認します。

【sum とdecodeによる集計】
古典的な方法は以下の様にsumdecodeを組み合わせる構文です。

List 1

select prd.prod_id, prd.prod_name, sum(decode(extract(month from sls.time_id), 1, sls.quantity_sold)) as JAN, sum(decode(extract(month from sls.time_id), 2, sls.quantity_sold)) as FEB, sum(decode(extract(month from sls.time_id), 3, sls.quantity_sold)) as MAR, sum(decode(extract(month from sls.time_id), 4, sls.quantity_sold)) as APR, sum(decode(extract(month from sls.time_id), 5, sls.quantity_sold)) as MAY, sum(decode(extract(month from sls.time_id), 6, sls.quantity_sold)) as JUN, sum(decode(extract(month from sls.time_id), 7, sls.quantity_sold)) as JUL, sum(decode(extract(month from sls.time_id), 8, sls.quantity_sold)) as AUG, sum(decode(extract(month from sls.time_id), 9, sls.quantity_sold)) as SEP, sum(decode(extract(month from sls.time_id), 10, sls.quantity_sold)) as OCT, sum(decode(extract(month from sls.time_id), 11, sls.quantity_sold)) as NOV, sum(decode(extract(month from sls.time_id), 12, sls.quantity_sold)) as DEC, sum(quantity_sold) as subtotal from sales sls, products prd where sls.prod_id = prd.prod_id and sls.time_id between to_date('19980101','yyyymmdd') and to_date('19981231','yyyymmdd') group by prd.prod_id, prd.prod_name order by prd.prod_id; 

出力結果は以下の通りです。
Figure 1: sum and decode

この例では結果セットがそのままクロス集計表の形式になっているため、結果セットを扱う側のプログラム処理が簡単であるという長所があります。
この構文は横軸(列)の構成が年月の場合など、固定している場合には有用です。


【pivot】
Oracle Database 11g から、新たにpivot句が導入されました。
以下のSQLは前述のSQLと同等です。仕様によるものかは不明ですが、pivotを行う前に、対象となるデータを副問い合わせで絞り込んでおく必要があるようです。

List 2


select *
  from (select prd.prod_id,
               prd.prod_name,
               extract(month from sls.time_id) time_id,
               sls.quantity_sold from sales sls, products prd
         where sls.prod_id = prd.prod_id
           and sls.time_id between to_date('19980101','yyyymmdd') and to_date('19981231','yyyymmdd'))
 pivot (sum(quantity_sold) for time_id in (1,2,3))
 order by prod_id;

出力結果はFigure 1を参照してください。

残念ながら、現状ではpivot句のinに指定する値をサブクエリーで指定することができず、固定値として指定しなければならないため、この部分を動的SQLで組み立てる等の作り込が必要です。
この辺りには機能仕様の詰めの甘さを感じます。将来対応を期待する箇所です。


【group by cube()の使用】
group by cube()は、クロス集計結果を戻します。列の構成はselect句の指定のままですので、結果セットを受け取ったプログラム側でクロス集計表形式に展開する必要がありますが、前述の2つの方式と異なり、横軸の要素が事前に不明である場合でも、すべての組み合わせを自動で集計する点で優位です。
group by cube()は2つ以上のディメンジョンを同時に計算することができます。以下の例では、一般的な2次元の集計に、product表を結合してproduct.prod_nameを表示しています。

List 3



select prd.prod_id,
       prd.prod_name,
       sls.mm,
       sls.sum_quantity
  from products prd,
       (
        select prod_id,
               extract(month from time_id) as mm,
               sum(quantity_sold) as sum_quantity
          from sales
         where time_id between to_date('19980101','yyyymmdd') and to_date('19981231','yyyymmdd')
         group by cube (prod_id, extract(month from time_id))
       ) sls
 where prd.prod_id(+) = sls.prod_id
 order by prd.prod_id, sls.mm;

出力結果は以下の通りです。
Figure 2: group by cube()



[Summary]
There are three ways to perform cross tabulation.
[sum and decode]
The traditional way is List1. Figure 1 shows its result.

[pivot]
Oracle Database 11g releases pivod operator. With pivot, you can rewrite the previous SQL very simple. Please see List 2.
Unfortunately, you need to specify in clause with static values - in clause does not accept sub query. We hope this function will be improved in the future release.

[group by cube()]
group by cube() produces cross-tabulation values. Since it does not format its results like spread sheet, it is your application's duty to format spread sheet like output. Please see List 3 and Figure 2.

(*) for group by rollup(), please refer the past post here.