Grails の Validation で 自作Constraints を使う(Github経由)

(※ この記事ではGrails 2.3.6を使用しています。)

 

以下の3ステップで自作した制約(Constraint)をGrailsのバリデーションで使用できるようになります。

  1. プラグイン・プロジェクトで制約を定義したクラスを作成する
  2. GithubMavenレポジトリを作成し、作成したプラグインを配信可能にする
  3. Grailsアプリで作成したプラグインの読み込み設定をする

 

今回はサンプルとして、日付文字列の正当性チェックを行う制約を作成しました。
サンプルはGithubにて公開しています。[ my-validation-plugin ]

 

1. プラグイン・プロジェクトで制約を定義したクラスを作成する

Grailsはアプリケーション起動時に制約クラスを読み込みます。
プラグイン・プロジェクトにはアプリケーション起動時に、任意の処理を読み込ませる仕組みが用意されています。

f:id:onBass_naga:20140227224946p:plain

選択している部分が今回のメインとなるソースです。
が、説明は省略します。めんどい

 

下記のサイトが非常に参考になります。


しおしおの雑記帳
Grailsでカスタムバリデーションを作ってみた
http://siosio.hatenablog.com/entry/2012/07/07/135942


 

Tips

 

2. GithubMavenレポジトリを作成し、作成したプラグインを配信可能にする

Githubに配信用リポジトリを用意した後、ローカルにアップロード用のリポジトリを作成します。

 

下記のサイトが非常に参考になります。


bluepapa32’s Java Blog
GitHub を Maven 公開リポジトリにする (Gradle 編)

Literal Ice:
GrailsのプラグインリポジトリとしてGithubを使う


 
なお、私はMacにて以下の設定をしました。

settings.groovy

ls.release.scm.enabled = false

grails.project.repos.default = "localRelease"
grails.project.repos.localRelease.url = "file:///Users/username/repos/mvn/"

 

3. Grailsアプリで作成したプラグインの読み込み設定をする

以下の設定を追加すればOKです。

BuildConfig.groovy

repositories {
    mavenRepo "http://onbass-naga.github.io/mvn/"
} plugins {
    compile "com.example:my-validation-plugin:0.1"
}

使い方 

static constraints = {
    deadline date:true
}

 以上です。

 

(記事編集中にブラウザがクラッシュすると、やる気がなくなりますね〜。)

Grails の CommandObject で Session を使う

RESTな時代に、使う機会はほとんどない気もしますが、メモを残します。

 (Grails 2.3.5 で動作確認)

import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder as RCH

 

class SomeCommand {
    String value
    static constraints = {
        value(validator: {
                    if (it != RCH.getRequestAttributes().getAttribute(MY_KEY, RequestAttributes.SCOPE_SESSION))
                        return ['invalid.value']
        })
    }
}

上記は画面入力した値と、Sessionに保持していた値との凸合わせに使用した例です。

 

Java のプライベート・メソッドをテストする

Javaで書かれたレガシーコードを改修するお仕事では、リフレクションを使ってのテストは避けられないと思います。
リフレクションをそのまま使うとテストコードが煩雑になりすぎるので、簡単なラッパーを作成しました。

