NpgsqlはC#でPostgreSQLを使用する.Netオープンソースライブラリである。 少し、触れてみた限りでは使い方は予想以上に簡単であった。
今回は、ダウンロードした Npgsql.dll を実行ファイルと同じディレクトリにコピーした。
今回のプログラム開発では、カレントディレクトリを c:\gis として、 c:\gis\src にアプリケーションプログラムのソースファイル、c:\gis\bin に実行ファイルを置く。
まず、PostgreSQLデータベースとの接続テストのみを行う。 当然、**** のところには、実際のパスワードを記述する。
ここでは、using System.Data は不要であるが、実用レベルのプログラムでは必要となる。
// sql01.cs // c:\gis>csc /out:bin\sql01.exe /r:bin\Npgsql.dll src\sql01.cs using System; //using System.Data; using Npgsql; public class NpgsqlTest { public static void Main(String[] args) { NpgsqlConnection conn = new NpgsqlConnection("Server=127.0.0.1;Port=5432;User Id=postgres;Password=****;Database=osm20150611;"); conn.Open(); NpgsqlCommand command = new NpgsqlCommand("select version()", conn); String serverversion; try { serverversion = (String)command.ExecuteScalar(); Console.WriteLine("PostgreSQL server version: {0}", serverversion); } finally { conn.Close(); } } }
コンパイル・実行結果を下に示す。
c:\gis>csc /out:bin\sql01.exe /r:bin\Npgsql.dll src\sql01.cs Microsoft (R) Visual C# Compiler version 12.0.31101.0 for C# 5 Copyright (C) Microsoft Corporation. All rights reserved. c:\gis>sql01 PostgreSQL server version: PostgreSQL 9.4.1, compiled by Visual C++ build 1800, 64-bit
もう一つ、ネット検索で見つかったプログラムを試した。 (当然、Database、User ID、 Password、およびテーブル名は変更した)
// sql02.cs // csc /out:bin\sql02.exe /r:bin\Npgsql.dll src\sql02.cs using System; //using System.Data; using Npgsql; public class NpgsqlTest { public static void Main(String[] args) { NpgsqlConnection conn = new NpgsqlConnection("Server=127.0.0.1;Port=5432;User Id=postgres;Password=****;Database=osm20150611;"); conn.Open(); NpgsqlCommand command = new NpgsqlCommand("SELECT COUNT(*) FROM planet_osm_point", conn); // Execute the query and obtain the value of the first column of the first row Int64 count = (Int64)command.ExecuteScalar(); Console.Write("{0}\n", count); conn.Close(); } }
実行結果を下に示す。 データベース osm20150611 のテーブル planet_osm_point のレコード件数は 824893 という結果が得られた。
c:\gis>sql02 824893
テーブルplanet_osm_pointの列の数を求めるには、次のクエリを用いる。
SELECT count(*) FROM information_schema.columns WHERE table_name = 'planet_osm_point'
テーブルplanet_osm_pointに新しい列 shortname (text型)を追加するには、次のクエリを実行する。
var cmd = "ALTER TABLE planet_osm_point ADD shortname text;"; var cmdAdd = new NpgsqlCommand(strAdd, conn); cmdAdd.ExecuteNonQuery();
位置は指定できない。末尾に追加されていることを確認した。
通常の用途では、データベースの作成、削除は頻繁に実行するわけではない。 このため、自作地図システムでは、データベースの作成、削除は pgAdmin III で行ってきた。
今回、Just In Time で狭い範囲に対して OSMデータをダウンロードして、 それを PostgreSQL+PostGIS データベースにインポートして、Mapnikを使ってタイル画像を作成することを 考える。データベースは予め、手作業で作っておいてもよいが、ワンクリックで全ての操作を行うために、 データベースの作成や削除も Npgsql を使ってプログラムで行うことを試みたい。
PostgreSQLをインストールすると、データベース pgsql が必ず作られている。 データベースを作成するときには、このデータベース pgsql に接続する。
下にテストプログラムを示す。データベースの作成(CREATE DATABASE)およびコメントの追加(COMMENT ON DATABASE データベース名 IS)、 データベースの削除(DROP DATABASE)を確認した。
後半では、作成したデータベース test01 に接続し、SQL文「CREATE EXTENSION postgis;」を実行した。 これはタイル画像作成では PostGIS拡張を使うため、その準備命令である。
このようにして、データベースを作成してから osm2pgsql.exe を実行して OSMデータ(XML)をインポートする。
// npgsql01.cs // c:\gis>csc /r:bin\Npgsql.dll sys\src\misc\npgsql01.cs using System; using Npgsql; public class NpgsqlTest { public static void Main(String[] args) { NpgsqlConnection conn = new NpgsqlConnection("Server=127.0.0.1;Port=5432;User Id=postgres;Password=****;Database=postgres;"); conn.Open(); try { NpgsqlCommand command = new NpgsqlCommand("DROP DATABASE IF EXISTS test01", conn); command.ExecuteNonQuery(); command = new NpgsqlCommand("CREATE DATABASE test01", conn); command.ExecuteNonQuery(); string comment = "'Test Database " + DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss") + "'"; command = new NpgsqlCommand("COMMENT ON DATABASE test01 IS " + comment, conn); command.ExecuteNonQuery(); } finally { conn.Close(); } conn = new NpgsqlConnection("Server=127.0.0.1;Port=5432;User Id=postgres;Password=****;Database=test01;"); conn.Open(); try { NpgsqlCommand command = new NpgsqlCommand("CREATE EXTENSION postgis;", conn); command.ExecuteNonQuery(); } finally { conn.Close(); } } }
select文の事例(抜粋)を下に示す。 このプログラムはタブレット(Windows)で動かすときとノートPCで動かすときがある。
ノートPC(Windows)は仮想マシンでubuntuを動かしている(IPドレス192.168.56.2)。 PostgreSQLはこのゲストOS上で動いている。
この sqlコマンドはホストOS(Windows)からゲストOS(ubuntu)に送り込まれ、どこで実行される。
この sql文には含まれないが、もし、ファイルが含まれる場合には、 Windows上のファイルではなく、ubuntu上のファイルでなければならない。
string sql = "select ST_X(ST_Centroid(ST_Transform(way,4326))) as lon, " + "ST_Y(ST_Centroid(ST_Transform(way,4326))) as lat, name " + "from my_osmdata where name like '%" + word + "%' limit 1000; "; NpgsqlConnection conn = new NpgsqlConnection("Server=" + (GIS.PC == "tb" ? "127.0.0.1" : "192.168.56.2") + ";Port=5432;User Id=postgres;Password=****;Database=osm;"); conn.Open(); var list = new List<Node>(); try { NpgsqlCommand cmd = new NpgsqlCommand(sql, conn); var dr = cmd.ExecuteReader(); while (dr.Read()) { Node node = new Node(); node.lon = dr["lon"] != DBNull.Value ? (double)dr["lon"] : 0.0; node.lat = dr["lat"] != DBNull.Value ? (double)dr["lat"] : 0.0; node.name = (string)dr["name"]; list.Add(node); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
上の例で、dr["lon"] を文字列として取り出したいときは dr["lon"].ToString() とする。 もちろん ((double)dr["lon"]).ToString() でも誤りではない。
実際のメソッド全体を載せたので行数が多くなったが、骨子はブルーで示した。 ここではトランザクションを使っている。 command.ExecuteNonQuery()で update文が実行されるが、 結果がデータベースに反映されるのは、transaction.Commit() を実行したときである。 command.ExecuteNonQuery()でエラーが起きたときは、その更新は transaction.Rollback() で取り消される。
下のプログラムでは日本地図領域を小さい区画に分割して、繰り返し処理を行っている。 transaction.Commit()は区画ごとに行っているため、 エラーが起きたときはその区画の処理が取り消されるだけで、 すでに Commitした区画の更新処理は取り消されない。
もし、エラーの頻発が予想される場合には、どこまで実行したかを記録して置き、 そこから再開するように配慮すべきであるが、このプログラムでのエラーは稀なため、 その配慮はしていない。ただし、複数回実行しても問題はないようになっている。
一度、updateクエリでタイムアウトエラーが起きた。小区画でも、 東京、大阪などでは実行時間が大きくなる。 そのため、CommandTimeout = 0 を追加した。 これは、実行時間無制限を意味し、タイムアウトエラーは起きなくなる。
一般に、巨大な update処理全体を一つのトランザクションにするのは避けるべきである。 そうしないと、一時的に使用されるディスクスペースが巨大となり、実行時間も長くなる。
ただし、このプログラムの場合は、トランザクション・サイズの都合で小区画に分割したのではない。 このupdateクエリの実行時間はレコード数Nの二乗のオーダーになる。このupdate時間短縮のために分割している。
クエリ実行時ではなく、コネクション・タイムアウトも起こりうる。 何年か前に経験した。対策方法は、再度経験したときに、追記する。
public static void DetectDuplicationThread(Object obj) { Stopwatch sw = Stopwatch.StartNew(); int zoom = (int)obj; int count = 0; int NN = (int)Math.Pow(2, zoom); for (int x = 0; x < NN; x++) { bool fPrn = true; for (int y = 0; y < NN; y++) { if (!Zone.IsJapan(zoom,x,y)) { continue; // 日本地図領域外 } if (fPrn) { Monitor.WriteLine("DetectDup x=" + x); fPrn = false; } double lon1 = Zone.X2Lon(x, zoom); double lat1 = Zone.Y2Lat(y+1, zoom); double lon2 = Zone.X2Lon(x+1, zoom); double lat2 = Zone.Y2Lat(y, zoom); var bbox = "Box2D(ST_Transform(ST_GeomFromText('LINESTRING(" + string.Format("{0} {1}, {2} {3}", lon1, lat1, lon2, lat2) + ")', 4326), 900913)) "; var sql = "update my_osmdata small set tags = " + "(case when small.tags is null then '' else small.tags end) || hstore('dup','yes') " + "from my_osmdata big " + "where (small.way && " + bbox + ") and (big.way && " + bbox + ") " + "and small.tags->'dup' is null and big.way_area > small.way_area " + "and small.key = big.key and small.val = big.val " + "and big.name is not null and my_match(big.name, small.name, " + "case when small.key='amenity' then small.val else null end) " + "and ST_Within(small.way, big.way);"; var ConnectionString = "Server=192.168.56.2;Port=5432;" + "User Id=postgres;Password=****;Database=osm;"; var connString = new NpgsqlConnectionStringBuilder(ConnectionString) { CommandTimeout = 0 }; using (var conn = new NpgsqlConnection(connString)) { conn.Open(); using (var transaction = conn.BeginTransaction()) { var command = new NpgsqlCommand(sql, conn); try { command.ExecuteNonQuery(); transaction.Commit(); } catch (NpgsqlException) { transaction.Rollback(); throw; } } } count++; } } sw.Stop(); Monitor.WriteLine("重複検出 回数: " + count + ", 時間: " + sw.Elapsed.ToString()); }
マイOSM地図では下に示すカラムからなるテーブル my_osmdata に現在約200万レコードを登録している。
CREATE TABLE my_osmdata ( osm_id text, way_area real, key text, val text, name text, tags hstore, z_order int, way geometry(Geometry,3857) )
1レコードずつインサートする場合、パラメータを使うのが一番楽である。
var cmd = new NpgsqlCommand("insert into my_osmdata(id, name) values (:osm_id, :name)", con); cmd.Parameters.Add(new NpgsqlParameter("osm_id", DbType.String) { Value = "abc" }); cmd.Parameters.Add(new NpgsqlParameter("name", DbType.String) { Value = "Test" }); cmd.ExecuteNonQuery();
しかし、1レコードずつの登録では時間がかかりすぎる。 一度に複数レコードを登録するには、次のようにする。 この場合もパラメータを使う方法があるかも知れないが、ちょっと思いつかなかった。
INSERT INTO my_osmdata (osm_id, name) VALUES ('n123', 'abc'), ('w456', 'def'), : ('r555', 'xyz');
そのため、今回はパラメータを使わずに次のようにした。 1000レコード毎に一括登録するようにした。当然、最後は端数レコードの登録となる。
geometry型もhstore型も文字列形式であるが、 それぞれ、 '...'::geometry、'...'::hstore のようにデータ型を明記する必要がある。
文字列の中に '(一重引用符)を含む場合には、エスケープ文字 ' を付ける必要がある。 更に頭に e を付けて、 e'...' の形にする必要がある。
以上のように、パラメータ型とは異なる配慮が必要となる。
string insert = "insert into my_osmdata(osm_id, way, way_area, key, val, name, tags, z_order) values "; StringBuilder sb = new StringBuilder(); sb.Append(insert); int cntRecord = 0; while (...) { // レコード算出(省略) if (cntRecord > 0) sb.Append(", "); sb.Append("(" + osm_id + ", "); sb.Append("'" + way + "'::geometry, "); sb.Append(way_area + ", "); sb.Append("'" + key + "', "); sb.Append("e'" + val.Replace("'","''") + "', "); if (name == null) sb.Append("null, "); else sb.Append("e'" + name.Replace("'","''") + "', "); sb.Append("'" + tags + "'::hstore, "); if (z_order == DBNull.Value) sb.Append("null) "); else sb.Append(z_order + ") "); if (++cntRecord%1000 == 0) { sb.Append(";"); var sql = sb.ToString(); var cmdIns = new NpgsqlCommand(sql, conn_insert); cmdIns.ExecuteNonQuery(); sb = new StringBuilder(); sb.Append(insert); cntRecord = 0; } } if (cntRecord > 0) { sb.Append(";"); var cmdIns = new NpgsqlCommand(sb.ToString(), conn_insert); cmdIns.ExecuteNonQuery(); }