dashmarkを作りながらいろいろとハマっていること

[-]:dashmarkはWebKit SQL API(HTML5 client-side database storage)とCSS Animationにベタベタに依存したアプリケーションですが、似たようなことをやっている人が極端に少ないことと、私自身がJavaScriptにもSQLにも、プログラミングそのものにも経験と知識が少ないためにしょうもないところでハマってしまう。

ソースは[-]:dashmarkの先にある[-]:dashmark pagedevelopment versionに上がっているものがすべてなので、興味がある人はぜひ見てほしい……とても恥ずかしいけど。

今までにハマってしまったところを備忘録として記載しておきます。

非同期処理

WebKit SQLの処理は非同期に行われるため、検索などの結果を利用する処理にちょっとした工夫が必要、っていうかSQLの結果はいつ帰ってくるかわからないし複数同時に走っているような状態を想定しないと、存在しないDOMを先に消しにいったりしてちょっと面倒なことになる。

ただ、非同期に処理が行われることを、私は楽しんでいる。[-]:dashmarkのインクリメンタルサーチなんて非同期処理でないとインタラクションが悪くて使い物にならなくなるところだったけど、そもそも[-]:dashmarkはSQL APIとCSS Animationという非同期処理に依存するつもりで作っているので、楽しみながら作ることができている。

バグか仕様か……

例えば、以下のような処理は期待する結果が得られない。私のJavaScript理解が大いに間違っている可能性もあるけれど。

TABLE Aからtextを持ってきてTABLE Bの同じuidのある行に移植するようなとき、はじめは下のように書いてしまったが、すべてのrow[‘text’]が同じ値になってしまう。

SQLのキューに積む段階で、var rowの中身がすべて同じものになってしまっているらしい。私の理解が間違っていると信じたい。ひょっとしたら、SQL statementの仕様かもしれない。

db.transaction( function(tx){
tx.executeSql( "SELECT uid, text FROM [TABLE A]",[],
function(tx, result){
for (var i = 0; i < result.rows.length; ++i){
var row = result.rows.item(i);
tx.executeSql( "UPDATE [TABLE B] SET text = ? where uid = ?",[ row['text'], i])
}
}),
function(tx, error){})
)
});

結局、この処理は文字列で固めて別の関数に処理を渡すことでとりあえず回避しているのだけど、本来不要な関数が増えてしまっている。

db.transaction( function(tx){
tx.executeSql( "SELECT uid, text FROM [TABLE A]",[],
function(tx, result){
for (var i = 0; i < result.rows.length; ++i){
var row = result.rows.item(i);
var text_string = row['text'];
updateText(text_string);
}
}),
function(tx, error){})
)
});

某氏より、以下のようにしてみては?とアドバイスを頂いた。だが、これでもrowStringがすべてresult.rows.item(result.rows.length)[‘text’]、つまり最後のiで取得された値になってしまう。困ったものだ。

db.transaction( function(tx){
tx.executeSql( "SELECT uid, text FROM [TABLE A]",[],
function(tx, result){
for (var i = 0; i < result.rows.length; ++i){
var row = result.rows.item(i);
 var rowString = row['text'];
tx.executeSql( "UPDATE [TABLE B] SET text = ? where uid = ?",[ rowString, i])
}
}),
function(tx, error){})
)
});

人間が読まない通しナンバーのid

SQL入門みたいなチュートリアルを読んでいると必ず登場するのがPRIMARY NUMBERを使ったシリアルナンバー処理。これは悪。伝票番号のように人間が後から読まないidなら、通しナンバーは絶対につけてはいけない。私も初期のバージョンで通しナンバーをつけてしまい、大きな失敗をしてしまった。

通しナンバーをidに使うデメリットはいくつかあるけれど、私がぶつかったのは下の二つ。

  1. 欠番の登場
  2. 処理の衝突

データを削除すると必ず登場する欠番。欠番が出てきた時点で「通し」ナンバーであるメリットのほとんどが失われ、ただの「数字で構成される文字列」に成り下がる。

もう一つは上でも挙げた非同期処理にまつわる衝突の問題で、こちらは深刻だ。

新規idの取得と、データの挿入が一文のクエリで表現できない処理がある場合、idの取得とデータ挿入の間に他のSQLが実行される可能性がある。それが「最大のidを持つrowからデータを抽出する」ような処理だった場合、抜き出すべき列にはnullしか入っていない。そりゃ、nullが帰ってきたら再帰的に有為なデータが採れるようにする処理を書くのは可能かもしれないけど不毛だ。

WebKit SQL APIは私が使う範囲でまともに処理が動いている間はデータベースのロックが発生しないけれど、nullの参照やresultを拾って行う処理が重かったりするとロックしてしまう。一度ロックしたデータベースはいつ復帰するのかわからないので、衝突しない、しにくい設計にしておかないと、扱うデータが多くなってきたときにハマるような気がする。

