JDBCドライバとバックエンドのやりとり。フロントエンド/バックエンドプロトコル
追記:キャプチャの結果が間違っていた(Row descriptionとData rowを混同していた)ので直した。
追記:JDBCドライバのログ出力も載せた。
PostgreSQLのフロントエンド/バックエンドプロトコルを、JDBCを使うプログラムを使って確認してみた。通信のキャプチャにはWiresharkを使った。
使ったJavaプログラム
使ったプログラムは以下のようなもの。1つのテーブル(branches)に対しSELECT文を実行する。よくあるケースで試したかったので、autoCommitはオフにした。branchesテーブルには11件のデータが入っている。ResultSet#nextを繰り返している途中でデータアクセスが発生するようにfetchSizeは6とし、11件より小さくした。
public class PostgreSqlTest { Connection conn; @Before public void before() throws Exception { Class.forName("org.postgresql.Driver"); String url = "jdbc:postgresql://10.0.1.87/test"; Properties props = new Properties(); props.setProperty("user", "postgres"); props.setProperty("password", "postgres"); conn = DriverManager.getConnection(url, props); conn.setAutoCommit(false); } @After public void after() throws Exception { if (conn != null) { conn.rollback(); conn.setAutoCommit(true); conn.close(); } } @Test public void test() throws Exception { PreparedStatement ps = conn.prepareStatement("select bid from branches"); try { ps.setFetchSize(6); ResultSet rs = ps.executeQuery(); try { while (rs.next()) { System.out.println(rs.getString(1)); } } finally { rs.close(); } } finally { ps.close(); } } }
やりとりされたメッセージ
あるメソッドを呼び出したときにどんなメッセージが送られたかを示す。ここでは、メッセージの種類だけを示し、実際に送られたデータは示さない。メッセージの詳細はドキュメントの45.4. メッセージの書式にある。
1.DriverManager.getConnection()したとき
フロントエンド(JDBCドライバ)はバックエンド(PostgreSQL)に Startup message を送る。
Type: Startup message
バックエンドはフロントエンドに次のメッセージを返す。
Type: Authentication request Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Parameter status Type: Backend key data Type: Ready for query
メッセージはまとめられて返される。Backend key dataは、この接続専用のキャンセルキーを送っている。
2.PreparedStatement#executeQuery()したとき
フロントエンドはバックエンドに次のメッセージを送る。
Type: Parse Type: Bind Type: Execute Type: Parse Type: Bind Type: Describe Type: Execute Type: Sync
いくつかのメッセージがまとめられて一度の通信で送られる。
最初のParse、Bind、ExecuteはBEGINステートメントに対応する。BEGINステートメントはautoCommitが無効のときに自動で送られる。2番目のParse、Bind、ExecuteはSELECTステートメントに対応するもの。Describeの必要性についてはいまいち理解しきれていないと思ったけど、フィールドのメタデータの要求っぽい(返されるデータはResultSetMetaDataや型の判別で使われる)。そのあとにSyncでメッセージを送信。
バックエンドはフロントエンドに次のメッセージを返す。
Type: Parse completion Type: Bind completion Type: Command completion Type: Parse completion Type: Bind completion Type: Row description Type: Data row Type: Data row Type: Data row Type: Data row Type: Data row Type: Data row Type: Portal suspended Type: Ready for query
戻りのメッセージもまとめられて返される。最初のParse completion、Bind completion、Command completionはBEGINステートメントに対応するもの。2番目のParse completion、Bind completionはSELECTステートメントに対応。Row descriptionはDescribeで要求したメタデータ(フィールドの名前や型情報)。次にData rowが6件。この6件は行の数。fetchSizeに6を指定しているので6件が返る。Portal suspendedでポータルが一時中断されたことがわかる(でも、このメッセージを送るだけでサーバ側の処理はsuspendedしようがしまいが変わりないように見えた)。Portal suspendedならば、JDBCドライバはまだポータルを閉じない。
3.7回目のResultSet#next()をしたとき
フロントエンドはバックエンドに次のメッセージを送る。
Type: Execute Type: Sync
Executeを再度実行し、次のデータを要求している。
バックエンドはフロントエンドに次のメッセージを返す。
Type: Data row Type: Data row Type: Data row Type: Data row Type: Data row Type: Command completion Type: Ready for query
残りの5件が返り、Command completionでSELECTステートメントが完了したことを示す。Portal suspendedではなくCommand completionなので、JDBCドライバはポータルを閉じる。
4.Connection#rollback()をしたとき
フロントエンドはバックエンドに次のメッセージを送る。
Type: Close Type: Close Type: Parse Type: Bind Type: Execute Type: Sync
1番目のCloseはステートメントのクローズ。2番目のCloseはポータルのクローズ。Parse、Bind、ExecuteはROLLBACKステートメントに対するもの。
Type: Close completion Type: Close completion Type: Parse completion Type: Bind completion Type: Command completion Type: Ready for query
1番目のClose completionはステートメントのクローズ。2番目のClose completionはポータルのクローズ。Parse completion、Bind completion、Command completionはROLLBACKステートメントに対応するもの
5.Connection#close()をしたとき
フロントエンドはバックエンドに次のメッセージを送る。
Type: Termination
接続を閉じる。
宿題 & 疑問
昨日の宿題
- Executeで途中でタプルを戻す場合、どこまで読んでおくのか?
- fetchSizeがあればその指定された行数に達するまでに読んだページの全タプルまで。次のResultSet#next()で新しいページを読むことになったとしても、一旦フロントエンドにデータを返す時点では先のページまでは読まない。
- スクロール可能なResultSetを使った場合は、全行をクライアント側に返しているかも?
- 返している。スクロール可能なResultSetを使った場合は、fetchSizeは無視され全件フロントエンド(クライアント側)に返される。これまで、サーバ側でデータを保持して実現していると思っていたよ。
疑問
データが複数ページにまたがっている場合、次のResultSet#next()で次のページを読む前に別のトランザクションによってデータが変えられたどうなる?文レベルの読み取り一貫性があるのなら、書き換わる前のデータが読めるはず?MVCCのスナップショットで実現?スナップショットってどんなデータ?
キャプチャするより実はJDBCドライバのログ機能を使ったほうがわかりやすかったり。。。
ログレベルをDEBUGにしておく。
org.postgresql.Driver.setLogLevel(org.postgresql.Driver.DEBUG);
すると次のようにコンソールに出力される。FEは「Front End」、BEは「Back End」のこと。
10:36:40.218 (1) PostgreSQL 8.3 JDBC3 with SSL (build 603) 10:36:40.234 (1) Trying to establish a protocol version 3 connection to 192.168.100.2:5432 10:36:40.234 (1) FE=> StartupPacket(user=postgres, database=test, client_encoding=UNICODE, DateStyle=ISO, extra_float_digits=2) 10:36:40.250 (1) <=BE AuthenticationOk 10:36:40.265 (1) <=BE ParameterStatus(client_encoding = UNICODE) 10:36:40.265 (1) <=BE ParameterStatus(DateStyle = ISO, YMD) 10:36:40.265 (1) <=BE ParameterStatus(integer_datetimes = off) 10:36:40.265 (1) <=BE ParameterStatus(is_superuser = on) 10:36:40.265 (1) <=BE ParameterStatus(server_encoding = UTF8) 10:36:40.265 (1) <=BE ParameterStatus(server_version = 8.3.5) 10:36:40.265 (1) <=BE ParameterStatus(session_authorization = postgres) 10:36:40.265 (1) <=BE ParameterStatus(standard_conforming_strings = off) 10:36:40.265 (1) <=BE ParameterStatus(TimeZone = US/Eastern) 10:36:40.265 (1) <=BE BackendKeyData(pid=3075,ckey=1822665832) 10:36:40.265 (1) <=BE ReadyForQuery(I) 10:36:40.265 (1) compatible = 8.3 10:36:40.265 (1) loglevel = 2 10:36:40.265 (1) prepare threshold = 5 getConnection returning driver[className=org.postgresql.Driver,org.postgresql.Driver@55571e] 10:36:40.296 (1) simple execute, handler=org.postgresql.jdbc2.AbstractJdbc2Statement$StatementResultHandler@1855af5, maxRows=0, fetchSize=6, flags=9 10:36:40.312 (1) FE=> Parse(stmt=S_1,query="BEGIN",oids={}) 10:36:40.312 (1) FE=> Bind(stmt=S_1,portal=null) 10:36:40.312 (1) FE=> Execute(portal=null,limit=0) 10:36:40.312 (1) FE=> Parse(stmt=S_2,query="select bid from branches",oids={}) 10:36:40.312 (1) FE=> Bind(stmt=S_2,portal=C_3) 10:36:40.312 (1) FE=> Describe(portal=C_3) 10:36:40.312 (1) FE=> Execute(portal=C_3,limit=6) 10:36:40.312 (1) FE=> Sync 10:36:40.312 (1) <=BE ParseComplete [S_1] 10:36:40.312 (1) <=BE BindComplete [null] 10:36:40.312 (1) <=BE CommandStatus(BEGIN) 10:36:40.312 (1) <=BE ParseComplete [S_2] 10:36:40.312 (1) <=BE BindComplete [C_3] 10:36:40.312 (1) <=BE RowDescription(1) 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE DataRow 10:36:40.312 (1) <=BE PortalSuspended 10:36:40.328 (1) <=BE ReadyForQuery(T) 10:36:40.328 (1) FE=> Execute(portal=C_3,limit=6) 10:36:40.328 (1) FE=> Sync 10:36:40.328 (1) <=BE DataRow 10:36:40.328 (1) <=BE DataRow 10:36:40.328 (1) <=BE DataRow 10:36:40.328 (1) <=BE DataRow 10:36:40.328 (1) <=BE DataRow 10:36:40.328 (1) <=BE CommandStatus(SELECT) 10:36:40.328 (1) <=BE ReadyForQuery(T) 10:36:40.343 (1) simple execute, handler=org.postgresql.jdbc2.AbstractJdbc2Connection$TransactionCommandHandler@eee36c, maxRows=0, fetchSize=0, flags=22 10:36:40.343 (1) FE=> CloseStatement(S_2) 10:36:40.343 (1) FE=> ClosePortal(C_3) 10:36:40.343 (1) FE=> Parse(stmt=S_4,query="ROLLBACK",oids={}) 10:36:40.343 (1) FE=> Bind(stmt=S_4,portal=null) 10:36:40.343 (1) FE=> Execute(portal=null,limit=1) 10:36:40.343 (1) FE=> Sync 10:36:40.343 (1) <=BE CloseComplete 10:36:40.343 (1) <=BE CloseComplete 10:36:40.343 (1) <=BE ParseComplete [S_4] 10:36:40.343 (1) <=BE BindComplete [null] 10:36:40.343 (1) <=BE CommandStatus(ROLLBACK) 10:36:40.343 (1) <=BE ReadyForQuery(I) 10:36:40.343 (1) FE=> Terminate