とりあえず雑記帳(跡地)
文字列の部分一致検索とページング
最終更新:
fujiyan
-
view
Slim3で部分一致検索(Like検索)を頑張ってみる
(2012/06/19追記)
- 検索結果のキャッシュのことを考えたら、本ページのような小細工をせずに、おとなしく結果全件をListで取得したほうが良い気がしてきました…。
- 検索結果の件数が大きい場合を想定して、Listで取得するのを控えていたのですが、そもそも、そんな検索を許さないようにしたほうが健全ですしね…。
Datastoreでの文字列検索
- GAEのDatastoreでは、Entityの検索方法として、プロパティの文字列の前方一致をネイティブでサポートしている。
- よって、Slim3でも基本は文字列の前方一致となる。
package jp.fujiyan.gae.datastoretest;
import java.text.DecimalFormat;
import java.util.List;
import jp.fujiyan.gae.datastoretest.meta.FooMeta;
import jp.fujiyan.gae.datastoretest.model.Foo;
import junit.framework.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slim3.datastore.Datastore;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
public class QueryTest {
private final LocalServiceTestHelper helper =
new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
private static DecimalFormat FORMAT = new DecimalFormat("0000");
@Before
public void setUp() throws Exception {
helper.setUp();
}
@After
public void tearDown() throws Exception {
helper.tearDown();
}
@Test
public void test1() {
// 2000個のEntityを作成
for (int i = 1; i <= 2000; i++) {
Foo foo = new Foo();
foo.setName("Foo" + FORMAT.format(i));
Datastore.put(foo);
}
// "Foo11"で始まるEntityは100個
List<Foo> list = Datastore.query(Foo.class).filter(FooMeta.get().name.startsWith("Foo11")).asList();
Assert.assertEquals(100, list.size());
}
}
InMemoryFilter
- 前方一致だけでは何かと不便なので、Slim3では部分一致でも検索が可能な手段を用意している。
- InMemoryFilterという仕組みで、Datastoreから返された検索結果に対してフィルタリングをかけている。
※以降、面倒なのテストメソッドのみ掲示…
@Test
public void test1() {
// 2000個のEntityを作成
for (int i = 1; i <= 2000; i++) {
Foo foo = new Foo();
foo.setName("Foo" + FORMAT.format(i));
Datastore.put(foo);
}
// "2"を含むEntityは543個
List<Foo> list = Datastore.query(Foo.class).filterInMemory(FooMeta.get().name.contains("2")).asList();
Assert.assertEquals(543, list.size());
}
limitとoffset
- ページングを実施する上で必要な処理として、例えば、ページ毎10件のページング処理で、8ページ目に該当するレコードを取得する場合には、71件目から10件分だけレコードを抽出する処理がある。
- GAEのDatastoreでは、Queryのn件目から取得(offset)、クエリの結果をn件まで取得(limit)、というのがネイティブでサポートされている。
- よって、Slim3でもoffsetやlimitが利用可能
- これさえ使えば、ページングも楽々ですよ。
@Test
public void test1() {
// 2000個のEntityを作成
for (int i = 1; i <= 2000; i++) {
Foo foo = new Foo();
foo.setName("Foo" + FORMAT.format(i));
Datastore.put(foo);
}
// "Foo11"で始まるEntityは100個
List<Foo> list = Datastore.query(Foo.class).filter(FooMeta.get().name.startsWith("Foo11")).offset(71).limit(10).asList();
Assert.assertEquals(10, list.size());
Assert.assertEquals("Foo1171", list.get(0).getName());
Assert.assertEquals("Foo1172", list.get(1).getName());
Assert.assertEquals("Foo1173", list.get(2).getName());
Assert.assertEquals("Foo1174", list.get(3).getName());
Assert.assertEquals("Foo1175", list.get(4).getName());
Assert.assertEquals("Foo1176", list.get(5).getName());
Assert.assertEquals("Foo1177", list.get(6).getName());
Assert.assertEquals("Foo1178", list.get(7).getName());
Assert.assertEquals("Foo1179", list.get(8).getName());
Assert.assertEquals("Foo1180", list.get(9).getName());
}
InMemoryFilterとlimit/offsetは併用できない…
- じゃあ、あとはInMemoryFilterを使えば、部分一致検索の結果に対してページングが出来そうですね。
@Test
public void test1() {
// 2000個のEntityを作成
for (int i = 1; i <= 2000; i++) {
Foo foo = new Foo();
foo.setName("Foo" + FORMAT.format(i));
Datastore.put(foo);
}
// "2"を含むEntityは543個
List<Foo> list = Datastore.query(Foo.class).filterInMemory(FooMeta.get().name.contains("2")).offset(0).limit(10).asList();
// 543個の中から先頭10個をとったはずが…
Assert.assertEquals(10, list.size());
}
上記のテストの実施結果は
junit.framework.AssertionFailedError: expected:<10> but was:<1> ...
- なんと1件しか取れていません。
- これは、SlimがGAEのネイティブなQueryを先に実施し、その結果に対してInMemoryFilterを適用するからです。
- 上記の場合、2000個のEntityに対して、最初にoffset(0).limit(10)が実施されます。その結果、Foo0001~Foo0010の10個のEntityが返されます。
- そのFoo0001~Foo0010の結果に対して、contains("2")のInMemoryFilterが実施されるため、結果はFoo0002の1件しか残らなくなるのです。
じゃあ、地道にasIterator()で拾い上げようと思ったら…
- offsetやlimitが使えないとなると、クエリ結果を最初から順番に拾い上げて、ページ先頭レコードまでスキップした後、目的の件数分取得する、しかなさそう。
- ちょうどModelQuery#asIterator()というメソッドがクエリ結果を拾い上げるIteratorを返してくれそうです。では早速…
java.lang.IllegalStateException: In case of asIterator(), you cannot specify filterInMemory(). ...
- はい、一旦InMemoryFilterを適用してしまうと、asIterator()は使えないのです。
- まぁ、Slim3の仕様として諦めてください…。
そこそこ自前で処理するしかなさそうです
- ということで、何もFilterを適用しないModelQueryの結果に対して、指定の条件でフィルタリングした結果を拾い上げるIteratorを作るしか無さそうです。
- キモなところはApacheのCommons Collectionsを使っています。
- まぁ代わりにGuavaのIteratorsを使ってもイイかと思います。
package jp.fujiyan.gae.datastoretest;
import java.util.Iterator;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.slim3.datastore.InMemoryFilterCriterion;
import org.slim3.datastore.ModelQuery;
/**
* InMemoryFilterでasIterator()が使えないため用意しています。
* @author Fujiyan
*
* @param <M>
*/
public class ModelQueryIterator<M> implements Iterator<M> {
public interface IPredicate<M> {
boolean evaluate(M model);
}
protected FilterIterator delegate;
public ModelQueryIterator(ModelQuery<M> query, InMemoryFilterCriterion criterion) {
final InMemoryFilterCriterion innerCriterion = criterion;
this.delegate = new FilterIterator(query.asIterator(),
new Predicate() {
@Override
public boolean evaluate(Object obj) {
return innerCriterion.accept((M) obj);
}
}
);
}
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@Override
public M next() {
return (M) delegate.next();
}
@Override
public void remove() {
delegate.remove();
}
}
- いろいろ工夫の余地はありますが、とりあえず検証用のサンプルとして…。
- 使い方は下記の通り
@Test
public void test1() {
// 2000個のEntityを作成
for (int i = 1; i <= 2000; i++) {
Foo foo = new Foo();
foo.setName("Foo" + FORMAT.format(i));
Datastore.put(foo);
}
// "2"を含むEntityは543個
Iterator<Foo> iterator = new ModelQueryIterator<Foo>(Datastore.query(Foo.class), FooMeta.get().name.contains("2"));
Assert.assertEquals("Foo0002", iterator.next().getName());
Assert.assertEquals("Foo0012", iterator.next().getName());
Assert.assertEquals("Foo0020", iterator.next().getName());
Assert.assertEquals("Foo0021", iterator.next().getName());
Assert.assertEquals("Foo0022", iterator.next().getName());
Assert.assertEquals("Foo0023", iterator.next().getName());
Assert.assertEquals("Foo0024", iterator.next().getName());
Assert.assertEquals("Foo0025", iterator.next().getName());
Assert.assertEquals("Foo0026", iterator.next().getName());
Assert.assertEquals("Foo0027", iterator.next().getName());
}
- あとは、このModelQueryIteratorで、指定のレコードまでスキップ/指定の件数を取得、というお決まりのページング処理を行えば、何とかなるかと思います。