JDBCドライバの作り方

## はじめに

サーバレスなRDBを実現出来ないかと考えてみたのですが、CloudRunとか使えば何とかなりそうな気がします。
ただ、CloudRunはJDBCをしゃべれないので、それならJDBCドライバを自作して裏側をHTTPにしてしまえと思って作り方を調べてみました。

調べてみると意外にもJDBCドライバは簡単に作れるようなので、とりあえず最低限のスケルトン実装を作ってみました。
裏側にRDBがいる訳でもないので、基本的には単にエコーを返すだけの以下を作ってみます。

- テスト用メインクラス
- JDBC Driver
- JDBC Connection
- JDBC Statement
- JDBC ResultSet

## テスト用の実行処理

まずは兎にも角にもテスト実行するメインクラスを作ってみます。MyDriverとMyConnectionが自作したJDBCドライバです。
戻り値がMyConnectionである事を確認するだけのシンプルな実装ですが、これで自作したJDBCドライバが適切に呼び出されている事が分かります。

github:
https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/Main.java

```java
var url = "jdbc:myjdbc://localhost:80/testdb";

Class.forName("cn.orz.pascal.jdbc.MyDriver");
try (var con = DriverManager.getConnection(url); var st = con.createStatement()) {
    st.execute("INSERT DUMMY SQL");
    try (var rs = st.executeQuery("SELECT DUMMY SQL")) {
        while (rs.next()) {
            System.out.println("rs[1]=" + rs.getString(1));
        }
    }
}
```

実行結果は以下の通りです。

```bash
jdbc uri: jdbc:myjdbc://localhost:80/testdb
execute sql: INSERT DUMMY SQL
execute sql: SELECT DUMMY SQL
rs[1]=a
rs[1]=e
rs[1]=h
MyResultSet close
MyStatement close
MyConnection close
```

## Driver

続いてJDBCドライバです。
ポイントは`static`イニシャライザでこれにより`Class.forName`でクラスロードしたタイミングで、ドライバの登録を済ませています。

また、JDBC URLもここで取得できます。実際の実装ではこのタイミングでURLをパースして接続先の情報を組み立てるのが一般的のようです。

なお、サンプルコードでは記載しているところ以外は`UnsupportedOperationException`で実装しています。必要に応じて実際のコードに置き換える必要があります。

github:
https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyDriver.java

```java
package cn.orz.pascal.jdbc;

public class MyDriver implements Driver {
    private static final String URI_PREFIX = "jdbc:myjdbc://";

    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException ex) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        if (!url.startsWith(URI_PREFIX)) {
            return null;
        }
        return new MyConnection(url, info);
    }

- 略 -
```

## Connection

ConnectionではStatementの作成を行います。
実際の実装では接続はConnectionの単位で維持されるのでコンストラクタなどで接続に行くことになるかと思います。

github:
https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyConnection.java

```java
package cn.orz.pascal.jdbc;

public class MyConnection implements Connection {
    public MyConnection(String uri, Properties info) throws SQLException {
        System.out.println("jdbc uri: " + uri);
    }

    @Override
    public void close() throws SQLException {
        System.out.println(this.getClass().getSimpleName() + " close");
    }

    @Override
    public Statement createStatement() throws SQLException {
        return new MyStatement();
    }

- 略 -
```

## Statement

Statementではクエリの処理等を行います。SQLパーサーはここで呼び出される必要があります。
ちなみに、I/F上は単なる文字列を渡しているだけなのでSQL以外のクエリを渡す事も特に問題無く出来たりします。

ダミーではListのListをResultSetにマッピングして返しています。

github:
https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyStatement.java

```java
package cn.orz.pascal.jdbc;

public class MyStatement implements java.sql.Statement {

    @Override
    public boolean execute(String sql) throws SQLException {
        System.out.println("execute sql: " + sql);

        return true;
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        System.out.println("execute sql: " + sql);
        
        var result = new MyResultSet(List.of(
                List.of("a", "b", "c"),
                List.of("e", "f", "g"),
                List.of("h", "i", "j")
        ));
        return result;
    }

    @Override
    public void close() throws SQLException {
        System.out.println(this.getClass().getSimpleName() + " close");
    }

- 略 -
```

## ResultSet

ResultSetはクエリの実行結果です。
ダミーではイテレーターをラッピングする形で実装しています。

実際のコードでは実データをここで取り扱う事になりますが一般的にDBは大きいので、List的な実装ではなくイテレータ的な実装にする必要があるでしょう。

github:
https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyResultSet.java

```java
package cn.orz.pascal.jdbc;

public class MyResultSet implements java.sql.ResultSet {

    private final Iterator<List<String>> itr;
    private List<String> current;

    public MyResultSet(List<List<String>> source) {
        this.itr = source.iterator();
    }

    @Override
    public String getString(int index) throws SQLException {
        return current.get(index - 1);
    }

    @Override
    public boolean next() throws SQLException {
        var result = itr.hasNext();

        if (result) {
            current = itr.next();
        }
        return result;
    }

- 略 -
```

## まとめ

さて、試してみると思いのほか簡単にJDBCドライバの実装が出来ました。もっと大変かと思ったので少し拍子抜けですが、それだけI/Fがきれいに実装されているという事ですね。

JDBCドライバをハック出来れば、SQLをフックしてログに出すとか、Proxy化して別のDBに飛ばす、あるいはKVSや別のDB実装に繋ぐなどがJPAなどのORMを含めて難なく実現できます。

APIは多いので真面目に実装すると多少根気はいりそうですが、実際のJDBCをラッピングするタイプであれば比較的簡単に作れそうです。

これは何かと便利そうなので、お道具箱に入れておくと良さそうですね。

それではHappy Hacking!

## 参考

- [JDBC over Thriftを作成する　その１（JDBCドライバの作り方）](https://symfoware.blog.fc2.com/blog-entry-380.html)
- [Class.forName で DB アクセスできるようになる理由](https://spider-man.hatenadiary.org/entry/20080307/p1)
- [GitHub - opentracing-contrib/java-jdbc](https://github.com/opentracing-contrib/java-jdbc/blob/master/src/main/java/io/opentracing/contrib/jdbc/TracingDriver.java)