どの現場でも毎度同じようなコードを書いてる気がしますが、意外とだれも用意してくれないんですよね。(^-^;

(Github にもコミットしています。レポジトリ

なぜか配列の[]が記事から消えるので、適当に補完してください!編集画面では表示されてるのに。。。

 

MethodInvoker.java

package util;

import java.lang.reflect.Method;

/**
 * 可視性を意識せずにメソッドを実行するためのユーティリティ・クラスです.
 * @author naga
 */
public class MethodInvoker {

    private Class clazz;
    private Object targetInstance;
    private String methodName;
    private Object argValues;
    private Class argTypes;

    /**
     * メソッド実行対象クラスのインスタンスと実行メソッド名を引数とするコンストラクタ.
     * @param instance メソッド実行対象クラスのインスタンス nullの場合の挙動は保証しません
     * @param methodName 実行メソッド名 nullの場合の挙動は保証しません
     */
    public MethodInvoker(Object instance, String methodName) {
        this.targetInstance = instance;
        this.clazz = instance.getClass();
        this.methodName = methodName;
    }

    /**
     * 実行対象メソッドが複数の引数を必要とする場合に使用します.
     * @param values 実行時の引数の実値 実行対象メソッドのシグニチャに定義されている順番で配列にセットしてください
     * @param types 実行時の引数の型 リフレクションにより対象メソッドを特定するために指定が必要です。
     * 引数の実値に対応した順番で配列にセットしてください。
     */
    public void args(Object values, Class types) {
        if (values == null || types == null
                || values.length != types.length) {
            throw new IllegalArgumentException();
        }

        this.argValues = values;
        this.argTypes = types;
    }

    /**
     * 実行対象メソッドが引数を必要とする場合に使用します.
     * @param value 実行時の引数の実値
     * @param type 実行時の引数の型 リフレクションにより対象メソッドを特定するために指定が必要です。
     */
    public void arg(Object value, Class type) {
        if (type == null) {
            throw new IllegalArgumentException();
        }

        this.argValues = new Object {value};
        this.argTypes = new Class { type };
    }

    /**
     * メソッドを実行します.
     * @return メソッドに戻り値がある場合、{@code Object}型で返却します.
     * 戻り値の型を指定したい場合、{@link #invokeAndReturn(Class)}を使用してください。
     * @throws RuntimeException 例外はすべて{@link RuntimeException}にラップして投げます
     */
    @SuppressWarnings("unchecked")
    public Object invoke() {
        try {
            if (hasArgs()) {
                Method method = clazz.getDeclaredMethod(methodName, argTypes);
                method.setAccessible(true);
                return method.invoke(targetInstance, argValues);
            } else {
                Method method = clazz.getDeclaredMethod(methodName);
                method.setAccessible(true);
                return method.invoke(targetInstance);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * メソッド実行後、戻り値を指定した型にキャストし返却します.
     * @param type 戻り値の型
     * @param <E> 型パラメータ
     * @return 指定した型にキャストした戻り値
     * @throws RuntimeException 例外はすべて{@link RuntimeException}にラップして投げます
     */
    @SuppressWarnings("unchecked")
    public <E> E invokeAndReturn(Class<E> type) {
        return (E) this.invoke();
    }

    private boolean hasArgs() {
        return argValues != null;
    }
}

使用方法はテストクラスを参考にしてください。 

MethodInvokerTest.java (一部抜粋)

@RunWith(Suite.class)
@Suite.SuiteClasses({
        MethodInvokerTest.正常系テスト.class,
        MethodInvokerTest.異常系テスト.class
})
public class MethodInvokerTest {

    public static class 正常系テスト {

        @Test public void 引数なしメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi !!"));
        }

        @Test public void 引数ありメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            method.arg("Tom", String.class);
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi, Tom !!"));
        }

        @Test public void 複数引数ありメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            method.args(new Object {"Tom", 3}, new Class {String.class, int.class});
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi, Tom3 !!"));
        }
    }

    public static class 異常系テスト {

        @Test(expected = Exception.class)
        public void テスト対象メソッドが存在しない場合は例外を投げる() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "non Exists Method");
            method.invoke();
        }
    }
}

本当はこんなもの使わなくて済むのが一番良いのですが、ね。

おれオレ SQLアンチパターン ~勝手な約束~

ここ数日、SQLアンチパターンを読み返していました。

本日ちょっと痛い目を見たので、後続のため残しておきます。

 

おれオレ SQLアンチパターン ~勝手な約束~

開発中のアプリは Oracle の Sequence により採番した値をIDとして使用している。

問題となった画面は、とあるテーブルの値をレコード作成順に表示する仕様になっている。

本来であれば登録日にてソートをかけるべきだが、ちょっとした理由により登録日を使用することが出来なくなったため、IDをソート順に使用した。。。

 

知っていれば当たり前の話なんですが、Oracle の Sequence は設定により登録順とシーケンスの順番が一致しないということが起こります。

(create sequence するときに、order オプション付けないと順番が保障されなくなります)

そう、画面に表示したレコードは想定した順番にはなっていなかったのです。

 

今回は思い込みで設計バグを埋め込んでしまったので、「勝手な約束」というタイトルをつけてみました。

IDに一意性以外の役割を持たすのが問題な気もするので、「多目的ID」でも良かった気もしますね。

 

 

若手の皆様は私の屍を越えて行ってください。

恥ずかしいわ~~~

 

Chrome で ime-mode:inactive のような挙動を実現する。

汚いやり方です。

HTML5のinput type="tel" に一時的にタグを書き換えます。。。。

$(function() {
    $('input.alphanumeric').on({
        'focus': function(){ $(this).attr('type', 'tel'); },
        'blur': function(){ $(this).attr('type', 'text'); }
    });
});

半角数値だけを入力するなら、input type="number" でも良いですが、半角数値以外を入力した場合はフォーカスアウトで入力内容がクリアされるのが難点。
text に戻してるのは、良心の葛藤による無駄なあがきw

