GAEのデータストアで使えるJDO関数
JDO2.2で規定されている関数のうち、どれとどれがデータストアで利用できるのか調査してみました。結論としては、
- 「Collection.contains」は利用可。
- 「String.matches」は制限付きで利用可。
- 「String.startWith」も利用可だが、「String.matches」と同じくソート条件の制約を受ける。
- 他はすべて利用不可。
関数 | 利用可/不可 | 補足 |
---|---|---|
contains(Object) | ○ | |
get(Object) | × | そもそもMap型のフィールドを記録できない |
containsKey(Object) | ||
containsValue(Object) | ||
isEmpty() | × | |
size() | × | |
toLowerCase() | × | |
toUpperCase() | × | |
indexOf(String) | × | |
indexOf(String, int) | × | |
matches(String) | △ | 前方一致検索のみ可能。 ソート順が制約を受ける(絞込みで使用した属性でしかソートできない。) |
substring(int) | × | |
substring(int, int) | × | |
startsWith(String) | △ | ソート順が制約を受ける(絞込みで使用した属性でしかソートできない。) |
endsWith(String) | × | |
Math.abs(numeric) | × | |
Math.sqrt(numeric) | × |
ソースもちょっと見てみたのですが、クエリ解析の実装は
org.datanucleus.store.appengine.query.DatastoreQuery
の 848行目 あたりにあって、上記3つ以外はエラーになるコードになっています(たぶん)。
検証用のテストケース
検証で使用したテストケースは以下の通りです。そもそも使い方が間違ってるよ!というところがあったらご指摘頂ければ幸いです。
FunctionTest
import static com.google.inject.matcher.Matchers.annotatedWith; import static com.google.inject.matcher.Matchers.any; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.jdo.JDOFatalUserException; import javax.jdo.Query; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.google.appengine.api.datastore.dev.LocalDatastoreService; import com.google.appengine.tools.development.ApiProxyLocalImpl; import com.google.apphosting.api.ApiProxy; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; /** * 関数のテスト */ public class FunctionTest { /** * 各テスト毎に呼ばれる前準備。 */ @Before public void setUp() throws Exception { ApiProxy.setEnvironmentForCurrentThread(new Environment()); ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){}); } /** * 各テスト毎に呼ばれる後始末。 */ @After public void tearDown() throws Exception { // テストで登録したデータを削除 ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); LocalDatastoreService datastoreService = (LocalDatastoreService) proxy.getService("datastore_v3"); datastoreService.clearProfiles(); ApiProxy.setDelegate(null); ApiProxy.setEnvironmentForCurrentThread(null); } /** * 基本動作のテスト */ @Test public void test_basic() { Service service = HOME.getInstance(Service.class); // テスト用データの登録 TestData[] datas = service.add( new TestData( "aaa", 1, asList( "a", "b" ), createSet( "a", "b" )), new TestData( "bbb", 2, asList( "b", "c" ), createSet( "b", "c" )), new TestData( "ccc", -2, asList( "c" ), createSet( "c" ) ), new TestData( "abc", 0, Arrays.<String>asList( ), createSet( ) ) ); // contains { List<TestData> list = service.list("list.contains(arg1)", "String arg1", "b"); assertEquals( asList( datas[0], datas[1] ), list ); list = service.list("set.contains(arg1)", "String arg1", "c"); assertEquals( asList( datas[1], datas[2] ), list ); } // isEmpty { try { service.list("list.isEmpty()", ""); fail(); } catch ( UnsupportedOperationException e ) {} try { service.list("set.isEmpty()", ""); fail(); } catch ( UnsupportedOperationException e ) {} } // size { try { service.list("list.size() == 2", ""); fail(); } catch ( UnsupportedOperationException e ) {} try { service.list("set.size() == 2", ""); fail(); } catch ( UnsupportedOperationException e ) {} } // toLowerCase { try { service.list("string.toLowerCase() == arg1", "String arg1", "aaa"); fail(); } catch ( UnsupportedOperationException e ) {} } // toUpperCase { try { service.list("string.toUpperCase() == arg1", "String arg1", "AAA"); fail(); } catch ( UnsupportedOperationException e ) {} } // indexOf { try { service.list("string.indexOf(arg1) == -1", "String arg1", "a"); fail(); } catch ( UnsupportedOperationException e ) {} } // matches { // 前方一致のみ利用可。 // このとき、ソート条件はmatchesで指定した属性しか使用できない。 List<TestData> list = service.list("string.matches(arg1)", "String arg1", new Object[]{"a%"}, "string"); assertEquals( asList( datas[0], datas[3] ), list ); // matchesで指定した属性以外でソートするとエラー try { service.list("string.matches(arg1)", "String arg1", new Object[]{"a%"}, "integer"); fail(); } catch ( JDOFatalUserException e ) {} // 末尾が「%」でない以下の条件は利用不可 try { service.list("string.matches(arg1)", "String arg1", new Object[]{"%a"}, "string"); fail(); } catch ( UnsupportedOperationException e ) {} try { service.list("string.matches(arg1)", "String arg1", new Object[]{"a.."}, "string"); fail(); } catch ( UnsupportedOperationException e ) {} try { service.list("string.matches(arg1)", "String arg1", new Object[]{".a."}, "string"); fail(); } catch ( UnsupportedOperationException e ) {} // 「%」のみ使用可で「.」は文字列として認識されている様子・・・。 list = service.list("string.matches(arg1)", "String arg1", new Object[]{".b%"}, "string"); assertEquals( new ArrayList<TestData>(), list ); list = service.list("string.matches(arg1)", "String arg1", new Object[]{"_b%"}, "string"); assertEquals( new ArrayList<TestData>(), list ); } // substring { try { service.list("string.substring(1) == arg1", "String arg1", "aa"); fail(); } catch ( UnsupportedOperationException e ) {} try { service.list("string.substring(1, 1) == arg1", "String arg1", "a"); fail(); } catch ( UnsupportedOperationException e ) {} } // startsWith { // 利用可だが、同時に利用可能なソート条件は制限される List<TestData> list = service.list("string.startsWith(arg1)", "String arg1", new Object[]{"a"}, "string"); assertEquals( asList( datas[0], datas[3] ), list ); // startsWithで指定した属性以外でソートするとエラー try { service.list("string.startsWith(arg1)", "String arg1", new Object[]{"a"}, "integer"); fail(); } catch ( JDOFatalUserException e ) {} } // endWith { try { service.list("string.endWith(arg1)", "String arg1", "a"); fail(); } catch ( UnsupportedOperationException e ) {} } // Math.abs { try { service.list("Math.abs(integer) == arg1", "Integer arg1", "1"); fail(); } catch ( UnsupportedOperationException e ) {} } // Math.abs { try { service.list("Math.sqrt(integer) == arg1", "Integer arg1", "1"); fail(); } catch ( UnsupportedOperationException e ) {} } } /** * Setを生成する * @param strings 文字列 * @return Set */ private Set<String> createSet(String... strings) { Set<String> set = new HashSet<String>(); for ( String str : strings ) { set.add(str); } return set; } /**コンテナ*/ static final Injector HOME = Guice.createInjector( new AbstractModule(){ @Override protected void configure() { bind(Service.class).to(ServiceImpl.class); bindInterceptor( any(), annotatedWith( Tx.Persistence.class ), new Tx.PersistenceManagerInterceptor()); } }); /** * データストアへのアクセスサービス */ static interface Service { /** * データを登録する * @param data データ * @return 登録後のデータ */ TestData[] add( TestData... data ); /** * データの一覧を取得する * @param filter フィルタ文字列 * @param params パラメータ定義 * @param args パラメータ値 * @return データ一覧 */ List<TestData> list(String filter, String params, Object... args); /** * データの一覧を取得する * @param filter フィルタ文字列 * @param params パラメータ定義 * @param args パラメータ値 * @param ソート順 * @return データ一覧 */ List<TestData> list(String filter, String params, Object[] args, String order); } static class ServiceImpl implements Service { @Override @Tx.Persistence public TestData[] add( TestData... datas ) { Tx.getPersistenceManager().makePersistentAll(datas); return datas; } @Override public List<TestData> list( String filter, String params, Object... args ) { return list( filter, params, args, "id ASC" ); } @Override @Tx.Persistence @SuppressWarnings("unchecked") public List<TestData> list(String filter, String params, Object[] args, String order) { Query q = Tx.getPersistenceManager().newQuery( TestData.class ); q.setFilter( filter ); q.declareParameters( params ); q.setOrdering(order); return new ArrayList<TestData>( (List<TestData>) q.executeWithArray( args ) ); } } }
TestData
import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Key; /**テスト用データ*/ @PersistenceCapable(identityType = IdentityType.APPLICATION) class TestData { /** ID */ @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) Key id; /** 文字列 */ @Persistent String string = ""; /** 数値 */ @Persistent int integer = 0; /** 配列 */ @Persistent List<String> list = new ArrayList<String>(); /** セット */ @Persistent Set<String> set = new HashSet<String>(); TestData( String string, int integer, List<String> list, Set<String> set ) { this.string = string; this.integer = integer; this.set = set; this.list = list; } @Override public boolean equals ( Object obj ) { if ( obj == null ) { return false; } if ( obj instanceof TestData ) { Object[] that = getValues((TestData) obj); return Arrays.deepEquals(that, getValues(this)); } return false; } @Override public int hashCode () { return Arrays.deepHashCode(getValues(this)); } private static Object[] getValues(TestData v) { return new Object[] { v.id, v.integer, v.list, v.set, v.string }; } }
Tx
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.jdo.JDOHelper; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.Transaction; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; /** * トランザクション */ public class Tx { /** * {@link PersistenceManager}ホルダ */ private static ThreadLocal<PersistenceManager> pmHolder = new ThreadLocal<PersistenceManager>(); /** * {@link PersistenceManager}を取得する。 * @return {@link PersistenceManager} */ public static PersistenceManager getPersistenceManager() { return pmHolder.get(); } /** * {@link PersistenceManager}を取得する。 * @return {@link PersistenceManager} */ private static PersistenceManager createPersistenceManager() { return pmfInstance.getPersistenceManager(); } /** * {@link PersistenceManagerFactory}は作成コストが高いらしいのでアプリケーションごとに一意にすること。 */ private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional"); /** * 永続化処理を行うメソッドを示すアノテーション */ @Retention( RetentionPolicy.RUNTIME ) @Target({ElementType.METHOD }) public static @interface Persistence {}; /** * トランザクションの開始と終了を行うメソッドを示すアノテーション */ @Retention( RetentionPolicy.RUNTIME ) @Target({ElementType.METHOD }) public static @interface WithTx {}; /** * PersistenceManager生成インターセプタ */ public static class PersistenceManagerInterceptor implements MethodInterceptor { @Override public Object invoke ( MethodInvocation mi ) throws Throwable { PersistenceManager pm = getPersistenceManager(); if ( pm != null ) return mi.proceed(); try { // PersistenceManager を取得 pm = createPersistenceManager(); pmHolder.set( pm ); return mi.proceed(); } finally { // PersistenceManager はクローズ必須。 try { if ( pm != null ) pm.close(); } finally { pmHolder.set( null ); } } } } /** * トランザクションインターセプタ */ public static class TxInterceptor implements MethodInterceptor { @Override public Object invoke ( MethodInvocation mi ) throws Throwable { PersistenceManager pm = getPersistenceManager(); Transaction tx = pm.currentTransaction(); if ( tx.isActive() ) return mi.proceed(); try { tx.begin(); // トランザクション開始 Object res = mi.proceed(); tx.commit(); // コミット return res; } finally { if (tx.isActive()) tx.rollback(); } } } }