より良い環境を求めて このページをアンテナに追加 RSSフィード

2012-02-28

[] S2JDBCのiterate()で OutOfMemoryError

S2JDBCを使って DBのデータ数十万件をまとめてDLしようとしたらエラーになった。

getResultList()じゃなくてiterate()使ったら結果をまとめて保持しなくなるからメモリを使わなくなるんじゃないの?と思ったけどダメ。

WicketのResourceStream系がキャッシュしてるのか自分で書いたオブジェクトを書き出す処理がミスってたのか何か使い方が間違ってたのか分からずハマった。

Eclipse Memory Analyzer でダンプを見たら、どこかでデータを全件保持している模様。


2/29 ここから追記

S2JDBC+PostgreSQLだとs2jdbc.diconのfetchSizeプロパティの設定+トランザクションをきちんと開始する、で解決しそう。(とりあえずローカル環境でエラーの再現 → エラー修正の確認まではできた)

PostgreSQL: http://old.postgresql.jp/wg/jpugdoc/jdbc/jdbc-8.3dev-600/query.html#query-with-cursor http://ml.postgresql.jp/pipermail/pgsql-jp/2005-May/010531.html

MySQL: http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html


更新のときだけトランザクションを真面目にやって、表示系は無しでいいやと思って前に変えたのだった… *1



見ているサイトは情報が古すぎ or 誤読 or 間違いでしたorz

大昔のサーバー(PostgreSQL7.3とか?)を使っているのでない限り、昨日書いた記述は間違い。各々のDB&バージョンごとに注意点があるようなのでそれぞれチェックする必要があるっぽい。日記コメント、ブクマコメントの指摘助かりました。

追記おわり 以下間違い

おそらく、原因はこれ。

S2JDBC を使わずに直接 JDBC API を呼び出してみても,

PreparedStatement#executeQuery() の呼び出し中に

OutOfMemoryError になってしまい,ResultSet が

返ってきません.

このことから,S2JDBC は関係ないように思われます.

これが MySQL の問題なのか Connector/J の設定なのか

よく分かりませんが,まずは JDBC 直接呼び出しで

結果を取得する方法を調べてみてはいかがでしょうか?

http://ml.seasar.org/archives/seasar-user/2008-July/015066.html

基礎となるサーバーがスクロール可能なカーソルをサポートしていないの

で、スクロール可能性は別のレイヤーOracle JDBC によって実装する必要

があります。

この機能は、スクロール可能な結果セットの行をクライアント側のメモリ

ー・キャッシュに格納することにより、実現されていることに注意してくだ

さい。

重要:スクロール可能な結果セットの行はすべて、クライアント側の

キャッシュに格納されます。そのため、結果セットに多くの行、多くの列

または非常に大きな列が含まれていると、クライアント側のJava Virtual

Machine に障害が発生する可能性があります。大きな結果セットにはスク

ロール可能性を指定しないでください。

Oracle サーバーのスクロール可能なカーソル、すなわちサーバー側のキャッ

シュは、将来のOracle リリースでサポートされます。

http://www.oracle.co.jp/forum/thread.jspa?messageID=11006931

ResultSet


SELECTを実行した結果は、ResultSetで受け取る。

next()を使って結果の各レコードを処理する。レコード内の項目の値は、(データ型に応じて)getString()等を使って取得する。

PostgreSQLJDBCが返すResultSetでは、複数レコードを取得するSQLの場合は 全結果を一度に取り込もうとするらしい。

レコード数が無茶苦茶多い(何十万件とか何百万件とかだ)と、OutOfMemoryExceptionが発生する。

http://www.ne.jp/asahi/hishidama/home/tech/postgres/jdbc.html

全体的にJDBCはそういう設計?


つまり、いくらS2JDBCアプリでメモリを使わないようにしてもJDBCのResultSetが全件溜め込んでいるのでアウト。

仕方がないので全件取得するところをlimitとoffsetで分割したものを繋げてストリームに書き出すことにした。

なんか微妙だ。

2012-01-07

[] S2JDBCでhstore用のwhereクラスを作る

S2JDBCでPostgreSQLのhstoreを使う - より良い環境を求めて の続き。


S2JDBCのwhereメソッドに指定するクラスは、Whereクラスを継承して作れる。

abstract class PgsqlOperator<PARAM_TYPE> implements Where {
    protected final CharSequence propertyName;
    protected final PARAM_TYPE param;
    public PgsqlOperator(CharSequence propertyName, PARAM_TYPE param) {
        this.propertyName = propertyName;
        this.param = param;
    }
    @Override
    public Object[] getParams() {
        return new Object[] { param };
    }
    @Override
    public String[] getPropertyNames() {
        return new String[] { propertyName.toString() };
    }
}

単純にプロパティパラメーターを取るクラスをまず作って

