JSDBでデータベースを触る2007年05月09日 02時58分10秒

このままじゃ使いづらいってば。

本家のCookbookを見て、ちょっと試してみたところ、個人的にちょっと扱いづらかったのでいきなりTableクラスとRecordクラスを拡張することから始めてみた。

使いづらかった部分というのは、

  • Tableクラスで列情報にアクセスするのにtitleメソッド(カラム名の取得)、typeメソッド(型情報の取得)と分かれている
  • RecordクラスのコンストラクタがStringベースでちょっと面倒だった
といったあたりで、また行の抽出でループ処理が必須になるのでEnumerableにしてみることにした。以下はその拡張コード。prototype.jsが必要。
// Tableクラスの拡張
Object.extend( Table.prototype, {
	_each : function(iterator) {
		for(var i = 1; i <= this.count; i++) {
			iterator( this.getRow( i ) );
		}
	},
	// カラム名を配列で取得する
	colNames : function() {
		return this.getColumnInfo().map( function( col ) { return col.name; } );
	},
	// カラム情報の配列を取得する。orderは列位置、nameはカラム名、typeは型情報
	getColumnInfo : function() {
		var result = [];
		for(var i = 1; i <= this.colCount; i++) {
			result.push( {
				order : i,
				name : this.title( i ),
				type : this.type( i - 1 ).toArray().join("")
			} );
		}
		return result;
	}
} );
Object.extend( Table.prototype, Enumerable );

// Recordクラスの拡張
Object.extend( Record.prototype, {
	// 名前:値形式の単純なオブジェクトを返す
	toHash : function() {
		var result = {};
		for(var i = 0; i < this.count; i++) {
			result[ this.name( i ) ] = this.value( i );
		}
		return result;
	}
} );

// オブジェクトをパラメータにとってRecordを作成するファクトリメソッド
Record.fromHash = function(hash) {
	var src = $H( hash ).map( function(entry) {
		return [ entry.key, "=", entry.value == null ? "NULL" : entry.value ].join("");
	} ).join(",");
	return new Record( src );
}

じゃあ使ってみるか

このような拡張をした上で、データベースへアクセスしてみる。ここではmdbを使用してみた。

適当なmdbファイルを「test_db」としてODBCデータソース(ユーザDSNまたはシステムDSN)に登録しておいて、以下のようなコードでテーブルの作成、行の挿入、行の取得を行う。

js>var odbc = new ODBC("DSN=test_db")
js>odbc.exec("create table t_test ( id integer primary key, name text )")
true
js>var updater = new Table("odbc://test_db/t_test");
js>updater.add( Record.fromHash( { id : 1, name : "test 1" } ) )
-1
js>updater.add( Record.fromHash( { id : 2, name : "test 2" } ) )
-1
js>var table = odbc.query( "select * from t_test" );
js>table.each( function(record) {
2:      writeln( $H( record.toHash() ).inspect() )
3: } )
#<Hash:{'id': '1', 'name': 'test 1'}>
#<Hash:{'id': '2', 'name': 'test 2'}>

js>table.close()
true
js>updater.close()
true
js>odbc.close()
true
js>

まずは、「new ODBC()」でODBCオブジェクトを初期化する。この例ではmdbを使用したためユーザID・パスワードはないが、通常のDBMSの場合は「UID=ユーザID;PWD=パスワード」を追加する必要がある。

次のODBC#execでテスト用のテーブルを作成している。insert/delete/updateの場合もexecメソッドで実行する。

次の変数「updater」は今作成したテーブルの更新に使用するTableオブジェクト。この後に説明するODBC#queryメソッドの戻りもTableオブジェクトだが、それは読み取り専用らしく、更新する場合はこの例のようにURIをパラメータにしてTableをnewする必要がある。

その後はTable#addメソッドで行を新規に追加しているが、追加する行の初期化には、このエントリで拡張したRecrd::fromHashメソッドを使用している。このような拡張をしない場合は、
updater.add( new Record( "id=1,name=test 1" ) );
のようになる。

