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

Javaのログ出力ライブラリメモ

知識がlog4jで止まっていたので調査。

Javaのログ出力: 道具と考え方
javaのロガーが多すぎて訳が解らないので整理してみました - 文系プログラマによるTIPSブログ

・・・迷路で遊ぶ暇は無いんだけどな、、、
とりあえず「SLF4J+Logback」でそんなに問題にはならんだろう。

JAX-RS 返却用クラスはデフォルトコンストラクタ必須

タイトルの通り。JAXBの仕様?らしい。

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getIt() {
        return new User(1);
    }

この場合、Userクラスが以下だとダメ。

public class User {
    public User(int id) {}
}

ちゃんとデフォルトコンストラクタ作るべし。

public class User {
    public User() {}
    public User(int id){}
}

JSON・XMLを返すけどエラー時はステータスコードやメッセージを選びたい

JAX-RSで200とか404とか返すだけなら、メソッド戻り値にResponseを指定してResponseを操作すればいい。

@RequestScoped
@Path("myresource")
public class MyResource {
    @GET
    public Response getIt() {
        return Response.ok().build();    // 200が返る
    }
}

JSONとかXMLとか返したいなら、戻り値にオブジェクトを指定して@ProducesでMIME(MediaType)を指定する。

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getIt() {
        User user = userService.getUser(1);
        return user;    // JSONが返る
    }


本題。
通常はJSONXMLを返したいけど、データベースなどにデータが無い場合は404にしたい。
参考:Chapter 7. Representations and Responses

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getIt() {
        try {
            User user = userService.getUser(1);
            return user;    // JSONが返る(ステータスコード:200)
        } catch (NoResultException e) {
            throw new WebApplicationException(404);    // 404が返る
        }
    }

WebApplicationExceptionをthrowする。ステータスコード返すだけならコード番号を渡す。
メッセージも変えたいならResponseを渡す。

throw new WebApplicationException(
    Response.status(Status.NOT_FOUND).entity("error. error. error.").build()
);


長すぎるし。。。
多分、WebApplicationExceptionを継承したクラス作るほうが幸せになれる。

public class CustomException extends WebApplicationException {
    public CustomException() {
        super(Response.status(Status.NOT_FOUND).build());
    }

    public CustomException(String message) {
        this(Status.NOT_FOUND, message);
    }

    public CustomException(Status status, String message) {
        this(status.getStatusCode(), message);
    }

    public CustomException(int code, String message) {
        super(
            Response.status(code).entity(message).build()
        );
    }
}

他にもExceptionMapperを使ってExceptionとレスポンスをマッピングさせる方法もある。
NoResultException -> 404 とかやってもいいけど、エラーの切り分けが面倒になりそう
(データが無いのか、単純にパスが間違っているのか、etc...)
なのでやめておく。


余談
このままだとtext/plainだけど、JSON(XML)返すサービスならエラーもJSON(XML)で返すべき?
まあMediaType指定するだけでいいから何とでもなるんだけど。

DeltaSpike(Data)を諦める

DeltaSpikeもひと通り調べ終わり、実際にコーディングを進めていたがDeltaSpike Dataを使うのを諦めることにした。

理由は原因不明のOutOfMemoryError (heap space)が発生して、どうしても発生源が突き止められなかったから。
JAX-RSの1リクエストでRepositoryでデータを取得してデータを返す分には問題無いが、せいぜい同時5リクエスト程度でOutOfMemoryになる。
テストデータは20件程度でメモリを食う要素も無いはず。
Tomcatのヒープを増やそうが、あらゆる場所にEntityManager.clear()を入れようが解決せず。

Repositoryを使わずEntityManagerからDtoデータを引っ張ってくるようにコードを書き直すと問題なく動いたので、仕方なくEntityManagerを使ってカリカリ書くことにする。
とりあえずJSONを返せればいいので、膨大に手間が増えることはないだろうし、一週間以上やっているので正直これ以上深入りする時間が無い。
(し、これ以上精神が持たない。ほんとにツラい。)

Tomcat8 + Java8 + DeltaSpike1.5.1

Jersey + CDIで@Contextが動かない

Jersey + CDIの構成で、ContainerRequestFilterの中でUriInfoを使おうとしてハマった。
構成は JAX-RS(Jersey) + JPA (on CDI) / on Tomcat - edgegram がベースでDeltaspikeを少々追加。

public class FormTypeFilter implements ContainerRequestFilter {
    @Context
    private UriInfo uriInfo;

    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println(uriInfo);
    }
}

UriInfoがnullにしかならない。ResourceInfoも試したが同様。
調べてもそんな事例なさそうだし(そもそもJersey + CDI自体情報少ないし)。


他のライブラリが影響しているのかと、色々抜き差しした結果、

  • 単純なJersey(jersey-container-servletだけ)にするとInjectされる
  • CDIを使おうとする(jersey-cdi1xを入れる)とnullになる

という所まで何とかこぎつける。
ただ、jersey-cdi1xがダメとなるとCDIが使えなくなる。

何か無いのか? とmarvenリポジトリを検索してみると、jersey-cdi1x-servletというのを発見。
jersey-cdi1xの代わりにjersey-cdi1x-servletを入れて試すと

・・・

UriInfo入った。ちゃんとInjectされている。
他の自前クラスも@Injectできているし、CDIコンテナも問題なく動いている様子。
正解かどうか分からないが、とりあえず動くのでいいことにする。

ハマり時間:8時間

PostgreSQLのbyteaカラムに手動でデータを入れる

初期データ用にbyteaデータを入れようと調べてみたが、psqlからのINSERTはできない?
仕方なくJDBCで放り込むことにした。(その時点で既に手動ではないが)
Chapter 7. Storing Binary Data

Class.forName("org.postgresql.Driver");
Connection con = DriverManager.getConnection("jdbc:postgresql://dbhost:5432/test", "test", "test");

File file = new File("C:/image.png");
FileInputStream fis = new FileInputStream(file);

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

JDBCなんて使うの数年前に研修で教えて以来だわ。
余談だけど、今時の開発者の中にはJDBC学ばずに、O/Rとかライブラリ使うしか分からない人もいたりするのかなぁ。