backbone.js モデルのバリデーション

backbone.jsでモデルのバリデーションを行うにはモデルでvalidateメソッドを実装します。
下のSourceにあるサンプルだと1つのエラーで1つずつAlertを表示するような形でしたが、複数の入力フィールドがあるのでまとめてエラーを出したいと思います。

var MyModel = Backbone.Model.extend({
	validate: function(attr){
		var error_len = 0;
		var errors = {};
		/*
                いろいろバリデーション
                */

		if( !attr.kana.match.match(/^[\u30A0-\u30FF]+$/) ){
			errors["kana"] = "全角カナで入力してね";
		}
		if ( attr.email && !attr.email.match(/^[A-Za-z0-9]+[\w-]+@[\w\.-]+\.\w{1,}$/) ){
			errors["email"] =  "Emailアドレスがおかしいよ";
		}
		
		// Javascriptの連想配列はlengthが使えないので
		for(key in errors){
			error_len++;
		}
		
		if(error_len > 0){
			return errors;
		}
	}
});

//view

var MyView = Backbone.View.extend({

	initialize: function(){
		this.model.on("error", this.showError, this);
	},
	showError:  function(model, errors){
		var msg = "";
		for(key in errors){
			$("#"+key).addClass("error");
			msg += " - "+ errors[key] + "\n";
		}
		alert(msg);
		for(key in errors){
			$("#"+key).removeClass("error");
		}
		
	},

エラーがあった場合は、フィールド毎のエラーメッセージをalert内に表示します。
エラーメッセージが表示されている間は、そのフィールドの入力テキストのボーダーを変更するcssを使っています。

Source:
http://backbonejs.org/#Model-validate
http://stackoverflow.com/questions/10334923/backbone-model-validation-standards-is-it-wrong-to-do-it-this-way

regexp:
http://befine.jugem.jp/?eid=29
http://programmer-toy-box.sblo.jp/article/16431782.html
http://phpjavascriptroom.com/?t=js&p=arrayobject#a_length

PlayFramework Listを使ってIN句でqueryをかける

idとtypeでqueryをかけて結果を取得するような場合、bindを使ってListの内容をそのままIN句に使うことができます。
String型のリストなら自動でシングルクォートを付けて、SQLエスケープもやってくれるようです。

List<MyModel> list = new ArrayList();

List<Long> idList = new ArrayList();
// idListにIDを追加
List<String> typeList = new ArrayList();
// typeListに抽出するTypeを追加
list = MyModel.find("id IN (:idList) AND typeStr IN (:typeList)").bind("idList", idList).bind("typeList", typeList).fetch();
// id IN (1, 2, 3) AND typeStr IN ('foo', 'bar', 'hoge');

return list;

Source:
http://www.playframework.org/documentation/1.2/guide6
http://stackoverflow.com/questions/7205955/is-my-sql-query-prone-to-sql-injection-or-other-attacks

backbone.js モデル保存後のCallback

Backbone.jsでモデル保存後のcallbackを設定するには、1つ目の引数に保存するプロパティ値を渡して、2番目の引数にsuccessとerrorを渡します。
success、errorはどちらか一方だけでもOKかと思います。

this.model.save(
	{var1: "test", var2: "hoge"},
	{
		success: function(model, resp){
			alert("saved!");
		},
		
		error: function(model, resp){
			alert("failed to save...");
		}
	}
);

PlayFramework ManyToManyのリレーション

// 親モデル
@Entity
public class Parent extends Model{
	@ManyToMany(mappedBy="parents", cascade=CascadeType.ALL) 
	@OrderBy("birthday")
	public List<Child> children;
}

// 子モデル
@Entity
public class Child extends Model{
	@ManyToMany
	public List<Parent> parents;
}

テスト用にYAMLで定義するには、先に親モデルを追加してから子モデルに親を関連付けます

Parent(parent1)
  name: John

Child(child1)
  name: Dana
  parents: [parent1]
  

Source:
http://docs.oracle.com/javaee/6/api/javax/persistence/CascadeType.html
http://docs.jboss.org/ejb3/app-server/tutorial/relationships/relationships.html
http://www.playframework.org/documentation/1.2/yaml

http.maxParams

大きめのデータをPOSTで飛ばしているときに、コンソールに警告が出ていました。

Number of request parameters xxxx is higher than maximum of
1000, aborting. Can be configured using 'http.maxParams'

application.confに次の行を追加すると解決しました。
# 別に2000じゃなくてもいいです。
http.maxParams = 2000

この辺にヒントがありそうです。
https://github.com/eamelink/play/blob/123b5ce46b752169a5e2299e6f814acdb49dd396/framework/src/play/data/parsing/UrlEncodedParser.java

PlayFrameworkでセッションID、DBのデータが必要なFunctionalTest

たぶんFunctionalTestの使い方が間違ってますが一応書こうと思います。

まずセッションIDについて。
開発中のアプリで、セッションにモデルのIDを保存しておくメソッドと、そのIDを後から使うメソッドがあります。
簡略化して書くと下のようなコードです。実行時には問題ないのですが、FunctionalTestだと、同じテストの中でfooとbarを呼び出していても、SessionIdがリクエストごとに違うので、barを呼び出したときには"[sessionId]-mymodel"はNullになってしまいます。

	public static void foo(){
		MyModel model = new MyModel();
		// いろいろ処理
		model.save();
		
		// セッション(クッキー)にIDを保存しておく
		String sessionId = session.getId()+"-mymodel";
		session.put(sessionId, model.id);
		render();
	}
	
	public static void bar(){
		Long myModelId = Long.parseLong(session.get(session.getId()+"-mymodel")); //これがNullになる
		// いろいろな処理
		render();
	}

fooとbarの間で同じセッションIDを使わせるために、こんな形にしました。

    	HashMap params = new HashMap();
    	params.put("param1", "test1");
    	params.put("param2", "test2");
    	Response response = POST("/foo", params);
    	
	Request req = newRequest();
	req.url = "/bar"; // urlを指定しないとだめらしいです。
	req.path = "/bar";
	req.cookies.put("PLAY_SESSION", response.cookies.get("PLAY_SESSION")); // fooと同じセッションを使う
        response = POST(req, "/bar");

他にもいろいろと試してみましたが、結局どれもうまくいかず、上の方法に落ち着きました。
ちなみに下の方法だとテストが1つだけならうまくいくものの、別の同じようなCookieを使うテストを追加したとたんに java.util.concurrent.ExecutionException: play.exceptions.JavaExecutionExceptionが発生しました。

	Request req = newRequest();
	req.url = "/bar";
	req.path = "/bar";
	Cookie c1 = new Cookie();
	c1.name = "PLAY_SESSION"; 
	c1.value = "10d4f52bc60ddf2c65769b4b73b3ffa9fae988eb-%00___ID%3A7fcac13a-d06f-4d3a-b417-827c0c24a5d8%00%007fcac13a-d06f-4d3a-b417-827c0c24a5d8-mymodel%3A"+myModel.id+"%00";
	req.cookies.put("PLAY_SESSION", c1);

それからDB関係の操作でもいろいろとFunctionalTestを作る中でハマッたのでメモします。
テストの中でモデルを作成したり更新したりして、Controllerのアクションを呼び出して結果をAssertする、というようなことをすると、モデルのデータが意図したものになっていなかったり、「play.exceptions.JPAException has been caught, The JPA context is not initialized」などのエラーが出ました。

間違ってたらツッコミをお願いしたいのですが、これはたぶんJPAがFunctionalTestからは独立して存在しているからなのだと思います。FunctionalTestの中でテスト用のモデルを作ったり、Fixtureを使っていても、それが意図した時点でDBにきちんと反映されているとは限らない、とうことなのだと思います。FunctionalTestの中でJPAを使うときには、明示的にJPA ContextをInitializeして、いつトランザクションを開始するのか、いつ終了するのか、といったことを指定する必要があるのだと思います。

具体的にはメソッドの中でモデルをいじってどうのこうのする場合は、下の記述を入れないとだめでした。

	@Before
	public void setUp() throws Exception {
	    new Job() {
	        @Override
	        public void doJob() throws Exception {
	        	Fixtures.deleteAllModels(); 
	        	Fixtures.loadModels("data.yml");
	        }
	    }.now().get();
	}
	
	@Test
	public void someTest(){
		// 引数はRollbackするかどうか
		JPAPlugin.startTx(false); 
		// このあたりでDBへの処理など
		// テスト用の送信Requestを作る
		Request req = newRequest();
		req.url = "/save"; 
		req.path = "/save";
    		req.params.put("param1", "Test");
    		req.params.put("param2", "Test");
		// Controllerのアクションを呼び出し
		POST(req, "/save");
		// DBからUpdateされた結果がほしいときにはclearを呼び出す(?)
		JPA.em().clear(); 
		MyModel m = (MyModel)MyModel.findById(id);
		// トランザクションを終了
		JPAPlugin.closeTx(false);
		
		assertEquals("Success!", m.resultText);
	}

UnitTestの時に「JPA context is not initialized」エラーが出ないのは、モデルには@Entityアノテーションがあるので、最初の呼び出し時にJPAが自動でInitializeされているからだと思います。試しにモデルのUnitTestの中でJPAPlugin.startTx(false)、JPAPlugin.closeTx(false)を記述すると、それ以降のテストではJPA contetxt is not Initializedのエラーが出ます。

それからOneToManyなどで子としてCollectionを持っている場合は、モデルを取得した後にCollection
にアクセスしようとすると「A org.hibernate.LazyInitializationException has been caught, failed to lazily initialize a collection of role: models.MyModel.children no session or session was closed」というエラーが発生する場合があります。子になっているCollectionはiteratorにアクセスがあるまでは、実際にはDBから取得されないようです。なのでmodel.children.iterator()を、Collectionを使う前に呼び出しておくと、ちゃんとロードされます。これはトランザクションの終了前(JPAPlugin.closeTxの前)に行う必要があります。

いろいろあやふやな部分がありますが、いまの理解ではこんなところです。

Sources:
http://openejb.apache.org/jpa-concepts.html
http://www.playframework.org/documentation/api/1.2.5/play/db/jpa/JPAPlugin.html
http://docs.oracle.com/javaee/5/api/javax/persistence/EntityManager.html
http://www.playframework.org/documentation/1.2.4/model#stateless
http://stackoverflow.com/questions/7593802/functional-test-in-playframework-fails-when-adding-items-to-cart

Jasmine+Sinon.jsを使ってAlertが正しいテキストで表示されることを確認する

confirmでも同じように使えました。

// Spec
describe("MyAlertTest", function(){
	beforeEach(function(){
		setFixtures(sandbox());
		$("#sandbox").append("<button id='testBtn' value='test' />");
		this.alertSpy = sinon.stub(window, "alert").returns(true);
	});
	
	//これなしだとTypeError: Attempted to wrap alert which is already wrappedエラーが発生
	afterEach(function(){
		window.alert.restore();
	});
	
	it("should tell you it is clicked", function(){
		$("#testBtn").click();
		expect(this.alertSpy).toHaveBeenCalledWith("it is clicked!");
	});
});