Jersey(MOXy)でJSONバインドをカスタマイズする

JSONをリクエストで投げてJersey(MOXy)でPOJOとして受ける際に、Integerのフィールドに空文字が来ると「0」に変換される。
0じゃなくてnullで受けたい、というように変換をカスタマイズした場合はXmlAdapterを継承したadapterを作る。

参考:JAXBのXmlAdapterを自作する - Programming Studio

JerseyというかJAXBの仕様らしい。なのでアノテーションやクラスがXml・・・となってJSONで使うには違和感があるがちゃんと動作する。

  • marshal():POJO => JSON(XML)の変換処理
  • unmarshal():JSON(XML) => POJOの変換処理 をそれぞれ実装する。

で、@XmlJavaTypeAdapterでadapterクラスを指定する。
@XmlJavaTypeAdapterは@Target(value={PACKAGE,FIELD,METHOD,TYPE,PARAMETER})となっているので、指定する箇所で有効範囲が変わる。

いちおう試して動いたが、結局Backbone.stickitのonSetで空文字 => null変換して済ませちゃったのは内緒の話。

BeanManagerを取得する on Weld, Tomcat

Weld 2.3.1 + Jersey2.22.1
Tomcat8

発端はEntityに登録者IDや更新者IDを埋め込みたかったが、Entityで@SessionScopedなBeanを@Injectできなかったから。
EntityListener使ってみたり紆余曲折あったが、時間が無いので経過は省くとして結局BeanManagerを取得して直接Beanを取ってくることに。

参考その1:CDIのBeanManagerを使う - CLOVER

InitialContext ic = new InitialContext();
BeanManager bm = (BeanManager) ic.lookup("java:comp/BeanManager");
Set<Bean<?>> beans = bm.getBeans(SessionDto.class);
Bean<?> bean = bm.resolve(beans);
SessionDto sessionDto = (SessionDto) bm.getReference(bean, SessionDto.class, bm.createCreationalContext(bean));
// SessionDtoが@SessionScopedなクラス

これではlookup()するところでNamingExceptionになってできず。
おそらくTomcatで動かしているからだと思われる。

参考その2:jsf - BeanManager on Apache Tomcat 7.0.47 cannot create resource instance - Stack Overflow
Tomcatの場合、"java:comp/BeanManager"ではなく"java:comp/env/BeanManager"でlookupするとある。
確かにTomcatでDataSourceをlookupする時は"java:comp/env/・・・"で見に行く。
あとcontext.xmlにもResourceを追加する。

 <Resource name="BeanManager"
        auth="Container"
        type="javax.enterprise.inject.spi.BeanManager"
        factory="org.jboss.weld.resources.ManagerObjectFactory" />

で、どうにかSessionDtoが取れた。
紆余曲折時間:約2時間

backbone.validationメモ

参考:thedersen/backbone.validation · GitHub
backbone 1.2.0
backbone.validation 0.11.5

backbone自体にvalidation機能はあるが、処理をいちいち書かないといけないし複雑化すると後から読むのも大変。
なのでbackbone.validationプラグインを使ってスッキリさせる。

Model

バリデーターはModelに「validation」プロパティで指定する。
key=Modelのkey、 value=バリデーター。
処理を記述する場合はfunctionを指定。戻り値にエラーメッセージを返す。

var model = Backbone.Model.extend({
  validation: {
    name: function(value, attr, computedState) {
      if (value == '名無しの権兵衛') return '名前不正';
    }
  }
});

もしくは別でfunctionを作っておいてfunction名を指定。

  validation: {
    name: 'validateName'
  },
  validateName: function() {(略)}

よくあるバリデーターが用意されている。

  validation: {
    name: { required: true }, // 必須
    agreement: { acceptance: true }, // 許諾(true or 'true'のみ可。「同意しますか?」チェックボックスなんかで)
    age: { min: 1 }, // 最小(以上)
    age2: { max: 100 }, // 最大(以下)
    zipcode: { length: 7 }, // 桁数
    password: { minLength: 8 }, // 最小桁数
    password2: { maxLength: 256 }, // 最大桁数
    password3: { rangeLength: [8, 256] }, // 桁数範囲
    language: { oneOf: ['jp', 'en', 'others'] }, // どれかに該当
    passwordRe: { equalTo: 'password' }, // 他のattributeと同値(passwordRe == 'password'ではなく、passwordRe == password)
    mail: { pattern: 'email' }, // 正規表現パターン(この場合はプラグイン定義済みのe-mailパターン)
    weight: { pattern: 'number' }, // 数値(小数も可)
    code: { pattern: 'digits' }, // 数字のみ(≠整数) /^\d+$/
    url: { pattern: 'url' }, // url
    word: { pattern: /^[A-Z]/ } // 独自正規表現
  }