というわけで、なんちゃって関数ではあるけれどguidでidを振って、前回のデータベースのバージョンアップの際に通しナンバーのidを捨てることができた。

もしも、何らかの理由で通しナンバーが取得したければ、以下のようにして取得できる_ROWID_とviewで結合すればいい。

db.transaction( function(tx){
tx.executeSql(
"SELECT count() FROM PRIMARY_SCOPE LIMIT 1",[],
function(tx, result){
"SELECT _ROWID_ FROM [TABLE NAME]",[],
function(tx, result){
for(var i = 0; i < result.rows.length; ++i){
var serial_text = result.rows.item(i)['_ROWID_'];
}
}),
function(tx, error){})
)
});

まだまだたくさん

ほとんどは私の理解が足りないために躓いているのだろうけれど、気付いたことがあったらまた書きます。

10件のコメント

  1. select を送ってる tx.executeSQL の中で、update を送ってる tx.executeSQL があることにそこはかとない不安を感じるのですが。一つのトランザクション(tx)で、実行途中の select の executeSQL に加えて、update って送って大丈夫なのですか? なんか、不安です。トランザクションそのものは入れ子にできないのですかね??

  2. tx.executeSql()の第三引数は、resultが出た「後で」実行される関数を置きます。ですので、selectが終わってない訳ではなくて、selectのキューが実行されてresultが出た「後」に実行されるのでupdateを置いても大丈夫、のはずです。

  3. あ、でも、トランザクションの入れ子は試したことがなかったです。ちょっと試してみます。WebKitのソースを追うのがいいのでしょうけど、きちんと読めるほどコード力が足りてません。

  4. いま、ちと http://www.whatwg.org の仕様を見てみましたが、「4.12.6 Processing model」を見る限りですと、SQL のメッセージはキューイングされます。で、callback の処理が「全て終わってから」次の行を実行します。「同じ値になってしまう」の「同じ値」とは、selectでとられた最後の値じゃないですか? callback の中で executeSql よんでも、即時では実行されずキューにたまります。bind変数 (SQL文の?のことです)を使っていますが、その時に記憶されるのは、恐らく「row」という変数であり、「rowの変数の値」じゃないです。(bind変数はそういうものです)、SQL文とbind変数のみが積まれていき、bind変数の値が積まれていないので、実行時の row の値が、積まれたupdateのSQL文を実行するときの row の中身で実行されるのじゃないかと思います。

    db.transaction( function(tx){
    tx.executeSql( ”SELECT uid, text FROM [TABLE A]”,[],
    function(tx, result){
    var rowstexts = new Array( result.rows.length );
    for (var i = 0; i < result.rows.length; ++i){
    var temprow = result.rows.item(i);
    rowtexts[i] = temprow[i][’text’];
    tx.executeSql( ”UPDATE [TABLE B] SET text = ? where uid = ?”,[ rowtexts[i], i])
    }
    }),
    function(tx, error){})
    )
    });

    ならうまくいくんじゃないかなぁと。ポイントは配列を使って、bind 変数を「ずらす」ことです。

  5. あー、でもrowstext の生存期間が不明。tx.executeSql の前で array を宣言したほうがいいかもしれないです。

  6. shiroさん、ありがとうございます。お察しの通り最後にselectで取得された値がbindされています。配列でずらしてあげると、うまく個別の値を取り出すことができました。これは、仕様、ですね。

  7. bind変数というのはそういうもの、ですので、仕様といえば仕様ですね。
    変数に格納されるものが変数の型によって変わるCみたいな言語の場合、「その変数」(を指し示すアドレス)だけを覚えて読み取るというbind変数の仕組みは理解できる仕組みですが、オブジェクト指向言語としてみれば、そのオブジェクトを持ってりゃいいわけで、仕組みとしてはかなりダサイと感じてます。

  8. SQLやOpenGLのようなC由来モジュールを扱うようになってくると、このような「仕様」がそこかしこに顔を出してくるのでしょうね。

  9. 通しナンバーですが、人が読むIDが別にあるならそちらをキーにしたほうが見通しが良くなるのは確かですね。ただ、ソースを詳しく読んでないので的外れなことを言っているかもしれませんが、使い方が違うような気が。IDENTITY属性による自動採番はリレーション用のユニーク値をわざわざ自分で作らなくて良いので便利です。付けた番号はDBMSから(定義と同じ型で)取り出せますし同期をとるための仕組みもあります。欠番はどうしようにもないのでforeachを使うなどしてidを絡めずにループを組むべば良い話です。嫌う人も多いけど採番ルールとか決めなくてもテーブル正規化をガシガシできるので結構便利ですよ。

  10. お久しぶりです。
    IDENTITYは伝票番号みたいなものを作るときには確かに便利ですね。ありがとうございます。ただ、今回はシーケンシャルなidが不要なアプリケーションですので、HTMLでもそのまま使えるidとしてguidでいかなるときにもユニークな値を使えるように作っています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

%d人のブロガーが「いいね」をつけました。