無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

GAEのデータストアで使えるJDO関数

JDO2.2で規定されている関数のうち、どれとどれがデータストアで利用できるのか調査してみました。結論としては、

  • 「Collection.contains」は利用可。
  • 「String.matches」は制限付きで利用可。
    • 前方一致条件(「hoge%」みたいな条件)のみ指定できる。
      • 後方一致(「%hoge」)や部分一致(「%hoge%」)は使えない。
      • ワイルドカードは「%」。(仕様書では「.*」が任意の長さの文字列になっているように読めるのだけど・・・)
    • 他のソート条件との併用は不可。(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();
            }
        }
    }
}