次に、ODBC#queryを使用してupdaterとは別にTableを取得している。この「table」は読み取り専用でaddメソッドは失敗する。

取得したtableをeachで列挙し、Record#toHashをHashにした上でinspectしてダンプしている。

最後に使用したTableとODBCをcloseして終了。

問題がちらほら

上記のサンプル程度では特に問題がないが、とりあえず目に付いた問題点をば。

  • Table#typeの戻り値の型がドキュメントと違う。type(1)のように引数を1つだけの場合はStringが帰ってくるはずだが実際は要素が1つのArrayが帰ってくる。
  • Table#typeで型情報は取得できるが、Record#valueで実際に取得する値は常にString
  • Table#addでレコードを追加すると、日本語のデータが化ける。insertクエリを使用した場合は化けない
  • Table#close、ODBC#closeを実行しても実際は接続が残っているぽい。mdbやSQLiteはjsdbを終了するまでファイル自体がロックされている
といった感じ。まあ、Table#typeはたいした話ではないが、日本語のデータが通らないとなるとこのオブジェクトモデルを利用したデータアクセスはほぼ使えないと思うのだが。

オブジェクトモデルが微妙に使いづらかったり、同じ情報を示すプロパティが複数あったり、あるオブジェクトの生成方法が複数あったり、設計も実装もちょっと中途半端な感じがする。特にODBC#queryで生成したTableでデータを更新できないのはなんだかなぁと思う。

