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!"); }); });