404 shin1のつぶやき ないわー Not Found
2012年9月6日木曜日
Google Pickerが便利すぎた
Google Pickerっていう、Google Docs(Google Drive)を前提としたPickerがめちゃ便利な事に気づいていませんでした。ずっと GWT 専用のコンポーネントなんだと勘違いしていたという。Pickerですが、基本的にはリソースのURLが返されます。Docsネイティブなリソースの場合にはそのアイコンやらも取得できます。
こんなカンジでDocs内の文書を選択させたり。

地図からPlaceを選択させたり。

Docsへアップロードさせて、アップロードされたファイルを選択したり。

使い方
google.picker.PickerBuilder()
をインスタンス化して、Builderを用意するaddView() addViewGroup()
で、左側に表示するカテゴリやグループ化したカテゴリを追加する- リソースが選択されてポップアップが閉じた時のコールバック関数を登録する
build()
して、setVisible(true)
する
<!DOCTYPE html> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> <meta charset="utf-8"> </head> | |
<body> | |
<div> <button id="show-docs-picker">Select Documents in Google Docs</button> </div> | |
<div> <ul id="picked"></ul> </div> | |
<script src="https://www.google.com/jsapi"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script> | |
<script type="text/javascript"> | |
(function () { | |
google.setOnLoadCallback(function () { | |
$('#show-docs-picker').on('click', showDocsPicker); | |
}); | |
google.load('picker', '1'); | |
function showDocsPicker() { | |
new google.picker.PickerBuilder() | |
.addViewGroup( | |
new google.picker.ViewGroup(google.picker.ViewId.DOCS) | |
.addView(google.picker.ViewId.DOCUMENTS) | |
.addView(google.picker.ViewId.SPREADSHEETS) | |
.addView(google.picker.ViewId.PRESENTATIONS) | |
.addView(google.picker.ViewId.FOLDERS) | |
.addView(google.picker.ViewId.FORMS) | |
.addView(google.picker.ViewId.PDFS) | |
) | |
.addView(google.picker.ViewId.MAPS) | |
.addView(google.picker.ViewId.IMAGE_SEARCH) | |
.addView(new google.picker.DocsUploadView()) | |
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED) | |
.setCallback(onPicked) | |
.build().setVisible(true); | |
} | |
function onPicked(data) { | |
if (data.action !== 'picked') return; | |
var k, v, picked, li; | |
$('#picked').empty(); | |
for (k in data.docs) { | |
picked = data[google.picker.Response.DOCUMENTS][k]; | |
li = $('<li></li>').appendTo('#picked'); | |
$('<img>', {src:picked.iconUrl}).appendTo(li); | |
$('<a></a>', {href:picked.url}).text(picked.name).appendTo(li); | |
} | |
} | |
}()); | |
</script> | |
</body> | |
</html> |
リファレンスのURL 書き忘れてたので追記: https://developers.google.com/picker/
2012年9月3日月曜日
App Engine Full-text Search API の使いどころ
今年になって Google App Engineに Full-text Search(以下FTS)という機能が追加されています。全文検索機能の事で、日本語にも対応しています。リリース前に私も appengine ja night 20 で紹介をしました。その時にも「日本語検索はおまけ、それ以外の機能が強力」という話をしたのですが、それについて少し具体例を交えて説明をしようと思います。
ログインユーザごとのアクセス権限を参照する処理
RDBでは簡単でも、データストアで苦戦することまちがいなしの要件として、次のような要件がよく出てきます。
- 企業向けアプリケーション等で、リソースへのアクセス制御をしたい
- リソースに対するアクセス許可リスト(ACL)として、ユーザIDだったり、ユーザを束ねたグループIDだったりを定義する
- ログインユーザに対して、リソースの一覧を提供する。もちろん、アクセスできるもののみが一覧表示される。
企業向けアプリケーションではまず必要になる、よくある処理だと思います。ここでは企業向けアプリケーションと書いていますが、例えばTwitterなどの、ユーザ間の関係が発生するアプリケーションでも同じような要件が出てきます。なお、ここでいうグループはディレクトリ的なものではなく、一ユーザが複数のグループに所属できるような関係を想定しています。
データストアで実装する
これをデータストアで実装しようとするとなかなか厄介ですが、大きく分けると「保存時に頑張る」「クエリ時に頑張る」の二種類の方針が考えられます。
後者の方針は早々に破綻する可能性があるため(一覧表示くらいならなんとかなるが、なんらかのフィルタがくっつくと処理に相当無理が出るし、固定の件数でページングするのも難しい)、多くの場合は前者の「保存時に頑張る」方法で次のように実現することになると思います。
- リソースが保存された時に、それにアクセスできると定義されたグループから、ユーザを展開する
- 展開したユーザ(リソースにアクセスできるユーザ)分だけ、「ユーザごとにアクセスできるリソース」を作成し、保存する。このエンティティはユーザのIDとリソースのIDを保持します。カインド Readable としましょう。
- ログインユーザがリソース一覧を表示する場合、Readableに対してクエリを発行します。
独自にインデックス用のエンティティを作るということです。この実装方法ですとアクセスの制限が広いリソース(全社員向け、とか!)が保存された場合、ものすごく大量の Readable エンティティが作成・保存されますし、それ以外に面倒な非同期処理も必要になります。例えば、グループ内のユーザが変更された場合や、ユーザが新たに発生した場合です。しかし、「クエリ時に頑張る」実装よりは、ユーザに対して良いパフォーマンスを提供できるので、保存時に頑張る、を選択することが多いのではないかと思われます。
FTSを利用する
FTSを使うとかなり簡単になります。
- リソースが保存された時に、リソースの主キーと、アクセスを許可するグループやユーザのIDを多値フィールドとしてFTSのDocumentに追加して、インデクスさせる。DocumentのIDとしては、リソースの主キーをエンコードした文字列を使用しとくと便利です(FTSにもkeysOnlyなオプションがあるのです)。アクセスを許可するグループやユーザのIDは ACL というフィールド名とします。
- 例えばログインユーザが g1 ,g2, g3 というグループに所属している u1 というユーザであれば、FTSで「ACL=u1 or ACL=g1 OR ...」とORでつなぐだけ。ソートやフィルタが必要であれば、それらのフィールドをDocumentに追加しておくと良いです。で、取得したリソースのキーを使って、データストアから必要なリソースをバッチGETすればリソース本体も取得できます。それが面倒なら、Documentの方に必要なフィールドを含めても良いでしょう(そうこうするうちにデータストアが不要になった、となる可能性もるかも)
工夫も何もありません、特にめづらしい実装にも思えませんが、FTSを日本語検索ではない目的で使用するのが強力だ、と言っていたのはこういう処理で使うことを想定していたためです。データストアだけでは実装コストもランニングコストも高くなってしまう処理が、工夫もなく簡単に実現できてしまうのです。グループのメンテナンスが発生した場合の整合性を維持する非同期処理は同じように残ってしまいますが、そこはデータストアに対するメンテナンスよりも軽くなるはずです。
これに日本語検索を加えてもいいです。今のところ、FTSを多用する場合の注意点は次のようなものがあるとおもいます。
- FTSはまだExperimentalなため、APIのRateと保存容量にLimitが存在する上、利用料金も決まっていません。これらふたつのLimitはGooglerに連絡すれば回避できますが、料金はちょっと心配です。 しかし、独自にインデックス用のエンティティを作って実装している場合はWrite Operationsもかなり大きいので、それら程度であれば、実装のコストが削減できる分FTSの方が良いですね。
- FTSはまだExperimentalなため、不具合が無いとはいいきれません。
- クエリに使える文字列は 2000文字が限界です. ひとりのユーザが数百のグループに所属…となると、それにひっかかってしまうかもしれません。それが心配な場合は、ユーザやグループのメールアドレスなど長い文字は使わず、別の短い値に変換して使うと良いでしょう。
まとめ
SDK1.7.0 から Datastore に対してもORによるフィルタが指定できるようになりましたが、その機能ですとカーソルを返せない(実際には複数クエリを並列実行してるだけなので、当たり前ですね)という問題があります。
Cloud SQLを組み合わせればFTSなしでも同じことができますが、テスト環境の手間が多少なりとも増えてしまうのがイヤな人にはデメリットです。が、このあたりは好みによりますね、素直にCloud SQLで慣れたRDBの設計・実装を行うのも実装コストを減らす選択肢のひとつです。Cloud SQLはスケールアウトしないという特徴にも注意が必要かも。一社のみで使うアプリケーションであれば良いですが、複数社に提供する業務アプリケーションであれば、ボトルネックになる可能性があります。
おまけ
肝心の日本語検索ですが、 Google検索 >>> Gmail検索 = FTS > Google Docs検索 といったかんじの性能じゃないかなーと感じました。
2012年6月21日木曜日
coffee, lessなプロジェクトをgruntで便利に
最近ようやくcoffeescriptにもLESSにも慣れてきました。で、そろそろgruntを活用しようかなと先月くらいから使ってみてますが便利です。
- *.less →(コンパイル)→ style.css →(圧縮)→
style.min.css
- *.coffee →(コンパイル)→ *.js →(連結)→ app.js →(圧縮)→
app.min.js
- *-test.coffee →(コンパイル)→ *-test.js →(連結)→
tests.js
app.min.js
,tests.js
を読み込んでqunitでテスト- index.htmlでは
app.min.js
とstyle.min.css
を読み込む
こんなかんじの事を簡単にできます。テストもcoffeescriptで書けて良いです。
現在リリースされている grunt v0.3.9 では coffee, less, sqwishに対応したタスクが無いので、カスタムタスクを grunt.js 内で登録して使用してます(pluginとして環境に登録はしていない)。
- $ grunt watch &
- lessやcoffeeファイルの編集を監視して、自動的にコンパイルして連結して圧縮してくれます。
- $ grunt clean
- 出力ファイルやフォルダを全て削除します。
- $ grunt
- lessやcoffeeファイルをコンパイルして連結して圧縮して、圧縮したjsを使ってqunitでテストしてくれます。
フォルダ構成
私の場合はほぼ全てのプロジェクトがGoogle App Engine/Javaプロジェクトで、mavenを使っているのでフォルダ構成は次のような状況です。
- src/main/coffee
- プロダクト用の coffee ファイル
- src/main/less
- less ファイル
- src/test/coffee
- テスト用の coffee ファイル
- src/test/qunit
- qunit.css, qunit.js, sinon.js, qunit.html(war/js/app.min.js, target/tests.js)
出力先となるフォルダ・ファイル。
- target/js
- プロダクト用の coffee ファイルをコンパイルしたJavaScriptファイル
- target/js.test
- テスト用の coffee ファイルをコンパイルしたJavaScriptファイル
- target/tests.js
- テスト用のcoffee ファイルをコンパイルしたJavaScriptファイルを連結したJavaScriptファイル
- war/css/style.css
- lessをコンパイルしたcssファイル
- war/css/style.min.css
- lessをコンパイルしたcssファイルを圧縮したファイル
- war/js/app.js
- プロダクト用のcoffeeファイルをコンパイルしたJavaScriptファイルを連結したJavaScriptファイル
- war/js/app.min.js
- プロダクト用のcoffeeファイルをコンパイルしたJavaScriptファイルを連結したJavaScriptファイルを圧縮したJavaScriptファイル
grunt.js
次がgruntの定義ですが、これを利用するには、nodejs, npm, phantomjs が必要で、npmでは grunt, less, coffee-script, sqwish をインストールしておく必要があります。
module.exports = function (grunt) { | |
grunt.initConfig({ | |
clean:{ | |
js:{ | |
files:[ | |
'<config:concat.src.dest>', | |
'<config:concat.tests.dest>', | |
'<config:min.dist.dest>' | |
], | |
dirs:[ | |
'<config:coffee.src.dest>', | |
'<config:coffee.tests.dest>' | |
] | |
}, | |
css: { | |
files: [ | |
'<config:less.dist.dest>', | |
'<config:sqwish.dist.dest>' | |
] | |
} | |
}, | |
less:{ | |
dist:{ | |
src:'src/main/less/customized.less', dest:'war/css/style.css' | |
} | |
}, | |
sqwish:{ | |
dist:{ | |
src:'<config:less.dist.dest>', dest:'war/css/style.min.css' | |
} | |
}, | |
coffee:{ | |
src:{ | |
dir:'src/main/coffee/', dest:'target/js/' | |
}, | |
tests:{ | |
dir:'src/test/coffee/', dest:'target/js.test/' | |
} | |
}, | |
concat:{ | |
src:{ | |
src:[ // coffeeファイルを追加する場合はここにも追加する。 | |
'target/js/lib1.js', | |
'target/js/lib2.js', | |
'target/js/main.js' | |
], | |
dest:'war/js/app.js' | |
}, | |
tests:{ | |
src:'target/js.test/*.js', dest:'target/tests.js' | |
} | |
}, | |
min:{ | |
dist:{ | |
src:'<config:concat.src.dest>', dest:'war/js/app.min.js' | |
} | |
}, | |
qunit:{ | |
files:'src/test/qunit/*.html' | |
}, | |
watch:{ | |
coffee:{ | |
files:['src/main/coffee/**/*.coffee', 'src/test/coffee/**/*.coffee', 'src/main/less/**/*.less'], | |
tasks:'coffee concat min' | |
}, | |
less:{ | |
files:['src/main/less/**/*.less'], | |
tasks:'less sqwish' | |
} | |
} | |
}); | |
// Default task. | |
grunt.registerTask('default', 'coffee concat min less sqwish qunit'); | |
// | |
// register custom tasks and helpers. | |
// | |
var log = grunt.log; | |
var exec = require('child_process').exec; | |
grunt.registerHelper('exec', function (opts, done) { | |
var command = opts.cmd + ' ' + opts.args.join(' '); | |
exec(command, opts.opts, function (code, stdout, stderr) { | |
if (!done) return; | |
if (code === 0) { | |
done(null, stdout, code); | |
} else { | |
done(code, stderr, code); | |
} | |
}); | |
}); | |
var handleResult = function handleResult(err, stdout, code, done) { | |
if (err) { | |
log.writeln(stdout); | |
done(false); | |
} else { | |
done(true); | |
} | |
}; | |
// task: coffee | |
(function (grunt) { | |
grunt.registerHelper('coffeec', function (fromdir, dest, done) { | |
var args = { cmd:'coffee', args:[ '--compile', '--output', dest, fromdir ] }; | |
grunt.helper('exec', args, function (err, stdout, code) { | |
handleResult(err, stdout, code, done); | |
}); | |
}); | |
grunt.registerMultiTask('coffee', 'compile CoffeeScript', function () { | |
grunt.helper('coffeec', this.data.dir, this.data.dest, this.async()); | |
}); | |
}(grunt)); | |
// task: less. | |
(function (grunt) { | |
grunt.registerHelper('lessc', function (from, dest, done) { | |
var args = { cmd:'lessc', args:[ '--compress', from, dest] }; | |
grunt.helper('exec', args, function (err, stdout, code) { | |
handleResult(err, stdout, code, done); | |
}); | |
}); | |
grunt.registerMultiTask('less', 'compile less', function () { | |
grunt.helper('lessc', this.data.src, this.data.dest, this.async()); | |
}); | |
}(grunt)); | |
// task: sqwish | |
(function (grunt) { | |
grunt.registerHelper('sqwishc', function (from, dest, done) { | |
var args = { cmd:'sqwish', args:[ from, '--output', dest] }; | |
grunt.helper('exec', args, function (err, stdout, code) { | |
handleResult(err, stdout, code, done); | |
}); | |
}); | |
grunt.registerMultiTask('sqwish', 'minify css', function () { | |
grunt.helper('sqwishc', this.data.src, this.data.dest, this.async()); | |
}); | |
}(grunt)); | |
// task: clean | |
(function (grunt) { | |
grunt.registerHelper('rm', function (targets, done) { | |
for (i in targets) { | |
var target = targets[i]; | |
var args = { cmd:'rm -f', args:[ target ] }; | |
grunt.helper('exec', args, function (err, stdout, code) { | |
handleResult(err, stdout, code, done); | |
}); | |
} | |
}); | |
grunt.registerHelper('rd', function (targets, done) { | |
for (i in targets) { | |
var target = targets[i]; | |
var args = { cmd:'rm -rf', args:[ target ] }; | |
grunt.helper('exec', args, function (err, stdout, code) { | |
handleResult(err, stdout, code, done); | |
}); | |
} | |
}); | |
grunt.registerMultiTask('clean', 'compile less', function () { | |
if (this.data.files) { | |
grunt.helper('rm', this.data.files, this.async()); | |
} | |
if (this.data.dirs) { | |
grunt.helper('rd', this.data.dirs, this.async()); | |
} | |
}); | |
}(grunt)); | |
}; |
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="utf-8"/> | |
<title>Tests</title> | |
<link rel="stylesheet" href="qunit.css"/> | |
</head> | |
<body> | |
<div id="qunit"></div> | |
<script src="qunit.js"></script> | |
<script src="sinon-1.3.4.js"></script> | |
<!-- ライブラリを追加する場合はここにも追加する --> | |
<script src="../../../war/js/jquery-1.7.2.min.js"></script> | |
<script src="../../../war/js/jquery.ba-hashchange.min.js"></script> | |
<script src="../../../war/js/app.min.js"></script> | |
<script src="../../../target/tests.js"></script> | |
<script> | |
</script> | |
</body> | |
</html> |
2012年3月12日月曜日
Google API Expertが解説する Google App Engine for Java実践ガイド が発売されます
書きました。3/16(金)に発売されます。Javaの経験が無いと厳しいです。。Javaの経験があり、Google App Engine/Javaに興味がある人、実際に現場で使っている・使う予定がある人の役に立つと思いますので、興味があればポチってみてください。
目次は次のようになっています。
- 第1章 イントロダクション
- 第2章 JUnitによるテスト
- 第3章 各機能の使い方
- 第4章 アプリケーションの設計の注意点
- 第5章 課金額の節約
- 第6章 提供されるツールの使い方
1章ではHello worldレベルのサンプルを実施し、2章では1章で使ったサンプルを作り直しながらGoogle App Engine/Javaの環境での自動テストの能力と、IDEの能力を引き出す方法を説明し、Google App Engine/Java環境での開発のリズムを掴めるよう説明します。一番ボリュームがある第3章では、まずは様々なサービス(機能)を実行する仕組みを説明して、その後サンプルアプリケーションを拡張しながら次のサービスについて説明します。
- Datastore Service
- Users Service
- Memcache Service
- Task Quere Service
- Scheduled Tasks
- Mail Service
- Channel Service
- Blobstore Service/File Service
- Images Service
- Prospective Search
- XMPP Service
- URLFetch Service
- Backends
- Pull Queues & Pull Tasks
- Log Service
- Capabilities Service
- OAuth Service
- Multitenancy
- Conversion API
Google App Engine/Java環境を使った場合の自動テスト対象の広さ&速さ、開発効率の高さ。それらの支えがあることで、アプリケーションのサーバ側がどれだけラクになるか、という事が伝えたい内容です。