古いバージョンのJQueryだと、#attrでtypeの変更はできないので注意してください。
このソースは1.10.2で動作確認しています。

また、WindowsXPではtypeがnumber・telでも半角入力に切り替わってくれないので、あわせて注意してください。

Grails で GORM と同一の Connection を使用する 2

前回の記事へいただいたコメントの方法で書きなおしてみました。

サンプルソースはGithubに公開しています。

 // -------------------------------------------------------------------------------

class CustomerService {
    def dataSource

    def update() {
        def sqlManager = createInstance(dataSource)
        def count = sqlManager.getCount("com/area_b/samples/select.sql")
        sqlManager.executeUpdate("com/area_b/samples/insert.sql")
        def count2 = sqlManager.getCount("com/area_b/samples/select.sql")
        def count3 = Customer.count
        print(count + ":" + count2 + ":" + count3)
    }

    def createInstance(dataSource) {
        def connectionProvider = new SpringConnectionProvider(
                transactionManager: new DataSourceTransactionManager(dataSource)
        )
        new SqlManagerImpl(
                connectionProvider: connectionProvider,
                dialect: new MySQLDialect()
        )
    }
}

 // -------------------------------------------------------------------------------

Service のフィールドにdataSourceを定義して、DIされた接続を使用。

Connection 取り出すためにゴニョゴニョしていた部分がスッキリしましたね!

※ resources.groovyに定義していた dataSource 等の記述は削除しています。

 

 

// 2013/06/10 追記 --------------------------------------------------------

@nobeans さんが、さらにスマートな方法でサンプルコードをリファクタリングしています。

はてなブログに投稿しました GrailsからMirageを使うサンプルがあったので試してみた - 豆無日記 http://t.co/w9GPxo0Uww
 
現場で知ってる人がいない技術について、コードでいろいろ教えてもらえるのは非常にありがたいです!
 

Grails で GORM と同一の Connection を使用する

Grails で GORM に使用している Connection を、GORM以外で使用する方法を記します。

サンプルソースはGithubに公開しています。

 

// 2013/06/08 追記 ---------------------------------------

コメントにいただいた方法で書きなおしてみました。

// --------------------------------------------------------

 

■何が問題?

以下のソースの場合、DIしたSqlManager の Connection と、GORMで使用している Connection が異なるため、count2(DI)とcount3(GORM)が異なる値となります。

insert.sql がコミットされるのは#update が終わってからなので、count3はレコード追加前の件数となります。

// -------------------------------------------------------------------  

class CustomerService {

    SqlManager sqlManager

    def update() {
        int count = sqlManager.getCount("com/area_b/samples/select.sql")

        sqlManager.executeUpdate("com/area_b/samples/insert.sql")

        int count2 = sqlManager.getCount("com/area_b/samples/select.sql")
        int count3 = Customer.count
    }
}

// -------------------------------------------------------------------  

ちょっと困ったちゃんですよね。

 

■GORMで使用しているConnectionを取り出す。

以下のような感じで、取り出すことができます。

// -------------------------------------------------------------------  

import jp.sf.amateras.mirage.SqlManager
import jp.sf.amateras.mirage.SqlManagerImpl
import jp.sf.amateras.mirage.dialect.Dialect
import jp.sf.amateras.mirage.provider.ConnectionProvider
import jp.sf.amateras.mirage.provider.DefaultConnectionProvider
import org.hibernate.SessionFactory
import org.hibernate.connection.ConnectionProvider as SCP
import org.hibernate.engine.SessionFactoryImplementor

class CustomerService {

    MySQLDialect dialect
    def sessionFactory

    def update() {

        SqlManager manager = createInstance(sessionFactory, dialect)

        int count = manager.getCount("com/area_b/samples/select.sql")

        manager.executeUpdate("com/area_b/samples/insert.sql")

        int count2 = manager.getCount("com/area_b/samples/select.sql")
        int count3 = Customer.count
    }

    SqlManager createInstance(SessionFactory sessionFactory, Dialect dialect) {
        SessionFactoryImplementor sfi = (SessionFactoryImplementor) sessionFactory
        SCP scp = sfi.getConnectionProvider()
        ConnectionProvider connectionProvider = new DefaultConnectionProvider()
        connectionProvider.connection = scp.connection
        SqlManager manager = new SqlManagerImpl()
        manager.connectionProvider = connectionProvider
        manager.dialect = dialect
        return manager
    }
}

// -------------------------------------------------------------------  

もっと良い方法がありそうな気もしますが、とりあえずの目標は達成!