Grails の Validation で 自作Constraints を使う(Github経由)
(※ この記事ではGrails 2.3.6を使用しています。)
以下の3ステップで自作した制約(Constraint)をGrailsのバリデーションで使用できるようになります。
今回はサンプルとして、日付文字列の正当性チェックを行う制約を作成しました。
サンプルはGithubにて公開しています。[ my-validation-plugin ]
1. プラグイン・プロジェクトで制約を定義したクラスを作成する
Grailsはアプリケーション起動時に制約クラスを読み込みます。
プラグイン・プロジェクトにはアプリケーション起動時に、任意の処理を読み込ませる仕組みが用意されています。
選択している部分が今回のメインとなるソースです。
が、説明は省略します。めんどい
下記のサイトが非常に参考になります。
しおしおの雑記帳
Grailsでカスタムバリデーションを作ってみた
http://siosio.hatenablog.com/entry/2012/07/07/135942
Tips
- 制約クラスはsrcフォルダの下に配備するのがお作法みたいです。
- org.codehaus.groovy.grails.validation.AbstractConstraint を継承します。
- 実装についてはGrails標準の制約クラスが参考になります。
https://github.com/grails/grails-core/tree/master/grails-core/src/main/groovy/org/codehaus/groovy/grails/validation
2. GithubにMavenレポジトリを作成し、作成したプラグインを配信可能にする
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アンチパターン ~勝手な約束~
本日ちょっと痛い目を見たので、後続のため残しておきます。
開発中のアプリは 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 で 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
}
}
// -------------------------------------------------------------------
もっと良い方法がありそうな気もしますが、とりあえずの目標は達成!