public class ExistFunction extends PgsqlOperator<String> {
    public ExistFunction(CharSequence propertyName, String param) {
        super(propertyName, param);
    }
    @Override
    public String getCriteria() {
        return "exist(" + propertyName + ", ?)";
    }
}

こんな感じで書く。


包含の演算子の場合は

public class CompriseOperator extends PgsqlOperator<HStore> {
    public CompriseOperator(CharSequence propertyName, HStore param) {
        super(propertyName, param);
    }
    @Override
    public String getCriteria() {
        return propertyName + "@> ?";
    }
}

こんな感じ。


それからユーティリティクラスを作る。

public class PgsqlOperations {
    public static <PropType> Where exist(PropertyName<PropType> propertyName, String param) {
        return new ExistFunction(propertyName, param);
    }
    public static Where comprise(PropertyName<HStore> propertyName, HStore param) {
        return new CompriseOperator(propertyName, param);
    }
    ....


使うときは

        FooEntity foo1 = select()
            .where(exist(foo(), "foo"))
            .getSingleResult();

        FooEntity foo2 = select()
            .where(comprise(foo(), new HStore("\"foo\"=>\"va\"")))
            .getSingleResult();

こんな感じ。

2012-01-06

[] S2JDBCPostgreSQLのhstoreを使う

PostgreSQLにはKVSのような型 hstore ( http://www.postgresql.jp/document/8.4/html/hstore.html ) がある。


横に巨大なExcelデータベースに突っ込む場合、いちいちカラム作らなくていいんじゃね?ということでhstoreにごっそり入れてみる試み。

そういうExcelに限って分類1、分類2、分類3とか書いてあって謎仕様。全部テキストで入れるからもう好きなだけ作ってくれーって感じで。




取り敢えず、取得と更新まで出来た。


http://d.hatena.ne.jp/kuniku/20090819/1250656540

ここを参考にそれぞれのクラスを作る。


PostgreDialectEx.java

public class PostgreDialectEx extends PostgreDialect {
    @Override
    public ValueType getValueType(PropertyMeta propertyMeta) {
        final Class<?> clazz = propertyMeta.getPropertyClass();
        if (clazz == HStore.class)
            return HStoreType.INSTANCE;

        return super.getValueType(propertyMeta);
    }

    @Override
    public ValueType getValueType(Class<?> clazz, boolean lob, TemporalType temporalType) {
        if (clazz == HStore.class)
            return HStoreType.INSTANCE;

        return super.getValueType(clazz, lob, temporalType);
    }
}

HStore型のデータはHStoreTypeを使うようにする設定。


HStoreType.java


public class HStoreType extends AbstractValueType {

    public static final HStoreType INSTANCE = new HStoreType();

    public HStoreType() {
        super(Types.OTHER);
    }

    @Override
    public void bindValue(PreparedStatement ps, int index, Object value) throws SQLException {
        if (value == null) {
            setNull(ps, index);
        } else {
            ps.setObject(index, value);
        }
    }

    @Override
    public void bindValue(CallableStatement cs, String parameterName, Object value) throws SQLException {
        if (value == null) {
            setNull(cs, parameterName);
        } else {
            cs.setObject(parameterName, value);
        }
    }

    @Override
    public Object getValue(ResultSet rs, int index) throws SQLException {
        PGobject obj = (PGobject) rs.getObject(index);
        //if (obj.getType().equals("hstore"))
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(ResultSet rs, String columnName) throws SQLException {
        PGobject obj = (PGobject) rs.getObject(columnName);
        //if (obj.getType().equals("hstore"))
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(CallableStatement cs, int index) throws SQLException {
        PGobject obj = (PGobject) cs.getObject(index);
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(CallableStatement cs, String parameterName) throws SQLException {
        PGobject obj = (PGobject) cs.getObject(parameterName);
        return new HStore(obj.getValue());
    }

    @Override
    public String toText(Object value) {
        if (value == null) {
            return BindVariableUtil.nullText();
        }
        return BindVariableUtil.toText(value);
    }

}

ほとんどObjectTypeと同じだけれど。


HStore.java


public class HStore extends PGobject {

    /**
     * 
     */
    private static final long serialVersionUID = -6859240870323667683L;

    private final HashMap<String, String> data = new HashMap<String, String>();

    public HStore() {
        super();
        setType("hstore");
    }
    public HStore(String text) {
        this();
        if (text == null || text.isEmpty()) {
            return;
        }

        setValue(text);
    }
    @Override
    public void setValue(String value) {
        for(;;) {
            String ret = parse(value);
            if (ret == null)
                break;
            value = ret;
        }
    }
    private String parse(String text){
        // "key"=>"value", "key"=>"value" の文字列を地道に解析する
        String buf = text;
        int keyStart = text.indexOf('"');
        if (keyStart == -1)
            return null;
        buf = text.substring(keyStart + 1);
        String key = null;
        for (int i = 0, length = buf.length(); i<length; i++) {
            if (buf.charAt(i) == '"') {
                if (i == 0) {
                    key = "";
                    buf = buf.substring(i+1);
                    break;
                }
                if (buf.charAt(i - 1) != '\\') {
                    key = buf.substring(0, i);
                    buf = buf.substring(i+1);
                    break;
                }
            }
        }
        String value = null;
        buf = buf.substring(buf.indexOf("=>") + 2);
        if (buf.startsWith("NULL, ")) {
            data.put(key, value);
            return buf.substring(buf.indexOf("NULL, ")+6);
        }
        buf = buf.substring(buf.indexOf('"') + 1);
        for (int i = 0, length = buf.length(); i<length; i++) {
            if (buf.charAt(i) == '"') {
                if (i == 0) {
                    value = "";
                    buf = buf.substring(i+1);
                    break;
                }
                if (buf.charAt(i - 1) != '\\') {
                    value = buf.substring(0, i);
                    buf = buf.substring(i+1);
                    break;
                }
            }
        }
        data.put(key, value);
        return buf;
    }
    @Override
    public String getValue() {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String key: data.keySet()) {
            if (!first)
                sb.append(", ");
            sb.append('"');
            sb.append(key);
            sb.append("\"=>");

            String value = data.get(key);
            if (value == null) {
                sb.append("NULL");
            }else {
                sb.append('"');
                sb.append(value);
                sb.append('"');
            }
            first = false;
        }
        return sb.toString();
    }
    @Override
    public int hashCode() {
        return data.hashCode();
    }
    @Override
    public boolean equals(Object o) {
        return data.equals(o);
    }
    public HStore put(String key, String value) {
        data.put(key, value);
        return this;
    }
    public String get(String key) {
        return data.get(key);
    }
    public HashMap<String, String> getInternalMap(){
        return data;
    }
}

キーが空白の場合と値がNULLの場合にそれぞれ対応するとややこしくなってきた。PostgreSQLのバージョンによってフォーマットが変わったら使えなくなるかも。(NULLが小文字になるとか区切りに空白が増えるとか)

Mapを引数に取るコンストラクタを書くのを忘れてる…まあいいか。


全体的に深追いしてないのであまり自信なし。


ここまで書けば、あとは s2jdbc.dicon に

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
        <include path="jdbc.dicon"/>
        <include path="s2jdbc-internal.dicon"/>

<component name="postgreDialectEx" class="com.example.pgsql.PostgreDialectEx" />

        <component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
                <property name="maxRows">0</property>
                <property name="fetchSize">0</property>
                <property name="queryTimeout">0</property>
                <property name="dialect">postgreDialectEx</property>
        </component>

</components>

設定を書くと完成。


S2JDBC-Genも対応しようと思ったけど拡張の仕方が分からなかったので保留。jarのコードを修正せずに拡張できるのだろうか…。


次は演算子と関数を作る。

2011-12-29

[] S2JDBCデータベーススキーマ中心の開発

S2JDBCJavaを修正→DB修正っていう方針だけれど、使い始めてからずっと逆向きでやっている。


やり方をメモ。

制作中のワークフローは

  1. EclipseのERMasterでERD作成
  2. DDL生成
  3. DBリビルドスクリプト実行 ※1
  4. Java更新スクリプト実行 ※2
  5. S2JDBCのタスク実行

という流れ。


サービスが稼働し始めたら

  1. DB更新スクリプト作成&実行
  2. エンティティのJava修正
  3. DBJavaアプリの本番反映
  4. ERD修正

という流れになる。稼働後の修正は書き換え箇所が少ないので手動でエンティティのコードとNamesを修正している。(Namesは再生成でもいいかも)



  • serviceパッケージ直下に生成されたサービスクラスは基本いじらない。
  • AbstractServiceクラスでselectメソッドをpublicでオーバーライドする。
    • 一度しか使わないselect系処理はページ用クラスに書く。(複数の画面で似たようなselect文を使う場合でも、微妙にjoinが異なることが多いので)
  • service.impl に独自クラスを作りトランザクション管理が必要な複数更新系メソッドを書く。
  • 必要ならgenタスク実行前にservice直下のファイルを削除(プライマリキーを変えた場合)、entityのファイルを削除(テーブル構成を変えるとエラーが出ることがある)。
  • ※1 では、生成したDDLでテーブルを作り直し、テストデータを入れたりシーケンスの初期値を設定したりしている。
  • ※2 では、gen task を ant run したあと、@Generatedアノテーションを消したり、OneToOne関連の不要なGenerationType.IDENTITY消したり、自己関連設定時に*Names.javaに生成される不要なimportを消したりしている。

現状はこんな感じでやっている。


DBFluteを使えばいいという話かもしれないが、こっちはサンプルを少し動かしただけでやめてしまった。SQLの組み立て方はS2JDBCの方が合っているし、シンプルで自由な方がいい。

ただequalsとhashCodeはEntity用のメソッドが欲しい。これはInterTypeで対応できるだろうか。エンティティはコンポーネントじゃないから無理だね…。

2011-12-13

[] Tomcat で S2Wicket、S2Chronos を使った時にログに clearReferencesThreads が出る

アンデプロイ時にエラーが出る。


S2Wicketの場合。

2011/12/13 16:57:37 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
致命的: The web application [/app] appears to have started a thread named [ModificationWatcher Task] 
but has failed to stop it. This is very likely to create a memory leak.

というエラーが出る。


ReloadingWicketFilter は、リロード時にはReloadingClassLoaderのdestroyメソッドを呼ぶが、終了時には呼ばない。本番環境で使うことが想定されていない?というかそもそもメンテされていない?


S2WicketFilterのdestroy時にReloadingClassLoaderのdestroyを呼ぶ。

        ClassLoader classLoader = getClassLoader();
        if (classLoader instanceof ReloadingClassLoader)
            ((ReloadingClassLoader) classLoader).destroy();

これで解決?

# 追記: initの最初に destroy() を呼んでいるが、ここでは ReloadingClassLoaderを破棄してはいけないのでこっちも変更する必要があった。




S2Chronosの場合。

2011/12/13 16:39:52 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
致命的: The web application [/app] appears to have started a thread named 
[chronos-pool-1-daemonthread-4] but has failed to stop it. This is very likely to create a memory leak.

のようなログ。


検索したら、同じ症状を発見。

http://sourceforge.jp/ticket/browse.php?group_id=4342&tid=26285 (diff)


setContextClassLoader(null) だけ変更すればいいのだろうかと思ってやってみるとタスクが動作しなかった。


1.0.1-SNAPSHOTだと色々変わってるようなので変更してみたがこれでもダメ。


あと時々タスクが実行されない。この時々がうまく再現できない。 DB連携絡みのプログラムのミスっぽい。または、CloneTaskしていないものを複数実行しようとしたとか。

NonDelayTriggerやDelayTriggerを頻繁に起動する + CronTrigger で、一年間放ったらかしていても動くのか少し不安が残る。

S2Chronosは手軽で便利なんだけど、cronでシンプルにwgetした方がトラブルになりにくいかな…。


DelayTrigger手動登録の代わりは普通にThread生成してsleepするか、GAEのようにURLを呼び出すか…。WicketのPageParametersでURL生成がリンクと同じようにできるから、URL呼び出しでもいいような気がしてきた。

2011-11-29

[] S2JDBCからApache Cayenneに移行した

WicketSeasar(S2Container、S2Wicket、S2JDBC)で一通り開発したけれど、なんだか合わないなと感じた。

不満だったのは以下の点。

  • S2JDBC-Genで生成したサービスクラスが使いにくい。
  • S2ContainerとWicketの両方にオブジェクト生成機能がある
    • HOT Deployが使えない。
    • S2Containerの管理するパッケージ(service、entityなど)とReloadingWicketFilterが管理するパッケージ(page、component)を意識する必要がある。
    • WARM deployを使うのでS2Containerが管理するservice以下などを変更するとWebサーバーの再起動が必要。
  • 開発モードではリロード・戻るなどの動作でエラーになる。

元々はS2JDBCを使いたいからPHPからJavaに来たのだが、S2JDBCに不満が出てきた。

あるページではItemDetailテーブル+Itemテーブルからデータを取得し、別のページではItemDetailテーブル+Itemテーブル+ItemCategoryテーブルからデータを取得する必要があったとする。このときJoinはどこに書くかという問題。ItemDetailService#getItemDetailWithItemとItemDetailService#getItemDetailWithItemAndCategoryという二つのメソッドを作るか、それとも例え使わないとしても関連テーブルから全部データを持ってくるか。

そもそも、どの情報をビューに出したいのかはビューが決めることで、表示する関連項目が一つ増えたくらいでServiceを変更したくない。

jdbcManager.from(Employee.class)
    .where(
        eq(name(), name),
        gt(salary(), salary))
    .getResultList();
http://s2container.seasar.org/2.4/ja/s2jdbc_typesafe.html

この処理が異なる5ページから呼ばれているとして、例えば詳細検索画面から呼び出すときだけ条件を追加する場合、新着ページから呼び出す場合だけソートを変更する場合、などなど。別のメソッドを作ればいいのか引数をどんどん増やしていくのが良いのかイマイチ分からない。

ServiceのメソッドがAutoSelectを返すようにしたパターンもやってみたけど二度手間感がある。


もう一つ、データベースを変更する場合、個人的には「ER図を書く → SQLを生成 → DBに反映 → Javaコード生成」の順でやりたい。

これがS2JDBC-Genだと逆にJavaに基づいてデータベースを定義する流れになる。ER図から書きたい。


# ※1

# 今書いていて気付いたが、トランザクションが必要な更新処理だけServiceクラスに書いて、取得処理はWicketのPageやComponentからJdbcManagerを呼び出してServiceを使わないっていう方法もあるね。自動生成されたServiceクラスは一切触らず、複数テーブルを更新するときは別途Serviceクラスを作る感じで。そうするとS2JDBC-Genを繰り返し実行しても問題にならない。

# 最初にSeasar2を使ったときにいきなりS2JDBC-Genから入ったけど、データの取得または一行更新なら別にサービスクラスすら必要ないんだよね。例題がServiceクラスを使う例ばかりなので使わないという選択肢がすっぽり抜けていた。


そこで現在22テーブル、50ページある制作中のシステムをApache Cayenneに移行した。


参考:

http://d.hatena.ne.jp/t_yano/20081118/1227008018

http://cayenne.apache.org/doc30/tutorial-persistent-objects.html

http://www.atmarkit.co.jp/fjava/products/cayenne/cayenne_2.html


Cayenneの設定

マニュアルに従ってGUIツールでDBからテーブル定義よ読み込みCayenneの設定ファイルを作りJavaコードを生成する。

プライマリーキーはMeaningful Primary Keyにして、Java側でも取得できるようにしておく。

プライマリキー名を指定せずにserialまたはbigserial型で生成した場合、テーブルごとのSequence Nameをテーブル名_列名_seqに指定する。

pom.xml

<dependency>
   <groupId>org.apache.cayenne</groupId>
   <artifactId>cayenne-server</artifactId>
   <version>3.0.2</version>
   
</dependency>
<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>3.0</version>
</dependency>
<dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-guice</artifactId>
    <version>1.5.3</version>
</dependency>

バージョン3.0.2を使った。

そしてS2JDBCを使わないならSeasar2も不要なのでGuiceを使う。


アプリケーションのjava

public class WebApp extends AuthenticatedWebApplication {
    /* for Cayenne 3.1 */
    // private static ServerRuntime cayenneRuntime;

    private static class ObjectContextProvider implements Provider<ObjectContext> {
        @Override
        public ObjectContext get() {
            try {
                ObjectContext ret = BaseContext.getThreadObjectContext();
                return ret;
            }catch (IllegalStateException e) {
                ObjectContext ret = DataContext.createDataContext();
                BaseContext.bindThreadObjectContext(ret);
                return ret;
            }
        }
    }

    private static class GuiceModule extends AbstractModule {
        @Override
        protected void configure() {
            //bind(ObjectContext.class).toInstance(DataContext.createDataContext());
            bind(ObjectContext.class).toProvider(ObjectContextProvider.class).in(Scopes.NO_SCOPE);
        }
    }
    public static Injector getInjector() {
        GuiceInjectorHolder holder = Application.get().getMetaData(GuiceInjectorHolder.INJECTOR_KEY);
        return holder.getInjector();
    }
    public static ObjectContext getDb() {
        return getInjector().getInstance(ObjectContext.class);
    }
    @Override
    protected void init(){
        super.init();
        // 文字コード・タイムゾーン設定
        getRequestCycleSettings().setResponseRequestEncoding("UTF-8");
        getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
        TimeZone.setDefault(TimeZone.getTimeZone("JST"));

        // アプリケーション設定
        IApplicationSettings appSettings = getApplicationSettings();
        appSettings.setInternalErrorPage(ErrorPage.class);
        appSettings.setPageExpiredErrorPage(ExpiredErrorPage.class);

        // フレームワーク設定
        getFrameworkSettings().add(new AnnotationEventDispatcher());

        // Guice設定
        getComponentInstantiationListeners().add(new GuiceComponentInjector(this, new GuiceModule()));

        // Cayenne設定
        /* for Cayenne 3.1
        if (cayenneRuntime != null)
            cayenneRuntime.shutdown();
        cayenneRuntime = new ServerRuntime("cayenne.xml");
        */

        ....

こんな感じで。

staticメソッドでいつでもGuiceからObjectContextを取得できるようにしておく。

bindThreadObjectContext の使い方はまだあまり分かっていない。


ユーティリティクラスを作る

Cayenneにはcount()のようなメソッドがないのでCountHelperを作る。

http://cayenne.195.n3.nabble.com/Create-count-query-based-on-SelectQuery-td131870.html

public class CountHelper {
    public static class RuntimeQuery extends RuntimeException {
        private static final long serialVersionUID = -827119367373517203L;
        public RuntimeQuery() {
            super();
        }
        public RuntimeQuery(String message, Throwable cause) {
            super(message, cause);
        }
        public RuntimeQuery(String message) {
            super(message);
        }
        public RuntimeQuery(Throwable cause) {
            super(cause);
        }
    }

    public static long count(DataContext context, SelectQuery query) {
        return count(context, query, context.getParentDataDomain().
            getDataNodes().iterator().next());
    }
    public static long count(DataContext context, SelectQuery query,
            DataNode node) {
        CountTranslator translator = new CountTranslator();

        translator.setQuery(query);
        translator.setAdapter(node.getAdapter());
        translator.setEntityResolver(context.getEntityResolver());

        Connection con = null;
        PreparedStatement stmt = null;
        try {
            con = node.getDataSource().getConnection();
            translator.setConnection(con);

            stmt = translator.createStatement();

            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return rs.getLong(1);
            }

            throw new RuntimeQuery("Count query returned no result");
        }
        catch (Exception e) {
            throw new RuntimeQuery("Cannot count", e);
        }
        finally {
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (con != null) {
                    con.close();
                }
            }
            catch (Exception ex) {
                throw new RuntimeQuery("Cannot close connection", ex);
            }
        }
    }

    static class CountTranslator extends SelectTranslator {
        @Override
        public String createSqlString() throws Exception {
            String sql = super.createSqlString();
            int index = sql.indexOf(" FROM ");

            return "SELECT COUNT(*)" + sql.substring(index);
        }
    }
}
http://osdir.com/ml/user-cayenne-apache/2009-09/msg00012.html

SelectQueryやExpressionを組み立てるクラスも作る。

ちょっと長いが全部載せる。

public class Q<T1> implements Serializable {
    private final Class<T1> clazz;
    private Expression exp;
    private Q(Class<T1> clazz) {
        this.clazz = clazz;
    }

    /**
     * 基底となるエンティティクラスを指定する
     * @param <T2>
     * @param clazz
     * @return
     */
    public static <T2> Q<T2> from(Class<T2> clazz) {
        return new Q<T2>(clazz);
    }

    /**
     * 現在のObjectContextでデータを再取得する
     * @param db
     * @param entity
     * @return
     */
    public T1 reload(ObjectContext db, Persistent entity) {
        exp = ExpressionFactory.matchExp(entity);
        return parse().getSingle(db);
    }
    /**
     * プライマリキーでエンティティを検索する
     * @param db
     * @param primaryKey
     * @return
     */
    public T1 getById(ObjectContext db, Object primaryKey) {
        return DataObjectUtils.objectForPK(db, clazz, primaryKey);
    }
    public T1 getSingle(ObjectContext db) {
        return parse().getSingle(db);
    }

    public Executor<T1> parse(){
        return new Executor<T1>(this);
    }

    private void addExp(Expression addition) {
        if (exp == null)
            exp = addition;
        else
            exp = exp.andExp(addition);
    }

    /**
     * 条件を文字列で指定する
     * @param where
     * @return
     */
    public Q<T1> where(String where){
        addExp(Expression.fromString(where));
        return this;
    }
    public Q<T1> where(Expression addition) {
        addExp(addition);
        return this;
    }

    /**
     * = 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> eq(String property, Object value) {
        addExp(ExpressionFactory.matchExp(property, value));
        return this;
    }

    /**
     * != 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> notEq(String property, Object value) {
        addExp(ExpressionFactory.noMatchExp(property, value));
        return this;
    }

    /**
     * col > ? 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> greater(String property, Object value) {
        addExp(ExpressionFactory.greaterExp(property, value));
        return this;
    }
    /**
     * col >= ? 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> greaterEq(String property, Object value) {
        addExp(ExpressionFactory.greaterOrEqualExp(property, value));
        return this;
    }
    /**
     * col < ? 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> less(String property, Object value) {
        addExp(ExpressionFactory.lessExp(property, value));
        return this;
    }
    /**
     * col <= ? 条件を追加する
     * @param property
     * @param value
     * @return
     */
    public Q<T1> lessEq(String property, Object value) {
        addExp(ExpressionFactory.lessOrEqualExp(property, value));
        return this;
    }

    /**
     * 昇順でソートする
     * @param sortPathSpec
     * @return
     */
    public Executor<T1> asc(String sortPathSpec) {
        return parse().asc(sortPathSpec);
    }
    /**
     * 降順でソートする
     * @param sortPathSpec
     * @return
     */
    public Executor<T1> desc(String sortPathSpec) {
        return parse().desc(sortPathSpec);
    }
    /**
     * エンティティのリストを返す
     * @param db
     * @return
     */
    public List<T1> getList(ObjectContext db) {
        return parse().getList(db);
    }
    /**
     * limitを設定する
     * @param limit
     * @return
     */
    public Executor<T1> limit(int limit) {
        return parse().limit(limit);
    }
    /**
     * offsetを設定する
     * @param offset
     * @return
     */
    public Executor<T1> offste(int offset) {
        return parse().offset(offset);
    }

    /**
     * joinのプリフェッチを設定する
     * @param prefetchPath
     * @return
     */
    public Executor<T1> join(String prefetchPath){
        return parse().join(prefetchPath);
    }
    public static class Executor<T3> {
        private final Q<T3> exQuery;
        private final SelectQuery query;
        private Executor(Q<T3> q) {
            exQuery = q;
            query = new SelectQuery(q.clazz, q.exp);
        }
        /**
         * 昇順でソートする
         * @param sortPathSpec
         * @return
         */
        public Executor<T3> asc(String sortPathSpec) {
            query.addOrdering(sortPathSpec, SortOrder.ASCENDING);
            return this;
        }
        /**
         * 降順でソートする
         * @param sortPathSpec
         * @return
         */
        public Executor<T3> desc(String sortPathSpec) {
            query.addOrdering(sortPathSpec, SortOrder.DESCENDING);
            return this;
        }

        /**
         * エンティティのリストを返す
         * @param db
         * @return
         */
        @SuppressWarnings("unchecked")
        public List<T3> getList(ObjectContext db){
            return db.performQuery(query);
        }

        /**
         * joinのプリフェッチを設定する
         * @param prefetchPath
         * @return
         */
        public Executor<T3> join(String prefetchPath){
            query.addPrefetch(prefetchPath);
            return this;
        }
        /**
         * limitを設定する
         * @param limit
         * @return
         */
        public Executor<T3> limit(int limit){
            query.setFetchLimit(limit);
            return this;
        }
        /**
         * offsetを設定する
         * @param offset
         * @return
         */
        public Executor<T3> offset(int offset){
            query.setFetchOffset(offset);
            return this;
        }

        public long getCountLong(ObjectContext db) {
            DataContext dc = (DataContext)db;
            return CountHelper.count(dc, query);
        }
        public Q<T3> getQuery() {
            return exQuery;
        }
        public T3 getSingle(ObjectContext db) {
            List<T3> list = getList(db);
            if (list.isEmpty())
                return null;
            else if (list.size() == 1)
                return list.get(0);

            throw new RuntimeException("Too many result.");
        }
    }

    public long getCountLong(ObjectContext db) {
        return parse().getCountLong(db);
    }

    /**
     * 行数を返す
     * @param db
     * @return
     */
    public int getCount(ObjectContext db) {
        return (int) getCountLong(db);
    }
}

一応流れるようなインターフェースにしたつもり。


他にもCayenne用のDataProviderなどを作る。

public abstract class CayenneDataProvider<T extends Persistent> implements IDataProvider<T> {
    protected abstract Q.Executor<T> getQuery();

    @Override
    public void detach() {
    }
    @Override
    public Iterator<? extends T> iterator(int first, int count) {
        return getQuery().limit(count).offset(first).getList(WebApp.getDb()).iterator();
    }
    @Override
    public int size() {
        return (int) getQuery().getQuery().getCountLong(WebApp.getDb());
    }
    @Override
    public IModel<T> model(T object) {
        return CayenneModel.of(object);
    }
}

ModelUtil.java

public class CayenneModel extends Model<T> {
    public static <T extends Persistent> IModel<T> of(T object){
        return new CayenneModel<T>(object);
    }
    public static <T extends Persistent> PropertyModel<T> of(Object modelObject, String expression){
        return new CayennePropertyModel<T>(modelObject, expression);
    }

    public static <T extends Persistent> ListModel<T> of() {
        return new CayenneListModel<T>();
    }
    public static <T extends Persistent> ListModel<T> of(List<T> list) {
        return new CayenneListModel<T>(list);
    }

    ...

各ページのS2JDBCのEntityクラスをCayenneのPersistentクラスに変える

ひたすら変更。

CompoundPropertyModelなどを使っている場合は、"item.name" と書いてあれば item.nameフィールドまたはitem.getName()メソッドが呼ばれるのでテンプレートのHTMLの変更は少ない。


モデルのデータ格納先をPersistentクラスにしている場合の注意点が二つ。

  • データ更新の場合、FormのonSubmitでバリデーションしてはいけない。
  • データ登録の場合、Persistentクラスはnewで生成し、onSubmitの中でObjectContext#registerNewObjectする。

Persistentクラスのデータを変更する=commitChanges時に反映される、ということに気をつける。


例えば、onSubmitの中でエラーがあるからといってcommitもrollbackもせずにいると、変更された値はセッションに保持され続けるので他のページに遷移した後に別の処理でcommitChangesを呼び出したときにエラーが出る。




他、ページやコンポーネントのフィールドとしてPersistentを保持した場合、セッションから復元されたときにデータが消える。

具体的にはHollowの状態になる。

http://cayenne.195.n3.nabble.com/Conditions-when-PersistenceState-gets-quot-hollow-quot-td691334.html


今のところ

public class CayenneLoader {
    @SuppressWarnings("unchecked")
    public static <T extends Persistent> T load(T obj) {
        if (obj == null)
            return null;
        if (obj.getPersistenceState() == PersistenceState.HOLLOW) {
            ObjectContext db = WebApp.getDb();
            obj = (T) DataObjectUtils.objectForPK(db, obj.getObjectId());
        }
        return obj;
    }
}


public class CayenneModel<T extends Persistent> extends Model<T> {
    public CayenneModel(T object) {
        super(object);
    }

    @Override
    public T getObject() {
        return CayenneLoader.load(super.getObject());
    }

    @Override
    public void setObject(T object) {
        super.setObject(object);
    }
}

このようなモデルを作って回避している。

ちょっと不安が残る。※2


S2Container関連の設定を削除する

mavenの依存関係やS2JDBC関連クラス、diconファイルを削除する。


S2WicketFilterがなくなるのでReloadingWicketFilterを継承したフィルタを作る。

public class AppWicketFilter extends ReloadingWicketFilter {
    private final Logger logger = LoggerFactory.getLogger(AppWicketFilter.class);

    static
    {
        ReloadingClassLoader.includePattern("com.example.project.wicket.**");
        ReloadingClassLoader.excludePattern("com.example.project.wicket.session.**");
    }
    @Override
    public void init(boolean isServlet, FilterConfig filterConfig) throws ServletException {
        // 再読み込み時にアプリケーションが破棄されるようにする
        destroy();

        for (URL str : ReloadingClassLoader.getLocations()) {
            logger.info("[classpath] {}", str);
        }
        for (String str : ReloadingClassLoader.getPatterns()) {
            logger.info("[pattern] {}", str);
        }

        ClassLoader previousClassLoader =
            Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(getClassLoader());
        super.init(isServlet, filterConfig);
        Thread.currentThread().setContextClassLoader(previousClassLoader);
    }
}

以上で移行終了。

Cayenneの使い方を調べつつ、22テーブル50ページの書き換えに四日かかった。


感想

移行初期段階はCayenne便利!とか思ってたけど、全部書き換えてみて ※2のhollowの状態になるのが結構キツい。

それに加えてJoinのselect文もS2JDBCより沢山発行するわけだから、selectしすぎじゃないか?と思う。


構成はGuiceを使うことによってだいぶ単純になった。

Webサーバーの再起動も若干早い。


現状をまとめると

  • メリット
    • joinを書かなくて良い
    • 構成がシンプル
    • ReloadingWicketFilterがSessionやEventDispatcherなど一部のクラス以外ほとんどで使える
    • 楽観的排他制御が強力
      • DBは変更の必要なし
  • デメリット
    • Hollowの扱いが面倒
      • Persistentオブジェクトをページやコンポーネントに持てない。必ずModelを使う必要がある。
        • 本来はそうすべきなのかも
    • select文発行が多い
      • outer join でprefetchできない?(あとで調べる)
  • 分からないこと
    • PkGeneratorってどうやって取得するの?
      • S2JDBCのinsert → ID取得 → commit(サービスクラスを抜ける) or rollback(例外throw) の方が楽かもしれない

メリットが当初の想定より少ない…。

※1の方法でも良いような気がしている。

結局自動的にjoinしてくれるより自分でjoinした方が直感的で分かりやすいのかも。

また考える。

2011-08-15

[] ポート80番でGlassFishを動かす

GlassFishに限らずポート1024未満でサーバーを動かす場合に authbind が使えるようだ。

http://jars.de/english/how-to-set-up-glassfish-2-on-debian-or-ubuntu

http://linuxchef.blogspot.com/2008/11/tomcat-glassfish-auf-port-80.html

apt-get install authbind
touch /etc/authbind/byport/80
chmod 500 /etc/authbind/byport/80
chown glassfish /etc/authbind/byport/80
http://jars.de/english/how-to-set-up-glassfish-2-on-debian-or-ubuntu

それに加えて、authbindがIPv6に対応していないので glassfish/domains/domain1/config/domain.xml

<jvm-options>-Djava.net.preferIPv4Stack=true</jvm-options>

オプションを追加してIPv6を無効にする。


起動は

. /lib/lsb/init-functions

CMD=/opt/glassfish3/bin/asadmin

case "$1" in
        start)
        log_daemon_msg "Starting application server" "glassfishv3prelude"
        sudo -u appserv authbind --deep $CMD start-domain

こんな感じのスクリプトをinit.dに置いた。検索したらいくつかサンプルが出てくるのでその辺を適当に。