appengineのSearch Service(full-text search) はまだexperimentalですが非常に魅力的な機能です。
正式リリースされればデータの種類によってはDatastoreの代わりに使用することもできそう。最大の特徴は検索の柔軟性にあるでしょうか。
ただし使ってみていくつか不思議な仕様に気づいたので注意が必要。簡単にまとめてみます。
DATE: a date with no time component とドキュメントにあるようにあくまで日付だけしか持てないようです。
timestampを管理したい場合などはちょっと工夫が必要ですね。
自分はNUMBER型を使用して、特定日時(例えば2010-01-01 00:00:00)からの秒数で保持するようにしました。
※NUMBER型の範囲は -2,147,483,647 ~ 2,147,483,647
eclipseの環境でtime部分をクリアしたDateをこのフィールドに渡しても、= のクエリーでヒットしません。
例えば今日が 2013-04-24 だとして、 new Date() をセットすると、date_field = 2013-04-24 でも date_field >= 2013-04-24 でもヒットしません。
production環境ではtime部分をクリアしてもしなくても = でヒットします。
addFieldを繰り返して同じフィールド名に異なる値を追加できるようです。
Document.newBuilder()
.addField(Field.newBuilder().setName("text1").setText("hoge")
.addField(Field.newBuilder().setName("text1").setText("fuga")
.build();
HTML型も可能でしたがNUMBER型はExceptionが発生しました。
Numeric operators only match against numeric and date fields. と書いてありますので当然かもしれませんが、TEXT型に = を指定したクエリーを実行しても完全一致にはなりませんでした。
例えば、text_field = hoge というクエリーを実行した場合に
- “hoge_2” はヒットしません。
- “this is hoge” はヒットします。
- “@hoge” はヒットします。
という結果でした。
さらにSDKでは全然結果が異なりました。。(これもバグ??)
完全一致が使えると(全文検索以外に)使い途が広がるのですが、文字種を限定するなど工夫が必要そうです。
とりあえず気づいたのはこんなところですがまだまだ落とし穴があるかもしれません。
App Engine Search (Full text search) の不思議な仕様 はコメントを受け付けていません
ソースコードは
こちら に公開しています。
データストアのRead/Writeの無料課金枠が結構シビアなのでもはやキャッシュなしではやっていけないと思ったのがこれを作るきっかけでした。(速度的にはデータストアの呼び出しがそんなに遅いとは感じないのであくまで課金対策が主眼です。)
既存のコードにできるだけ手を入れないで実現したかったのでApiProxyを使ってdatastoreのAPI呼び出しをhookし、protocol bufferのrequest,responseをそのままキャシュしてはどうかと思いつきました。
キャッシュ対象とするのはQuery(RunQueryメソッド)でGetはもともと安いので対象外としました。RunQueryメソッドをhookしてキャッシュにデータが存在すればdatastore APIは呼び出さずにキャッシュしたレスポンスを返す、キャッシュにデータが存在しなければそのままAPI呼び出しをしてresponseをキャッシュする、というしくみになっています。
同一カインドのデータがPutまたはDeleteされた場合はキャッシュを無効にするようにカインド毎のResetDateを持って管理しています。
WriteよりReadのほうが圧倒的に多いシステムではキャッシュヒット率が高くなるのでこのしくみは有効かと思います。
逆に同一カインドのエンティティが頻繁に更新されるようなシステムではあまり効果がないかもしれません。
- commons-logging-1.1
- commons-lang-2.4
- gdata-core-1.0
必要なjarファイルを追加して、web.xmlのfilterChain先頭に以下のフィルター設定を追加するだけです。
<filter>
<filter-name>CacheContextFilter</filter-name>
<filter-class>jp.honestyworks.pbcache.ContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CacheContextFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
- App EngineのMemcache Quota Limit を超えるサイズのデータも問題なくキャッシュされます。(適切なサイズにchunkされて保存します。)
- memcacheだけでなくThreadLocalなコンテキストにもキャッシュを保持します。(Request毎にクリアされます。)
- 上記ローカルキャッシュは使わない設定にすることもできます。
- Production環境でしか有効になりません。
- datastore APIのasyncCallはsyncCallにデグレードします。asyncCallの恩恵を受けているシステムでは使用しないほうが良いかと思います。
テストをするにあたり、slim3に組み込まれているktrwjrを使ってみましたがすごく便利!これは素晴らしいです。
オープンソースとしてありますので自己責任でご自由にお使いください。
使っていただけた方は些細な事でもフィードバックいただけると嬉しいです。protocol buffer はあまり詳しくないので探り探りの実装になってます。
フィードバックは私のtwitterまでお寄せください。 (@miztaka)
App Engine Java用データストアの透過キャッシュを作りました はコメントを受け付けていません
「楽観的排他制御」という言葉を初めて目にしたのはJavaフレームワークSeasar2のS2Daoにおいてでした。
それまでの開発ではSELECT FOR UPDATEを多用していた気がしますが、以来ほぼすべてのプロジェクトで「楽観的排他制御」のパターンを用いることにしました。統一的でシンプルな仕様だからです。
以下のようなしくみを言います。
- 全てのテーブルには version列(int)またはtimestamp列(timestamp)を用意する。(version列のほうがよいと思います。)
- レコードの更新をするときに version列もwhere句に含め、set句にはversion列をインクリメントする。(timestampの場合はtimestampをセット。)
- update件数が0件だったら例外を投げる。
- 上記動作をDBアクセスライブラリ(ORマッパー)が透過的に取り仕切る。(開発者は意識しない。)
Teeple2 でこれを実現する方法を解説します。
Teeple2のDBアクセスライブラリ Teeple_ActiveRecordでは、insertおよびupdateの実行前に teeple_activerecord_before_insert, teeple_activerecord_before_update という関数の存在を調べ、存在した場合はこれらを実行した後に insert(update)を実行するようになっています。
このhook関数にはエンティティオブジェクト自身が引数として渡ってくるので、ここでversion列をインクリメントしたり条件を追加することによって楽観的排他制御が実現できるわけです。
例えば以下のようなコードをuser.inc.phpに追加します。
function teeple_activerecord_before_update($obj) {
if (property_exists(get_class($obj), 'version') === TRUE) {
// 楽観的排他制御
$obj->setConstraint('version', $obj->version);
$obj->version += 1;
}
return;
}
Teeple2で楽観的排他制御を実現するには はコメントを受け付けていません
appengineでJSON-RPCサーバーのようなものを作りたいときなど ServletRequest#getInputStream()で取得できるInputStreamを使いたい場合があります。ところがslim3のcontrollerでこれをやろうとするとInputStreamのIllegalStateExceptionが発生します。
Jettyや大概のサーブレットサーバーは getInputStreamとgetParameter(s)を同時には使えないようです。
JSON-RPCならslim3のcontrollerは使う必要ないじゃんといえばそれまでなのですができれば慣れているもので全てやってしまいたいというのも事実。そこで以下のようなworkaroundで回避します。
- StreamFilterを作って、そこでServletRequestをWrapperクラスに置き換える (一番最初にこのFilterが動くようにする)
- RequestWrapperでは getInputStreamをoverrideし、Streamを再利用可能にする
なお、このworkaroundは http://d.hatena.ne.jp/machi_pon/20090120/1232420325 にて紹介されているやり方とほぼ同じです。(ナイスポストありがとうございました!)
public class StreamFilter implements Filter {
public void destroy() {
// TODO Auto-generated method stub
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
request = new BufferedServletRequestWrapper( req );
chain.doFilter(request, response);
}
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
}
public class BufferedServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] buffer;
public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
super( request );
InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buff[] = new byte[ 1024 ];
int read;
while( ( read = is.read( buff ) ) > 0 ) {
baos.write( buff, 0, read );
}
this.buffer = baos.toByteArray();
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new BufferedServletInputStream( this.buffer );
}
}
public class BufferedServletInputStream extends ServletInputStream {
private ByteArrayInputStream inputStream;
public BufferedServletInputStream(byte[] buffer) {
this.inputStream = new ByteArrayInputStream( buffer );
}
@Override
public int available() throws IOException {
return inputStream.available();
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return inputStream.read( b, off, len );
}
}
slim3のcontrollerでServletInputStreamを使いたいとき はコメントを受け付けていません