2009年9月12日土曜日

Only one inequality filter per query is supported

GAE/J上では複数のプロパティーに対して不等式フィルタが使えないということが分かった(┳◇┳)
参考:サンフランシスコの 24 時間: Geolocation App - Google App Engine - Google Code
App Engine のクエリには固有の制約があり、複数のプロパティ(経度と緯度など)に対して不等式フィルタを実行できないからです。
ノーorz。
まさにこの、”緯度経度で絞り込む”ではまっていたが、結局ずっと例外が出て・・・。
NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: Only one inequality filter per query is supported.  Encountered both latitude and longitude
当初、コードは以下のように書いていた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
List<Mutter> mutters = (List<Mutter>) query.execute(latitudeFrom, latitudeTo, longitudeFrom, longitudeTo);
pm.close();
でもこれだと、コンパイルエラーになる。理由はQueryのexecuteメソッドは引数3つまでのメソッドしかないから。

よって、以下のようにコードを変えてみた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
Object[] paramArray = { latitudeFrom, latitudeTo, longitudeFrom,longitudeTo };
List<Mutter> mutters = (List<Mutter>) query.executeWithArray(paramArray);
pm.close();
すると、上記のように「Only one inequality filter per query is supported.」が出て嵌る。 executeWithArrayメソッドの使い方がまずいのかと思い(結局これは見当違いだったが)、じかにクエリーを書く形式に変えてみる。
// SQL作成
String query = "SELECT FROM " + Mutter.class.getName()
  + " WHERE latitude >= " + latitudeFrom + " && latitude <= "
  + latitudeTo + " && longitude >= " + longitudeFrom
  + " && longitude <= " + longitudeTo + " ORDER BY date DESC";

PersistenceManager pm = PMF.get().getPersistenceManager();
List<Mutter> mutters = (List<Mutter>) pm.newQuery(query).execute();
pm.close();
でも相変わらず「Only one inequality filter per query is supported.」

で、結局冒頭に書いたとおり、それはGAE/Jでの制約だったのだ。

上記サイトで触れてあった、地図の境界ボックスを計算するオープンソースライブラリを見てみるもののPythonで書かれていたので避け、もう一方の方法Geohashを使うやり方でやってみた。

Geohashは緯度と経度の位置データから1つの文字列を生成する方法。どのような文字列が生成されるのかは、以下のサイトでテキストボックスに適当な緯度と経度を入力して確認できる。例えば「35.438399 139.361765」を入力すると神奈川県厚木市のイトーヨーカドーのGeohashが求められる。
参考:Geohash - geohash.org

GAE/Jのサイトでは、「ただし、この手法には、特に世界の特定地域において限界があります。」と書いてあったのが気になるが、とりあえず採用。Geohashのエンコードとデコードが書かれたJavaのライブラリが既にあったので、それを使ってみることにする。この辺はこちらを参考に。
参考:琴線探査: Geohashだなぁ
public boolean addMutter(String userName, double latitude,
  double longitude, String message, int boomColor, int radius) {
 boolean ok = false;

 // 緯度経度をGeohashでエンコード
 String geohash = Geohash.encode(latitude, longitude);

 Mutter mutter = new Mutter(userName, latitude, longitude, geohash,
   new Date().getTime(), message, boomColor, radius);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 try {
  pm.makePersistent(mutter);
  ok = true;
 } finally {
  pm.close();
 }

 return ok;
}

public List getMutterAround(double latitudeFrom,
  double longitudeFrom, double latitudeTo, double longitudeTo) {

 // geoboxの緯度経度(南西から北東)をGeohashにエンコード
 String geohashFrom = Geohash.encode(latitudeFrom, longitudeFrom);
 String geohashTo = Geohash.encode(latitudeTo, longitudeTo);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 Query query = pm.newQuery(Mutter.class);
 query.declareParameters("String geohashFrom, String geohashTo");
 query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
 query.setOrdering("geohash ASC");
 List<Mutter> mutters = (List<Mutter>) query.execute(geohashFrom,
   geohashTo);
 log.info("緯度" + latitudeFrom + "から" + latitudeTo + ", 経度"
   + longitudeFrom + "から" + longitudeTo + "までのデータは"
   + mutters.size() + "件です");

 pm.close();

 return mutters;
}
結果は成功。Geohashを使って、不等式を使わずに緯度経度での絞り込みが出来ました♪