I wondered if I could realize a serverless RDB, but I feel that it is quite so if I use CloudRun. However, CloudRun can't speak JDBC, so I thought I should make my own JDBC driver and use HTTP on the back side, so I investigated how to make it.
Surprisingly, it seems that the JDBC driver is easy to make, so I tried to make the minimum skeleton implementation for the time being. Since there is no RDB on the back side, basically I will make the following that just returns an echo.
--Main class for testing
First, let's create a main class that tests and executes both rabbits and horns. This is a JDBC driver created by MyDriver and MyConnection. It's a simple implementation that just confirms that the return value is MyConnection, but now you can see that your own JDBC driver is being called properly.
github: https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/Main.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));
}
}
}
The execution result is as follows.
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
Next is the JDBC driver.
The point is the static
initializer, which completes the driver registration when the class is loaded with Class.forName
.
You can also get the JDBC URL here. In the actual implementation, it seems that it is common to parse the URL at this timing and assemble the connection destination information.
In the sample code, it is implemented with ʻUnsupportedOperationException` except where it is described. You need to replace it with the actual code if necessary.
github: https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyDriver.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);
}
-Abbreviation-
Connection
Connection creates a Statement. In the actual implementation, the connection is maintained in the unit of Connection, so I think that you will go to the connection with the constructor etc.
github: https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyConnection.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();
}
-Abbreviation-
Statement
Statement processes queries. The SQL parser should be called here. By the way, since I / F is just passing a character string, it is possible to pass a query other than SQL without any problem.
In the dummy, List of List is mapped to ResultSet and returned.
github: https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyStatement.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");
}
-Abbreviation-
ResultSet
ResultSet is the result of query execution. The dummy is implemented by wrapping the iterator.
In the actual code, the actual data will be handled here, but since the DB is generally large, it will be necessary to implement iterator-like implementation instead of List-like implementation.
github: https://github.com/koduki/jdbc-skeleton/blob/master/src/main/java/cn/orz/pascal/jdbc/MyResultSet.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;
}
-Abbreviation-
Well, when I tried it, I was able to implement the JDBC driver more easily than I expected. I thought it would be more difficult, so I'm a little out of tune, but it means that the I / F is implemented neatly.
If you can hack the JDBC driver, you can hook SQL and output it to the log, proxy it and send it to another DB, or connect it to KVS or another DB implementation without difficulty, including ORM such as JPA.
Since there are many APIs, it seems that it will take some patience to implement it seriously, but if it is a type that wraps the actual JDBC, it seems to be relatively easy to make.
This seems to be useful, so it seems good to put it in the tool box.
Then Happy Hacking!
-Create JDBC over Thrift Part 1 (How to create a JDBC driver) --Why you can access DB with Class.forName
Recommended Posts