エラー時のメッセージは用意されているが、独自メッセージを出したい場合はmsgを指定。
というかデフォルトメッセージは英語なので全部指定することになる。

    name: {
      required: true,
      msg: '名前は必須です' // functionで文字列return、でも可
    }

複数バリデーター指定もできる。

    name: {
      required: true,
      minLength: 2,
      msg: '名前は必須で2文字以上'
    }

バリデーター毎にメッセージを変えたい場合は配列指定。

    name: [{
      required: true,
      msg: '名前は必須'
    },{
      minLength: 2,
      msg: '名前は2文字以上'
    }]

bind

Viewにバインドする。
バインドするタイミングは、model or collectionがinitializeされた後ならいつでもいい。
Marionette + stickitを使っていて、onShow()でstickitのbindをしてあるので、bind並びでvalidationもそこでbindすることにする。

onShow: function() {
  this.stickit(); // stickitのbind
  Backbone.Validation.bind(this); // validationのbind
}

Backbone.Validation.unbind(view)でunbindされる。

Modelのoverride

bindするとmodelのvalidate()とisValid()がオーバーライドされるので、自分で実装する必要がない。処理を書く手間が省けるのはやはり有難い。

あとpreValidate()が実装され、modelにsetする前に事前検証ができるようになる。
と言ってもstickit使っているとpreValidate()の使いドコロが無い。
とりあえずsave前にvalidate()を呼んでエラー表示することにする。

ポート番号8888

「昨日アップされたソース取り込んで動かしたら変なサイトに飛ぶんだけど」

いやそんなの仕込んだ覚えないし。でも本当に「free-merchants.com」というサイトに飛ばされている。それに自分のローカル環境では起きない。

chromeプラグインが悪さしてんのかなー」と思いプラグインを外してもらうと正常に。
どうやらこれが該当する様子。
New Tab Page redirects localhost:8888 to spam website
日付は2日前、バージョンアップでプラグイン周りがおかしくなったのかも。

確かに開発のローカル環境でwebサーバーを8888ポートであげていた。
プラグイン外して現象は起きなくなったが、念のためポートを変えることにする。
wikiに49152–65535がプライベート ポート番号と書いてある。
TCPやUDPにおけるポート番号の一覧 - Wikipedia

「へー、知らなかったなー」(吉田羊)
「あれ?知らなかったんですか?」(田中みな実)

吉田羊ほんとよく見るなー。ポカリのCMわりと好き。

・・・とりあえずプライベート番号から選んでおく。

結局chromeの現象ってプラグインが悪いのか本体が悪いのかどっちなんだ?
あんまり話題になってないてことはプラグイン側か?
プラグイン便利は便利だけど、やっぱり程々にしないと危険。

JPQLのfetch その2

JOIN (≠JOIN FETCH)

JPQLはJOIN FETCHではなくJOINだけでも書ける。

@NamedQuery(name="Find", query="SELECT DISTINCT t FROM Team t JOIN t.members m")
SELECT DISTINCT t1.ID, t1.NAME FROM member t0, team t1 WHERE (t0.teamId = t1.ID)
1
Aチーム
SELECT ID, NAME, teamId FROM member WHERE (teamId = ?) bind => [1]
    ハンニバル
    フェイス
    B.A.
    マードック
-----
2
MP
SELECT ID, NAME, teamId FROM member WHERE (teamId = ?) bind => [2]
    リンチー
    デッカー
    フルブライト
-----

FETCHしていないのでN+1になる。使いドコロとしてはWHERE条件にmemberを加えたいけどFETCHはしない場合など。

@NamedQuery(name="Find", query="SELECT DISTINCT t FROM Team t JOIN t.members m WHERE m.name LIKE '%B%'")

List<Team> temas = em.createNamedQuery("Find", Team.class).getResultList();
for (Team t : teams) {
    System.out.println(t.getId());
    System.out.println(t.getName());
    System.out.println("-----");
}
SELECT t.ID, t1.NAME FROM member t0, team t1 WHERE (t0.NAME LIKE ? AND (t0.teamId = t1.ID)) bind => [%B%]
1
Aチーム
-----

QueryHints.FETCH (eclipselink)

JPA上ではFETCHは子プロパティまでで、孫プロパティをJOIN FETCHしてもN+1になるが、QueryHintでEclipseLink独自ヒントQueryHints.FETCHを使うと、多段FETCHしてくれるらしい。
テーブルを増やして実験。

team

id name
1 Aチーム
2 MP

member

id name teamId titleId
1 ハンニバル 1 1
2 フェイス 1 2
3 B.A. 1 3
4 マードック 1 4
101 リンチ 2 1
102 デッカー 2 1
103 フルブライト 2 5