おまけで、TableにRecordを生成するファクトリメソッド(Table#createRow、とかね)がほしかったなぁ。そのほうが新しいレコード挿入するのに勝手がよいんだけど。

IEのコンテキストメニュー拡張2007年05月09日 20時15分34秒

なんとなく試してみたくなったり

普段GreatNewsというRSSリーダーを使っている。動作は軽快だしあまり不満はないのだが、内蔵のタブブラウザの機能がおまけ程度の機能しかないのでちょっと不満に思っていた。

んで、いままではリンクURLをコピーしてFirefoxに貼り付けていたのだが、読みたい記事がたまってくるとそれも面倒になってきた。要するに「Firefoxで開く」というメニューが欲しくなったのだ。

たぶんその手の右クリック拡張あるだろうなーと思って探していたら「そんなん、自分で作れ。」とばかりにR-Click! Union 拡張メニュースクリプトの作り方が見つかったので自作することに。

簡単じゃん、と思ったら

具体的な方法は先のリンク先を見てもらえばわかるので割愛するが、あとはメニューから呼び出されたときに

  • 右クリックされたリンクを取得し、
  • WScript.Shell.Run( url )で蹴っ飛ばせば
  • 標準のブラウザで開けるので以下のようなhtmlを書いてみた。withブロックで囲ってるのは単に好みの問題なのでご容赦を。
    <-- もう消しちゃったスクリプトだから動くかわかんないや。 たぶん平気だけど。-->
    <script language="JScript">
    with( external.menuArguments ) {
    	new ActiveXObject("WScript.Shell").Run( event.srcElement.href );
    }
    </script>
    
    メニューはリンク上での右クリック時のみに設定してあるため、このように非常にシンプルだ。(実はそうでもなかったが)

    ところが考えてみれば当たり前で、このスクリプトではセキュリティ警告がでてしまう。はて。

    工エェ(ry、VBSならいいの??

    他にレジストリ登録されている拡張メニューを見たところ、昔インストールした「紙 2001」(紙copiの前身)の取り込みメニューが登録されており、そちらを参考にさせてもらったところVBScriptでCreateObjectしていた。

    半信半疑で先のスクリプトをVBSで書き直そうと思ったが、ずいぶんとVBS書いてなかったのと、ものすげー冗長になるのが目に見えていたのでWScript.Shellを取得する部分のみおそるおそるVBSに置き換えた次のようなスクリプトを書いてみた。

    <-- もう消しちゃったスクリプトだから動くかわかんないや。 たぶん平気だけど。-->
    <scritp language="VBScript">
    Function GetShellObject()
    	Set GetShellObject = CreateObject("WScript.Shell")
    End Function
    </script>
    <script language="JScript">
    with( external.menuArguments ) {
    	GetShellObject().Run( event.srcElement.href );
    }
    </script>
    
    うまくいくじゃん!!...なんだか納得いかんけど。

    ひょんなことから

    まあ不恰好だがよいか、と思ってちょろちょろとテストしていたら、imgなリンクの時にはメニューが表示されないことに気がついた。場当たり的にレジストリのcontextsの値に画像時のフラグも加えたが、今度はevent.srcElementがimgになるので当然リンク先が取得できない。ああ、探索する必要があるのね、ふぅ。

    で、再帰で要素をバブリング探索するように書き換えてついでになにを思ったか未練たらしくVBS廃止→new ActiveXの復活をして以下のようなコードにした。

    <script language="JavaScript">
    with( external.menuArguments ) {
    	( function(ele) {
    		if( /^a$/i.test( ele.tagName ) ) {
    			new ActiveXObject("WScript.Shell").Run( ele.href );
    		} else {
    			if( ele.parentElement )
    				arguments.callee( ele.parentElement );
    		}
    	} )( event.srcElement )
    }
    </script>
    
    なぜか警告がでなくなった。はれ?

    それでいいのか?俺が悪いのか?

    警告でないじゃん、と思ったのはつかの間、これimgなリンクだと大丈夫で、普通のリンクだと怒られるという不可思議な現象になっていた。え、ひょっとしてコールスタックの数で挙動に違いが?本当か??

    またもや疑問をいだきつつ、コールスタックを稼ぐためにnew ActiveXObjectのみ別関数にしたところ、普通のリンクでも警告が出なくなった。

    <script language="JavaScript">
    function getShell() {
    	return new ActiveXObject("WScript.Shell");
    }
    
    with( external.menuArguments ) {
    	( function(ele) {
    		if( /^a$/i.test( ele.tagName ) ) {
    			getShell().Run( ele.href );
    		} else {
    			if( ele.parentElement )
    				arguments.callee( ele.parentElement );
    		}
    	} )( event.srcElement )
    }
    </script>
    
    本当にそれでいいのか?

    まぁ、ともあれ。

    セキュリティホールっぽく感じたのでスタック数を指定してWScript.Shellをnewするスクリプトを貼り付けたHTMLをIEに読み込ませたところ、スタック量に関係なくセキュリティ警告が表示された。あ~、びっくりした。

    ともあれ、これでGreatNews環境がすこしだけ快適になったのでよしとしよう。

コールスタックを重ねてみよう2007年05月09日 20時35分34秒

どこまで平気なのか!?

先のエントリで、コールスタック数によって警告されるか否かを検証するために、次のようなスクリプトを書いた。

( function(nest) {
	if( --nest <= 0 ) {
		alert( "completed" );
	} else {
		arguments.callee( nest );
	}
} )( 100/* ここでスタック数を調整 */ );
で、ちょろちょろパラメータ変えながら試してたら、やっぱり気になるじゃないですか。「どこまで深くして大丈夫なのか」が。

で、試してみました

手軽に実行できるよう、次のようなブックマークレット形式にしてごにょごにょやってみた。

javascript:(function(nest){if(--nest<=0) {alert("completed");}else{arguments.callee(nest);}})( 100 );
で、いくつかのブラウザでの実行結果は次の通り。
  • IE7 → 1757
  • Firefox(1.5.0.11、2.0.0.3) → 1000
  • Opera(9.20、8.54) → 3332
Operaはちょっと挙動が面白く、3332を超えても最後のalertは実行される。が、JavaScriptコンソールを確認するとstack overflowが記録されていた。

知っていたからといって別に役立つわけではないが、なんかの目安になるかな。