2010年9月10日金曜日

データベースと表形式

データベースではデータを表にして利用する場合が多いと思います。
この場合に、表計算ソフトなどのように入力したデータを枠の長さに合わせてクリップ処理したいと思う人も多いと思います。

表で入出力をするには<table>タグの中に<input>タグを入れる方法もあるのですが、以下の二つに理由でどうも私の好みに合いません(笑)
  • 上下左右に余白ができて場所を取りすぎる
  • HTML文またはJavaScript命令が長くなって読みにくくなる
<table>タグにJavaScriptのsetAttribute()でcontenteditableのスイッチを付けて直接編集できないかを試してみましたが、以下のようになって改行処理とスクロール処理が思うようにできません。

元の表が以下のような物とします。
    • 一つの英単語が長いと、その長さに合わせて枠が横に長くなります。
    • 枠に入りきらない英文または和文を入力すると、文章が改行されて枠が縦に長くなります。

    以下のCSS文を試してみましたが、思うような結果を得られませんでした。

    試したCSSの一覧。(命令にwebkitとあるのはwebkit独自の仕様で、この記事を書いた時点ではHTML5に採用されていません)

    width: 35px;
    -webkit-column-width: 35px;
    text-overflow: clip;
    white-space: nowrap;
    word-wrap: break-word
    word-wrap: normal;
    overflow: hidden;
    overflow-x: hidden;
    overflow-y: hidden;
    overflow-y: scroll;
    -webkit-user-modify: read-write-plaintext-only;
    -webkit-user-modify: read-write;

    今のところ、<table>タグの中に<input>タグを入れるか、<div> タグで四角を縦横に並べるしか方法が思い付きません。

    Safariのtransaction

    SafariのopenDatabase」の項目に引き続いて、以下のURLにあるSafariのデータベースの解説を見ていきたいと思います。

    Safari Client-Side Storage and Offline Applications Programming Guide: Introduction
    http://developer.apple.com/safari/library/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Introduction/Introduction.html

    Safariのデータベースで面倒なのが"Listing4-2 CreatingaSQLtable"にあるようなトランザクション処理の関数です。"Listing4-2 CreatingaSQLtable"にある最初のトランザクションは「SafariのopenDatabase」の項目にあるように他の関数に移し、すでにデータがある場合の処理は別にしたとして、データベースに初期データを入れる部分を抜き書きして、さらに初期データを一つだけにすると以下のようになります。

    1. function createTables()  
    2. {  
    3.  db.transaction(  
    4.   function (transaction) {  
    5.    transaction.executeSql('insert into people   
    6.     (name, shirt) VALUES ("Joe""Green");',  
    7.     [], nullDataHandler, errorHandler);  
    8.   }  
    9.  );  
    10. }  

    関数内の最初の二行で、データベースのオブジェクト db のメソッドである transaction に関数を渡しています。渡した関数の引数がオブジェクトになっていて、そのオブジェクトのメソッドである executeSql でSQL命令を発効しています。
    これだけでもチョット面倒ですが、さらに面倒なことに executeSql メソッドの引数が四つあって、その内の二つが関数で、その二つの関数がくせ者です(苦笑)

    そのくせ者の前に注意しなくてはならないのが transaction です。 db.transaction とその後の transaction は別物です。 function(transaction)transaction.executeSqltransaction は同じ変数で、その名前は transaction である必要はなく、 myTransaction などの他の変数名でも動作します。ここで同じ名前を使用しなくてはならない理由が私には思い付かず、サンプルコードとしては誤解を招きやすい解説だと思います。私は実際のコードを書く場合でも、後から調べることを考えて別名にしています。

    閑話休題、もとの「くせ者」の話に戻しましょう。この二つの関数は一方通行で、行ったまま戻ってきませんし、引数を渡すこともできません。呼び出される関数の引数は二つのオブジェクトと決まっています。マウスなどのイベントを処理するメソッド addEventListener に近い感じです。また、話しを脱線させると・・・個人的な経験を元にした推測にしか過ぎませんが、おそらくトランザクション処理をするために非同期コールを組み込む必要が出てこのような面倒なことをしているのではないか?と思っています。

    さて、 executeSql メソッドの第一引数と第二引数はC言語の printf 関数のようなイメージで使用しますが、変数が多くなると高い確率で間違います・・・つまり、私は間違いました(笑)また、初期化に限らず似たような処理を繰り返し実行することは良くあります。そこで私は「SafariのopenDatabase」の項目にあるSQL文の書き方にもう少し手を加えた方法を利用しています。一つの参考としてご紹介します。

    1. function createTables()  
    2. {  
    3.  var nameShirt = {      
    4.   "Joe""Green",  
    5.   "Mark""Blue",  
    6.   "Phil""Orange",  
    7.   "jdoe""Purple"  
    8.  }  
    9.  db.transaction(  
    10.   function (trans) {  
    11.    for(var index in nameShirt) {  
    12.     var mySQL = 'insert into people (name, shirt) VALUES ("' +  
    13.      index +  
    14.      '", "' +  
    15.      nameShirt[index] +  
    16.      '");';  
    17.     trans.executeSql(mySQL, [], nullDataHandler, errorHandler);  
    18.    }  
    19.   }  
    20.  );  
    21. }  

    上記の方法をさらに手を加えると、複数のテーブルを扱う時に便利になります。私は、object literalとArrayオブジェクトを組み合わせて、データベースの構造を宣言して、SQL文のテーブルの宣言部分を書いています。

    以上のように関数の中に関数が多重で入っており書きにくく、読みにくく、間違いやすい構文になっています。このために、"Listing 4-5 SQL insert query example"にあるような書き方は、面白いとは思いますが・・・関わりになるのは避けたいです(苦笑)

    SafariのopenDatabase

    数年前にSafariにSQLiteのデータベースがついて一時期話題になりましたが、その後も色々と改善が続いているようです。その解説が以下のURLにあります。

    Safari Client-Side Storage and Offline Applications Programming Guide: Introduction
    http://developer.apple.com/safari/library/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Introduction/Introduction.html

    ちなみに、この資料の日付は2010/01/20となっていますが、PDF版の作成日にはいくつかのバージョンがあるようです。

    この資料では"HTML 5 Offline Application Cache"や"Key-Value Storage"の解説が追加されています。

    データベースに関する解説も手が加えられています。"Relational Database Basics"はSQLの初心者を対象にデータベースの解説があり、"Using the JavaScript Database"でJavaScriptからデータベースを利用する方法が解説されています。"APPENDIX A: Database Example: A Simple Text Editor"にはサンプルコードを元に解説があります。

    解説中のサンプル コードはわかりやすい書き方になっていますが、まだ完成には時間が必要ではないかという気がします。例えば"Listing 4-1 Creating and opening a database"にあるデータベースを開く時の処理です。

    1. try {  
    2.  if (!window.openDatabase) {  
    3.   alert('not supported');  
    4.  } else {  
    5.   var shortName = 'mydatabase';  
    6.   var version = '1.0';  
    7.   var displayName = 'My Important Database';  
    8.   var maxSize = 65536; // in bytes  
    9.   var db = openDatabase(shortName, version, displayName, maxSize);  
    10.   // You should have a database instance in db.  
    11.  }  
    12. catch(e) {  
    13.  // Error handling code goes here.  
    14.  if (e == 2) {  
    15.   // Version number mismatch.  
    16.   alert("Invalid database version.");  
    17.  } else {  
    18.   alert("Unknown error "+e+".");  
    19.  }  
    20.  return;  
    21. }  
    22. alert("Database is: "+db);  

    この書き方であれば、データベースの名前やバージョンなど後から調べる時に便利そうです。

    ただし、このコードは関数内に入っていません。「最初に実行されるコード」の項目の1stの方法と同じようにすればこのままでも動作します。しかし、最初に一度しか実行しないコードをdeleteせずにメモリ上に置いておくのは無駄です。関数の中に入れて最初に実行させ最後にdeleteするのが妥当だと思います。ただし、この時には変数宣言のvar dbは式の外に置いてグローバル変数としないと他の関数でトランザクション処理ができなくなります。

    また、サンプルにある// You should have a database instance in db.というコメントは具体的に何をしようとしているのかが分かりません。データベースと同時にtaransaction()メソッドで'CREATE TABLE'を使ったテーブルの処理も考えているのかもしれませんが、この解説ではテーブルの処理は別の関数で処理しています。

    古いサンプルコードと比べるとopenDatabaseでデータベースが開けない時の処理がなくなり、try-catchcatchでエラー処理ができるようになったようです。エラー処理は"Handling Errors"と言う項目で解説されていますが、この通りには動いていないように思います。例えば上記のデータベースを開く時のcatchでの処理です。試しにversion1.12.0にすると、アラートに"Invalid database version."が表示されずに、以下のようなエラーとなりました。

    "Unknown error Error: INVALID_STATE_ERR: DOM Exception 11."

    さらに、"APPENDIX A: Database Example: A Simple Text Editor"にあるエラー処理では2の代わりに変数"INVALID_STATE_ERR"を置いていますが未定義のエラーになります。

    エラーのメッセージやエラーコードの処理が紹介されています。ただ、後半部分が何を意味するのか分かりません。
    1. function errorHandler(transaction, error)  
    2. {  
    3.  // error.message is a human-readable string.  
    4.  // error.code is a numeric error code  
    5.  alert('Oops.  Error was '+error.message+' (Code '+error.code+')');  
    6.   
    7.  // Handle errors here  
    8.  var we_think_this_error_is_fatal = true;  
    9.  if (we_think_this_error_is_fatal) return true;  
    10.  return false;  
    11. }  

    先にも書いた'CREATE TABLE'を使ったテーブルの処理は"Listing 4-2 Creating a SQL table"に書かれています。しかし、'CREATE TABLE'のSQL文は長文になりやすく、読みにくいものです。以下のような書き方の方が後から調べやすいと思います。また、複数のテーブルを扱う場合には、この方法を発展させると便利になります。

    1. db.transaction(  
    2.  function (transaction) {  
    3.   var mySQL = 'CREATE TABLE people(' +  
    4.    'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +  
    5.    'name TEXT NOT NULL DEFAULT "John Doe", ' +  
    6.    'shirt TEXT NOT NULL DEFAULT "Purple");';  
    7.   transaction.executeSql(mySQL, [], nullDataHandler, errorHandler);  
    8.  }  
    9. );  

    以上の内容をまとめると、今のところデータベースを開く関数は以下のような書き方が便利そうです。

    1. function initDB() {  
    2.  try {  
    3.   if (!window.openDatabase) {  
    4.    alert('not supported');  
    5.   } else {  
    6.    var shortName = 'mydatabase';  
    7.    var version = '1.0';  
    8.    var displayName = 'My Important Database';  
    9.    var maxSize = 65536; // in bytes  
    10.    db = openDatabase(shortName, version, displayName, maxSize);  
    11.    // You should have a database instance in db.  
    12.    db.transaction(  
    13.     function (transaction) {  
    14.      var mySQL = 'CREATE TABLE people(' +  
    15.       'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +  
    16.       'name TEXT NOT NULL DEFAULT "John Doe", ' +  
    17.       'shirt TEXT NOT NULL DEFAULT "Purple");';  
    18.      transaction.executeSql(mySQL, [], nullDataHandler, errorHandler);  
    19.     }  
    20.    );  
    21.   }  
    22.  } catch(e) {  
    23.   // Error handling code goes here.  
    24.   if (e == 2) {  
    25.    // Version number mismatch.  
    26.    alert("Invalid database version.");  
    27.   } else {  
    28.    alert("Unknown error "+e+".");  
    29.   }  
    30.   return;  
    31.  }  
    32.  // alert("Database is: "+db);  
    33.  delete initDB;  
    34. }  

    最初に実行

    最初に実行されるコード
    AppleのSafari 5.0.1とMac OS X版 Firefox 3.6.8でしか試していないのですが、JavaScriptで最初に実行されるコードを四つの方法で試してみました。

    試したコードは以下のような物です。
    1. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"  
    2.         "http://www.w3.org/TR/html4/loose.dtd">  
    3. <html lang="en">  
    4. <head>  
    5.  <meta http-equiv="content-type" content="text/html; charset=utf-8">  
    6.  <title>Untitled</title>  
    7. <script type="text/javascript" language="javascript">  
    8. <!--  
    9. // 1st  
    10. alert('without any function');  
    11.   
    12. // 2nd  
    13. function init() {  
    14.  alert('called from body');  
    15. }  
    16.   
    17. // 3rd  
    18. window.onload = function() {  
    19.  alert('called from window.onload');  
    20. }  
    21.   
    22. // 4th  
    23. function loaded() {  
    24.  alert('called from addEventListener');  
    25. }  
    26.   
    27. addEventListener('load', loaded, false);  
    28.   
    29. //-->  
    30. </script>  
    31. </head>  
    32. <body onload='init()'>  
    33. </body>  
    34. </html>  
    上記のコードをSafari 5.0.1で実行すると1st、4th、2ndの順で実行され、3rdは実行されません。

    また、上記のコードのbodyタグの引数onloadをなくすと、1st、3rd、4thの順で実行され、2ndは実行されません。

    Mac OS X版のFirefox 3.6.8で実行すると、1st、2nd、4thの順で実行され、3rdは実行されません。

    Safariと同様にbodyタグの引数onloadをなくすと、1st、3rd、4thの順で実行され、2ndは実行されません。

    3rdと4thは一般的に良く使用されるコードですが、1stと4thはAppleのエンジニアが書いたサンプルコードで見つけました。1stの方法はSafariに付属しているSQLiteのデータベースをJavaScriptから開く時に利用し、データベースのテーブルを開く時に4thの方法を利用していました。データベースを一番最初に開くという意味では1stの方法は便利かもしれませんが、メモリのフットプリント(占有領域)を小さくすると言う意味では以下の内容を考慮した方が良いと言えるでしょう。

    もったいない
    Appleの資料"Apple JavaScript Coding Guidelines"に、onload関連してメモリのフットプリント節約方法が書かれていました。

    http://developer.apple.com/safari/library/documentation/ScriptingAutomation/Conceptual/JSCodingGuide/Introduction/Introduction.html

    初期化コードなど一度しか実行しないコードは実行後にdeleteしてフットプリントを小さくすることを推薦しています。この資料では起動時の初期化コードを例として以下のようなコードを紹介しています。
    var foo = function()
    1. {   
    2.  // code that makes this function work   
    3.  delete foo;   
    4. }   
    5. window.addEventListener('load', foo, false);  
    おそらくはiPhoneなどメモリを多く積んでいない装置を想定した話しでしょうが、不用意にメモリを浪費するのは問題を引き起こしやすいですし、「もったいない」です(笑)

    Safariのデータベース

    Mac OS XにはFileMakerなどの便利で高性能なデータベースがありますが、SafariのSQLiteのようにオマケでついてくるデータベースもあります。

    SafariのSQLiteにはFileMakerのようなGUIによるわかりやすい開発環境やデバッグ環境はなく、HTMLとJavaScriptをテキストエディタで書かなくてはなりません。おそらくは、開発途上にあるデータベースと言っても良いと思います。

    Safariがデータベースとして使用しているSQLiteには以下のような利点があると思います。

    • 1.オープンソースである
    • 2.仕事として利用しても無料である
    • 3.デーモンを走らせる必要がない
    • 4.起動 終了が早い

    1.のおかげでSQLiteのデータベース ファイルを参照し、編集できるツールがいくつか公開されています。2.は個人使用だけでなく、仕事として利用しても無料であると言う意味です。3.はデータベースを利用する度に逐一デーモンを起動や終了しなくてもよいので便利です。デーモンを起動したままにするのも一案ですが、そのためにわずかでもシステム起動時間が遅くなり、メモリなどを無駄にするのは私には納得できません。ただし、トランザクション処理のために呼び出しが非同期コールのようになり、初めての人には分かりにくく使いにくかもしれません。4.はSafariの起動も速いことも相まって、チョットしたメモや参照に便利です。

    簡単な動作検証として小遣い帳を作り半年ほど利用してみました。小遣い帳のような小さなデータベースであれば十分に実用できます。ただし、SafariはWebブラウザのため、検索できても置換できません。日記(Blog)などの文章を入力するデータベースではJavaScriptやJavaなどによる検索置換のプログラムが別途に必要です。