Sql2oでJava8のDate and Time APIを使う

こちらのスライドでSql2oというライブラリを知りました。

Java が支える 人気ニュースアプリ NewsPicks の裏側 // Speaker Deck

素のJDBCは面倒だけど機能豊富なDB関連のライブラリ使う必要はない、ってときにすごく便利そうなので使ってみました。

Sql2oで、Java8で導入されたZonedDateTimeクラスなどをどうやれば使えるか、というお話です。

試したSql2oのバージョンは、1.5.4です。

Sql2o - Easy database query library - sql2o

まず、こんな感じのUserクラスがあったとします。

import java.util.Date
public class User {
    private int id;
    private String name;
    private Date joinedAt;
    public String toString() {
        return "User{id=" + id + ", name=" + name + ", joinedAt=" + joinedAt + "}";
    }
}

SELECTの結果を、Userオブジェクトにマッピングしたい場合は以下のように書きます。シンプルですね!

Sql2o sql2o = new Sql2o(DB_URL, USER, PASSWORD);
String sql = "SELECT id, name, joined_at FROM users WHERE id = :id";
try (Connection conn = sql2o.open()) {
    User user = conn.createQuery(sql)
        .addParameter("id", userId)
        .addColumnMapping("joined_at", "joinedAt")
        .executeAndFetchFirst(User.class);
}

ソースコード読んでたらJoda Timeにも対応してそうだったので試してみたら動きました。UserクラスのjoinedAtの型をJoda TimeのDateTimeに変えても動きます。

import org.joda.time.DateTime;
public class User {
    private int id;
    private String name;
    private DateTime joinedAt;
    public String toString() {
        return "User{id=" + id + ", name=" + name + ", joinedAt=" + joinedAt + "}";
    }
}

さて、Java8のZonedDataTimeを使うときはどうしたらいいでしょうか。Joda TimeのDateTimeに対応するのにConverterという仕組みを使っていたので、これを利用すればいけそうです。

Converterインタフェースを実装したクラスを用意します。こいつがZonedDateTimeへの変換を担当します。

import org.sql2o.converters.Converter;
import org.sql2o.converters.ConverterException;

import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;

private static class ZonedDateTimeConverter implements Converter<ZonedDateTime> {
    private final ZoneId zoneId;

    public ZonedDateTimeConverter(ZoneId zoneId) {
        this.zoneId = zoneId;
    }

    @Override
    public ZonedDateTime convert(Object val) throws ConverterException {
        if (val == null) return null;
        if (val instanceof Timestamp) {
            return ZonedDateTime.ofInstant(((Timestamp) val).toInstant(), zoneId);
        } else {
            throw new IllegalArgumentException();
        }
    }

    @Override
    public Object toDatabaseParam(ZonedDateTime val) {
        return new Timestamp(val.toInstant().toEpochMilli());
    }
}

Sql2oオブジェクトを生成するときにConverterを設定しておきます。ハッシュマップのキーは変換先のクラスを指定します。

Map<Class, Converter> converterMap = new HashMap<>();
converterMap.put(ZonedDateTime.class, new ZonedDateTimeConverter(ZoneId.of("Asia/Tokyo")));
Sql2o sql2o = new Sql2o(DB_URL, USER, PASSWORD, new NoQuirks(converterMap));

後はUserクラスで使う日時の型をZonedDateTimeに変更すればOKです!

import java.time.ZonedDateTime;
public class User {
    private int id;
    private String name;
    private ZonedDateTime joinedAt;
    public String toString() {
        return "User{id=" + id + ", name=" + name + ", joinedAt=" + joinedAt + "}";
    }
}

同じようにすればInstantクラスやLocalDateクラスに対応することもできそうですね。Optionalにも対応できるかなー。