TECHSCORE Advent Calendar 2015 の 1日目の記事です。
はじめに
Amazon Redshift を使っています。大きなサイズのデータを簡単にお安く分析できるので重宝しているのですが、小さなデータに対する応答時間は決して短くありません。やはりここは PostgreSQL などの普通の RDBMS と得意分野で使い分けたいところ。
ということで Redshift と PostgreSQL の両方に対して同時に JDBC 接続しようとしたんですが、ちょっと困ったことが起こったのでいろいろ調べてみました。
いきなり解決策を知りたい方はこちらへ。
何が困ったの?
JDBC を使って、Redshift と PostgreSQL の両方に接続してみます。極々シンプルな実装です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import java.sql.*; import java.util.*; class Test { public static void main(String[] args) throws Exception { Properties info = new Properties(); info.setProperty("user", "xxxxxxxx"); info.setProperty("password", "xxxxxxxx"); Connection conn; conn = DriverManager.getConnection( "jdbc:redshift://<Redshiftのホスト名>/xxxxxxxx", info); // ① System.out.println("Redshift conn: " + conn.getClass().getName()); // ★ conn = DriverManager.getConnection( "jdbc:postgresql://<PostgreSQLのホスト名>/xxxxxxxx", info); // ② System.out.println("PostgreSQL conn: " + conn.getClass().getName()); // ★ } } |
★の2箇所で、得られた Connection のクラス名を出力しています。
以下のようにコンパイルします。
1 |
$ javac Test.java |
実行時にはクラスパスに Redshift と PostgreSQL それぞれの JDBC ドライバの設定が必要です。以下のように実行してみました。
1 2 3 4 5 6 |
$ REDSHIFT_JAR=RedshiftJDBC41-1.1.9.1009.jar $ POSTGRESQL_JAR=postgresql-9.4-1205.jdbc41.jar $ export CLASSPATH=.:${REDSHIFT_JAR}:${POSTGRESQL_JAR} # ③ $ java Test Redshift conn: com.amazon.redshift.jdbc41.S41NotifiedConnection PostgreSQL conn: com.amazon.redshift.jdbc41.S41NotifiedConnection |
両方とも Redshift 用のクラスが使われています。これはちょっと嬉しくありません。
何が起こったの?
何が起こったのが調べるために、DriverManager のソースを見てみます。
今回使用したメソッド DriverManager.getConnection(String url, ...) からは最終的にこちらのメソッドが呼ばれます。メンバ変数 registeredDrivers に入っている Driver を順番に取ってきて、最初に接続に成功した Driver が使われるという実装になっています。
では、registeredDrivers にはどのように登録されるのでしょうか。
ここの静的初期化子にそのヒントがあります。
DriverManager がロードされた時にこの静的初期化子が実行され、loadInitialDrivers() が呼ばれます。loadInitialDrivers() ではサービス・プロバイダ・ロード機能を使って、java.sql.Driver サービスをロード(ServiceLoader.load(Driver.class))します。
(システムプロパティ jdbc.drivers に関する処理もありますが、ここは今回は関係ないので省略します。)
サービス・プロバイダ・ロード機能では、クラスパスの順に jar を走査して、jar 内の META-INF/services/java.sql.Driver に記述されたクラスのインスタンスが作成されます。
jar 内の META-INF/services/java.sql.Driver の記述は、それぞれ
- Redshift / com.amazon.redshift.jdbc41.Driver
- PostgreSQL / org.postgresql.Driver
となっていますので、これらのインスタンスが new されます。
さて、new org.postgresql.Driver() までたどり着きました。ソースを見てみます。
こちらの静的初期化子から register() が呼ばれ、ここで DriverManager.registerDriver(Driver) が呼ばれています。このメソッドで registeredDrivers に Driver が登録されています。
これまでの記述を簡単にまとめます。
DriverManager を使用する際に、クラスパス順に JDBC ドライバがロードされて DriverManager に登録されます。
DriverManager.getConnection(String url, ...) では登録順に Driver を使って接続し、最初に接続に成功した Driver が返却される、という動きになっています。
次に、今回の実行での動きを見てみます。
クラスパスは Redshift 用ドライバ -> PostgreSQL 用ドライバの順に設定しています(③)。
①では [jdbc:redshift] スキーマに接続しようとします。
最初に Redshift 用ドライバを試します。当然接続に成功しますので、Redshift 用ドライバが使われます。
②では [jdbc:postgresql] スキーマに接続しようとします。
ここでも最初に Redshift 用ドライバを試しますが、
Note
jdbc:postgresql://エンドポイント:ポート/データベース という旧形式で指定されている JDBC URL は、まだ動作します。
(Amazon Redshift 管理ガイド)
こちらも接続に成功し、Redshift 用ドライバが使われてしまいます。
解決したい その1 クラスパスでなんとか...
クラスパスの順序を変更して、先に PostgreSQL 用のドライバに接続するようにすれば解決できます。
上記のソースそのまま、再コンパイルなしで、クラスパスだけ変更して実行します。
1 2 3 4 5 6 |
$ REDSHIFT_JAR=RedshiftJDBC41-1.1.9.1009.jar $ POSTGRESQL_JAR=postgresql-9.4-1205.jdbc41.jar $ export CLASSPATH=.:${POSTGRESQL_JAR}:${REDSHIFT_JAR} # 変更箇所 $ java Test Redshift conn: com.amazon.redshift.jdbc41.S41NotifiedConnection PostgreSQL conn: org.postgresql.jdbc41.Jdbc41Connection |
確かに意図通りの動作ですが、クラスパスは必ずしも明示的に設定できるとも限りませんので、この方法はいまひとつです。
解決したい その2 明示的にドライバを指定する...
自動解決されるドライバが意図したものと異なるならば、明示的にドライバを指定することで解決できます。
DriverManager.getConnection(String url, ...) の代わりに、Driver インスタンスを new して Driver.connect(String url, ...) を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import java.sql.*; import java.util.*; class Test { public static void main(String[] args) throws Exception { Properties info = new Properties(); info.setProperty("user", "xxxxxxxx"); info.setProperty("password", "xxxxxxxx"); Connection conn; Driver driver; driver = (Driver)Class.forName("com.amazon.redshift.jdbc41.Driver").newInstance(); conn = driver.connect( "jdbc:redshift://<Redshiftのホスト名>/xxxxxxxx", info); System.out.println("Redshift conn: " + conn.getClass().getName()); driver = (Driver)Class.forName("org.postgresql.Driver").newInstance(); conn = driver.connect( "jdbc:postgresql://<PostgreSQLのホスト名>/xxxxxxxx", info); System.out.println("PostgreSQL conn: " + conn.getClass().getName()); } } |
コンパイルして、実行すると、
1 2 3 4 5 6 7 |
$ javac Test.java $ REDSHIFT_JAR=RedshiftJDBC41-1.1.9.1009.jar $ POSTGRESQL_JAR=postgresql-9.4-1205.jdbc41.jar $ export CLASSPATH=.:${REDSHIFT_JAR}:${POSTGRESQL_JAR} # 最初のうまくいかなかった例と同じ順序 $ java Test Redshift conn: com.amazon.redshift.jdbc41.S41NotifiedConnection PostgreSQL conn: org.postgresql.jdbc41.Jdbc41Connection |
意図通りの動作になっています。
でも、なんだかしっくりきません。確かに理屈はわかりますが、こんな面倒なことしなきゃいけないんでしょうか、Redshift のドライバは JDBC のバージョンが上がったらクラス名変わりそうだし...と思ったらスマートな解決方法がありました。
解決したい その3 決定版
Redshiftの JDBC ドライバー設定オプションに OpenSourceSubProtocolOverride というものがあります。
有効になっている場合、この設定は、Amazon Redshift JDBC ドライバと PostgreSQL JDBC ドライバとの間に発生する可能性のある競合を防ぎます。ご使用のアプリケーションが、Amazon Redshift JDBC ドライバを使ってクラスターに接続すると同時に、PostgreSQL JDBC ドライバを使って他のデータソースに接続する場合、PostgreSQL データソースに接続するために使用する JDBC URL にこの接続属性を追加します。
これを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import java.sql.*; import java.util.*; class Test { public static void main(String[] args) throws Exception { Properties info = new Properties(); info.setProperty("user", "xxxxxxxx"); info.setProperty("password", "xxxxxxxx"); Connection conn; conn = DriverManager.getConnection( "jdbc:redshift://<Redshiftのホスト名>/xxxxxxxx", info); System.out.println("Redshift conn: " + conn.getClass().getName()); conn = DriverManager.getConnection( "jdbc:postgresql://<PostgreSQLのホスト名>/xxxxxxxx?OpenSourceSubProtocolOverride=true", info); // ☆ System.out.println("PostgreSQL conn: " + conn.getClass().getName()); } } |
コンパイルして、実行します。
1 2 3 4 5 6 7 |
$ javac Test.java $ REDSHIFT_JAR=RedshiftJDBC41-1.1.9.1009.jar $ POSTGRESQL_JAR=postgresql-9.4-1205.jdbc41.jar $ export CLASSPATH=.:${REDSHIFT_JAR}:${POSTGRESQL_JAR} # 最初のうまくいかなかった例と同じ順序 $ java Test Redshift conn: com.amazon.redshift.jdbc41.S41NotifiedConnection PostgreSQL conn: org.postgresql.jdbc41.Jdbc41Connection |
うまくいきました。
おわりに
Redshift 用のドライバを使って PostgreSQL に接続しても、単純な機能を使用している限りではエラーにもならず、データも取得できてしまいます。TIMESTAMP 型のデータを取り出そうとしたときに「変換できないよ!」と言われ、そこから慌てて調べました。
気付かずに使ってしまっているケースもあるかと思います。一度実際に使われているクラスを確認することをおススメします。