C#からSpatiaLiteを利用する


SpatiaLiteとはSQLite上でGISを扱えるようにする拡張モジュールです。
PostgreSQLに対するPostGISのような位置づけです。

さて、こいつをC#で動かそうとすると、なかなか大変だったので、忘れないように手順をメモしておきます。

前提

今回は、

  • Visual Studio (VS)でC#のプロジェクトを作る。
  • そのプロジェクトのSystem.Data.SQLite上でSpatiaLiteの拡張モジュールをロードし使う
  • x64向けビルドを作る

という前提で話を進めます。

System.Data.SQLiteの準備

まずは、VSでSQLiteが使えるように、C#のプロジェクトのNuGetでSystem.Data.SQLiteをインストールします。
また、構成マネージャを使ってx64ビルドを作れるようにしておきます。

SpatiaLiteのダウンロード

SpatiaLite本家サイト:http://www.gaia-gis.it/gaia-sins/

こちらのページの最下部にMS Windows binariesというコーナーがあるので、そこのcurrent stable versionのamd64を選択します。

ファイル一覧ページに遷移したら、mod_spatialite-4.3.0a-win-amd64.7zを選択してダウンロード。

で、この7zファイルを解凍すると、mod_spatialite.dll以下、依存するdll群がまとめて入っているわけですが、こいつが曲者。
このモジュールたちをそのまま使おうとすると、mod_spatialite.dllのロード時にlibstdc++_64-6.dllでアクセスバイオレーションが発生して落ちます。

そこで、アクセスバイオレーションの発生しないものに置き換える必要があります。

libstdc++_64-6.dllの置き換え

libstdc++_64-6.dllは、MinGW64に同梱されています。

MinGW64: https://sourceforge.net/projects/mingw-w64/?source=typ_redirect

上記サイトからダウンロードし、インストールします。こんな感じの設定でインストールしました。

インストールが完了したら、インストール先ディレクトリのbin以下の「libstdc++-6.dll」「libgcc_s_seh-1.dll」の2つのファイルをコピーし、前節のspatialiteのdll群のものと入れ替えます。

この2つのファイルをコピーします。

置き換える際、ファイル名は以下のようにしてください。

  • 「libstdc++-6.dll」はmod_spatialiteに同梱されているほうに合わせて「libstdc++_64-6.dll」にリネームする。
  • 「libgcc_s_seh-1.dll」は「libgcc_s_seh-1.dll」のままにする。

「libgcc_s_seh-1.dll」のほうもmod_spatialite同梱版に合わせた名前にリネームすると、私の環境では正しく動作しませんでした。
まぎらわしいので、もともと同梱されていた「libgcc_s_seh_64-1.dll」は削除してしまいましょう。

dll入れ替え後のディレクトリはこんな感じになります。

図中、選択されている2ファイルが入れ替えたものです。

なお、このdll入れ替えについては以下を参考にしました。
http://blog.jrg.com.br/2016/04/25/Fixing-spatialite-loading-problem/

ビルドイベントの設定

mod_spatialite.dll以下関連dll群を、VSのC#プロジェクトが出力するexeと同じディレクトリにコピーするようビルドイベントを設定します。

プロジェクトのプロパティの「ビルドイベント」から、「ビルド後イベントのコマンドライン」に以下のような感じで書いておきましょう。

copy <mod_spatialiteのあるディレクトリ>\*.dll $(ProjectDir)$(OutDir)

動作確認

さて、ではいよいよ動作確認です。以下のようなコードを実行してみましょう。

using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace cstx64
{
    class Program
    {
        private static void ExecuteNonQuery(string sql, SQLiteConnection conn)
        {
            using (var command = new SQLiteCommand(sql, conn))
            {
                command.ExecuteNonQuery();
            }
        }

        private static void ExecuteScalar(string sql, SQLiteConnection conn)
        {
            using (var command = new SQLiteCommand(sql, conn))
            {
                command.ExecuteScalar();
            }
        }

        private static void LoadExtension(SQLiteConnection conn)
        {
            var modulePath = @"mod_spatialite";
            conn.EnableExtensions(true);
            conn.LoadExtension(modulePath);
        }

        public static void Main(string[] args)
        {
            string fileName = @".\test.db";
            if (System.IO.File.Exists(fileName))
            {
                System.IO.File.Delete(fileName);
            }

            using (var conn = new SQLiteConnection(string.Format("Data Source={0};Version=3", fileName)))
            {
                conn.Open();

                LoadExtension(conn);
                string sql = " CREATE TABLE IF NOT EXISTS t (id, name, point)";
                ExecuteNonQuery(sql, conn);

                sql = "INSERT INTO t VALUES(1, 'some', ST_GeomFromText('POINTZ(1.0 2.0 3.0)'))";
                ExecuteNonQuery(sql, conn);

                sql = "SELECT id, name, ST_AsText(point) AS pt FROM t";
                using (var command = new SQLiteCommand(sql, conn))
                {
                    using (var dr = command.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            int id = Convert.ToInt32(dr["id"].ToString());
                            string name = dr["name"].ToString();
                            string pt = dr["pt"].ToString(); ;
                            Console.WriteLine(String.Format("{0}, {1}, {2}", id, name, pt));
                        }
                    }
                }
                conn.Close();
            }
        }
    }
}


LoadExtensionメソッドで、SpatiaLiteの拡張モジュールをロードしています。
ロードする前にEnableExtensionsで拡張を有効化しなければならないので注意です。

(さらにいうと、このEnableExtensionsLoadExtensionの操作は、SQLiteConnectionインスタンスごとに実行する必要があります。)

ロードが正しく行われると、その後のINSERT文、SELECT文にあるようなST_xxxの関数が利用できるようになります。
ここでは適当に座標が(1,2,3)の点をpointカラムに投入し、それが取れていることを確認しています。

1, some, POINT Z(1 2 3)

これで動作確認完了です。お疲れさまでした。