title

id name
1 大佐
2 中尉
3 軍曹
4 大尉
5 准将
@Entity @Table(name="team")
public class Team {
    @Id
    private Integer id;
    private String name;
    @OneToMany(mappedBy="team")
    private List<Member> members;
}

@Entity @Table(name="member")
public class Member {
    @Id
    private Integer id;
    private String name;
    @ManyToOne
    @JoinColumn(name="teamId")
    private Team team;
    @ManyToOne
    @JoinColumn(name="titleId")
    private Title title;
}
@Entity @Table(name="title")
public class Title {
    @Id
    private Integer id;
    private String name;
    @OneToMany(mappedBy="title")
    private List<Member> members;
}
※getter, setter略

まずはhint無し。

@NamedQuery(name="Find", query="SELECT DISTINCT t FROM Team t JOIN FETCHt.members m JOIN FETCH m.title ti")

List<Team> temas = em.createNamedQuery("Find", Team.class).getResultList();
for (Team t : teams) {
    System.out.println(t.getId());
    System.out.println(t.getName());
    t.getMembers().forEach(m -> System.out.println("\t" + m.getName() + m.getTitle().getName()));
    System.out.println("-----");
}
SELECT DISTINCT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId, t0.titleId FROM member t0, team t1 WHERE (t0.teamId = t1.ID)
SELECT ID, NAME FROM title WHERE (ID = ?) bind => [4]
SELECT ID, NAME FROM title WHERE (ID = ?) bind => [2]
SELECT ID, NAME FROM title WHERE (ID = ?) bind => [1]
SELECT ID, NAME FROM title WHERE (ID = ?) bind => [3]
SELECT ID, NAME FROM title WHERE (ID = ?) bind => [5]
1
Aチーム
    マードック大尉
    フェイス中尉
    ハンニバル大佐
    B.A.軍曹
-----
2
MP
    デッカー大佐
    フルブライト准将
    リンチ大佐
-----

孫プロパティのtitleは個別にSELECTが実行されている。

hintを付ける。

@NamedQuery(name="Find", query="SELECT DISTINCT t FROM Team t JOIN FETCH t.members m JOIN FETCH m.title ti"
    , hints = {@QueryHint(name = QueryHints.FETCH, value = "t.members.title")})
SELECT DISTINCT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId, t0.titleId, t2.ID, t2.NAME FROM member t0, title t2, team t1 WHERE ((t0.teamId = t1.ID) AND (t2.ID = t0.titleId))
1
Aチーム
    マードック大尉
    フェイス中尉
    ハンニバル大佐
    B.A.軍曹
-----
2
MP
    デッカー大佐
    フルブライト准将
    リンチ大佐
-----

SELECT 1回でFETCHしている。

QueryHints.FETCH の裏効果

QueryHints.FETCHを使うと、自動でDISTINCT, JOIN FETCH状態になる。

@NamedQuery(name="Find", query="SELECT t FROM Team t JOIN t.members m JOIN m.title ti"
    , hints = {@QueryHint(name = QueryHints.FETCH, value = "t.members.title")})
SELECT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId, t0.titleId, t2.ID, t2.NAME FROM member t0, title t2, team t1 WHERE ((t0.teamId = t1.ID) AND (t2.ID = t0.titleId))
1
Aチーム
    ハンニバル大佐
    フェイス中尉
    B.A.軍曹
    マードック大尉
-----
2
MP
    リンチ大佐
    デッカー大佐
    フルブライト准将
-----

SQLにDISTINCTは付いていないが、グルーピングされて取得されているし、JOIN(≠JOIN FETCH)だがSELECT 1回で全部取得している。
hintがなければDISTINCTもFETCHもされない。

ハマったのはこの裏効果で、hint付きでJOIN(≠JOIN FETCH)をしていたJPQLを修正した際、孫プロパティが不要になったのでhintを外したら、FETCHもDISTINCTもされなくなったという。。。
QueryHints.FETCHの仕様が変わる可能性もあるし、DISTINCT, FETCHは必要に応じてちゃんと付けましょうという教訓。

JPQLのfetch その1

タイトルの通りだが勉強不足でハマったのでメモ。
Java8 + JPA2.1 + eclipselink2.6

テーブル

team

id name
1 Aチーム
2 MP

member

id name teamId
1 ハンニバル 1
2 フェイス 1
3 B.A. 1
4 マードック 1
101 リンチ 2
102 デッカー 2
103 フルブライト 2

Entity

@Entity @Table(name="team")
public class Team {
    @Id
    private Integer id;
    private String name;
    @OneToMany(mappedBy="team")
    private List<Member> members;
}

@Entity @Table(name="member")
public class Member {
    @Id
    private Integer id;
    private String name;
    @ManyToOne
    @JoinColumn(name="teamId")
    private Team team;
}
※getter, setter略

1件取得

@NamedQuery(name="Find", query="SELECT t FROM Team t WHERE t.id = :id")

Team t = em.createNamedQuery("Find", Team.class).setParameter("id", 1).getSingleResult();
System.out.println(t.getId());
System.out.println(t.getName());
t.getMembers().forEach(m -> System.out.println("\t" + m.getName()));

実行ログ

SELECT ID, NAME FROM team WHERE (ID = ?) bind => [1]
1
Aチーム
SELECT ID, NAME, teamId FROM member WHERE (teamId = ?) bind => [1]
    ハンニバル
    フェイス
    B.A.
    マードック

JPQLでJOINしていないのでgetMembers()の際にSQLが発行される、N+1問題というやつ。

なのでクエリでJOIN FETCHしてやると、memberもまとめて取ってくる。

@NamedQuery(name="Find", query="SELECT t FROM Team t JOIN FETCH t.members WHERE t.id = :id")
SELECT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId FROM member t0, team t1 WHERE ((t1.ID = ?) AND (t0.teamId = t1.ID)) bind => [1]
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック

複数件取得

@NamedQuery(name="Find", query="SELECT t FROM Team t")

List<Team> temas = em.createNamedQuery("Find", Team.class).getResultList();
for (Team t : teams) {
    System.out.println(t.getId());
    System.out.println(t.getName());
    t.getMembers().forEach(m -> System.out.println("\t" + m.getName()));
    System.out.println("-----");
}
SELECT ID, NAME FROM team
1
Aチーム
SELECT ID, NAME, teamId FROM member WHERE (teamId = ?) bind => [1]
    ハンニバル
    フェイス
    B.A.
    マードック
-----
2
MP
SELECT ID, NAME, teamId FROM member WHERE (teamId = ?) bind => [2]
    リンチー
    デッカー
    フルブライト
-----

1行と同様、N+1でデータを取得してくる。

なのでJOIN FETCHをする。

@NamedQuery(name="Find", query="SELECT t FROM Team t JOIN FETCH t.members")
SELECT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId FROM member t0, team t1 WHERE (t0.teamId = t1.ID)
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック
-----
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック
-----
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック
-----
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック
-----
2
MP
    リンチー
    デッカー
    フルブライト
-----
2
MP
    リンチー
    デッカー
    フルブライト
-----
2
MP
    リンチー
    デッカー
    フルブライト
-----

なんてこったい。

ということを無くすためにDISTINCTをつける。

@NamedQuery(name="Find", query="SELECT DISTINCT t FROM Team t JOIN FETCH t.members")
SELECT DISTINCT t1.ID, t1.NAME, t0.ID, t0.NAME, t0.teamId FROM member t0, team t1 WHERE (t0.teamId = t1.ID)
1
Aチーム
    ハンニバル
    フェイス
    B.A.
    マードック
-----
2
MP
    リンチー
    デッカー
    フルブライト
-----

SQLにdistinctがついた上で、オブジェクトレベルでもTeamでグループ化される。

その2へ続く

JDBCでINSERT後のシーケンス値を取得する

idカラムを作って自動採番するような場合。
JPAでINSERTすればidが入った状態でEntityが取れるけど、JDBCの場合にどうするか。
何故にJDBCかというと、バイナリデータを放り込むのにJPAではなくJDBCを使いたいから。
PostgreSQLのbyteaカラムに手動でデータを入れる - edgegram

参考:java - PreparedStatement with Statement.RETURN_GENERATED_KEYS - Stack Overflow

Statement.RETURN_GENERATED_KEYSを引数に追加すればできる。
Stetement使う(人が今どきいるか?)場合はexecuteUpdate()の第2引数、PreparedStatementならprepareStatement()で指定する。

PreparedStatement ps = con.prepareStatement("INSERT INTO binary_tbl (value) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
ps.setBinaryStream(1, fis, (int)file.length());
ps.executeUpdate();

その後、PreparedStatement(or Statement)からgetGeneratedKeys()すれば、自動採番されたシーケンス値の一覧がResultSetで返ってくる。

ResultSet rs = ps.getGeneratedKeys();
if (rs != null && rs.next()) {
    long id = rs.getLong(1);
}

Statement.RETURN_GENERATED_KEYSの代わりにカラム名のString配列を指定しておけばカラム名で取れる。

PreparedStatement ps = con.prepareStatement("INSERT INTO binary_tbl (value) VALUES (?)", new String[]{"id"});
・・・
long id = rs.getLong("id");

あまりしないと思うが、1テーブル複数シーケンスとかの場合はカラム指定の方が分かりやすい。

Java8 + PostgreSQL9.4