PHP
C#
SQLite3

C#でSQLite3を使ってみる

C#でSQLite3を使ってみたのでまとめ。DBが1つのファイルで完結するって何かと便利ですよね。

このへんの記事が参考になりました。

1.どのパッケージを使う?

ここが問題。NuGetで「SQLite」で検索すると、
sqlite1.png
まずどれを使っていいかわかりません。大抵「System.Data.SQLite」をインストールするのですが、これはEntity Frameworkや、SQLiteのLINQ拡張(これを入れなくてもLINQ to SQLが使える謎)など全部入った欲張りセットで、気づけば実行ファイルのディレクトリが大変なことになります。
sqlite2.png
SQLiteちょっと使いたいたいだけなのにライブラリ5MBは全然"Lite"ではないので、「System.Data.SQLite.Core」のみをインストールします(フルセットの場合はCoreも自動的にインストールされます)。SQLクエリを直に書いていくような場合はCoreだけで十分かと思います。ライブラリもかなりすっきりします。
sqlite3.png

注意:C#にはLINQを用いてSQLを実行するLINQ to SQLという機能(System.Data.LinqやSystem.Data.Linq.Mapping)があるのですが、SQLiteでTEXT型で定義したカラムに対して、LINQ経由のクエリで文字列をWHEREで比較するとNotSupportedExceptionが飛ぶという、ちょっといただけない現象があります。VisualStudio Community 2017、.NET4.6、System.Data.SQLite 1.0.106.0で確認済み。
カラム定義をNVARCHARに変えると発生しないとのこと(詳細は下記記事参照)ですが、そもそもSQLiteのデータ型にNVARCHARはなくて文字列はTEXTになるし、他言語においてTEXT型で作ったSQLiteを読ませることを考えると、ちょっとどうなの?とは思います。このTEXT型のカラムに対する例外は、LINQ経由ではなく、SQLクエリを直接書いて実行し、WHERE比較すると発生しませんでした(それはそう)。なので、入力補完が効かないデメリットはあるものの、SQLクエリを直接書くメリットはあります。

参考:System.Data.Linq.dll で NotSupportedException(解決) : http://shinta0806be.ldblog.jp/archives/9084461.html

2.SQLite3のバージョン確認

System.Data.SQLiteで使われているSQLiteのバージョンを確認します。

using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "select sqlite_version()";
                Console.WriteLine(cmd.ExecuteScalar());
            }
        }
    }
}

実行結果は(System.Data.SQLite.Coreのバージョンは1.0.106)

 3.21.0

無事、SQLite3の最新版でした。DataSourceの項目を「:memory:」にするとメモリ上に、「test.db」などにするとファイル上にデータベースを作成します。

3.INSERTとSELECT

SQLクエリをゴリゴリ書いていく場合はこうなります。文字列の結合が面倒なので独自の拡張メソッドを用意します。

using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "denco.db" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                //テーブル作成
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS denco(" +
                    "no INTEGER NOT NULL PRIMARY KEY," +
                    "name TEXT NOT NULL," +
                    "type TEXT NOT NULL," +
                    "attribute TEXT NOT NULL," +
                    "maxap INTEGER NOT NULL," +
                    "maxhp INTEGER NOT NULL," +
                    "skill TEXT)";
                cmd.ExecuteNonQuery();

                //データ追加
                cmd.InsertDenco(2, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち");
                cmd.InsertDenco(3, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー");
                cmd.InsertDenco(4, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス");
                cmd.InsertDenco(8, "天下さや", "アタッカー", "cool", 400, 240);
                cmd.InsertDenco(13, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁");
                cmd.InsertDenco(31, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ");

                //MaxAPが300以上のでんこをMaxHPで降順ソート
                cmd.CommandText = "SELECT * FROM denco WHERE maxap >= 300 ORDER BY maxhp desc";
                using (var reader = cmd.ExecuteReader())
                {
                    var dump = reader.DumpQuery();
                    Console.WriteLine(dump);
                }

                //LIKE句比較(ここがLINQとちょっと違う)
                cmd.CommandText = "SELECT * FROM denco WHERE name LIKE '新居浜%'";//新居浜~で始まる名前を抽出
                using (var reader = cmd.ExecuteReader())
                {
                    var dump = reader.DumpQuery();
                    Console.WriteLine(dump);
                }
            }
        }
    }
}

public static class SQLiteExtension
{
    public static int InsertDenco(this SQLiteCommand command, int no, string name, string type, string attr, 
        int maxap, int maxhp, string skill = null)
    {
        var skillstr = skill == null ? "null" : $"'{skill}'";
        command.CommandText = "INSERT INTO denco(no, name, type, attribute, maxap, maxhp, skill) VALUES(" +
            $"{no}, '{name}', '{type}', '{attr}', {maxap}, {maxhp}, {skillstr})";
        return command.ExecuteNonQuery();
    }

    public static string DumpQuery(this SQLiteDataReader reader)
    {
        var i = 0;
        var sb = new StringBuilder();
        while(reader.Read())
        {
            if(i==0)
            {
                sb.AppendLine(string.Join("\t", reader.GetValues().AllKeys));
                sb.AppendLine(new string('=', 8 * reader.FieldCount));
            }
            sb.AppendLine(string.Join("\t", Enumerable.Range(0, reader.FieldCount).Select(x => reader.GetValue(x))));
            i++;
        }

        return sb.ToString();
    }
}

値がnullの場合はオーバーロード用意しないとちょっと面倒かもしれません。nullの場合、闇雲に'{変数名}'とやってしまうと、空文字('')が代入されてしまいnullにはなりません。実行結果は次のようになります。

no      name    type    attribute       maxap   maxhp   skill
========================================================
4       恋浜みろく      トリックスター  heat    300     360     ダブルアクセス
2       為栗メロ        アタッカー      eco     310     300     きゃのんぱんち
8       天下さや        アタッカー      cool    400     240

no      name    type    attribute       maxap   maxhp   skill
========================================================
13      新居浜いずな    ディフェンダー  heat    290     336     重連壁
31      新居浜ありす    ディフェンダー  heat    270     350     ハッピーホリデイ

SQLiteのGUI管理ツールとして「DB Browser for SQLite」があります。上記のテーブルのDataSourceをdenco.dbとしてファイルに保存し、このソフトで見てみます。
sqlite4.png

4.REAL型の指数表記や特殊な値について

SQLクエリを直接書く場合は、REAL型のカラムで指数表記が大丈夫かな?と気になって試してみました。別にテーブルを作ります。

using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "decimal.db" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS decimal_table(" +
                    "id INTEGER PRIMARY KEY AUTOINCREMENT," +
                    "decimal REAL NOT NULL)";
                cmd.ExecuteNonQuery();

                var decimalVal = 3.14;
                Console.WriteLine(decimalVal);//3.14
                cmd.CommandText = $"INSERT INTO decimal_table(decimal) VALUES ({decimalVal})";
                cmd.ExecuteNonQuery();

                decimalVal = 31415926535897932384626.4338327950288;//指数表記になる
                Console.WriteLine(decimalVal);//3.14159265358979E+22
                cmd.CommandText = $"INSERT INTO decimal_table(decimal) VALUES ({decimalVal})";
                cmd.ExecuteNonQuery();

                cmd.CommandText = "SELECT * FROM decimal_table WHERE decimal > 10";
                var reader = cmd.ExecuteReader();
                Console.WriteLine(reader.DumpQuery());
            }
        }
    }
}

これはエラー起きませんでした。指数表記と非指数表記が混在しても大小比較できています。

3.14
3.14159265358979E+22
id      decimal
================
2       3.14159265358979E+22

DB Browser for SQLiteで見てみると、
sqlite5.png
指数表記のまま格納されています。なぜなら、SQLiteの公式ドキュメントのDataTypeを見ると、

REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.

とあり、IEEEの規格自体に指数表記が含まれるからです。当たり前といったら当たり前ですね。ただし、調子に乗ってInfinityやNaNを格納しようとしたらエラーになりました

using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS decimal_table(" +
                    "id INTEGER PRIMARY KEY AUTOINCREMENT," +
                    "decimal REAL)";
                cmd.ExecuteNonQuery();

                var decimalVal = 3.14;
                Action insertQuery = () =>
                {
                    cmd.CommandText = $"INSERT INTO decimal_table(decimal) VALUES ({decimalVal})";
                    cmd.ExecuteNonQuery();
                };
                insertQuery();//OK

                decimalVal = 31415926535897932384626.4338327950288;//指数表記になる
                insertQuery();//OK

                decimalVal = double.PositiveInfinity;
                //insertQuery();//エラー

                decimalVal = double.NegativeInfinity;
                //insertQuery();//エラー

                decimalVal = double.NaN;
                //insertQuery();//エラー

                cmd.CommandText = "SELECT * FROM decimal_table WHERE decimal > 10";
                var reader = cmd.ExecuteReader();
                Console.WriteLine(reader.DumpQuery());
            }
        }
    }
}
ハンドルされていない例外: System.Data.SQLite.SQLiteException: SQL logic error
no such column: ∞
   場所 System.Data.SQLite.SQLite3.Prepare(SQLiteConnection cnn, String strSql,
SQLiteStatement previous, UInt32 timeoutMS, String& strRemain)

±Inf,NaNに対するdouble.ToString()がよろしくなく、クエリ内で計算させるようにしたら格納できました。ちゃんと大小比較できます。

                var decimalVal = 3.14;
                Action insertQuery = () =>
                {
                    var decimalStr = decimalVal.ToString();
                    if (double.IsInfinity(decimalVal)) decimalStr = "1e10000";
                    if (double.IsNegativeInfinity(decimalVal)) decimalStr = "-1e10000";
                    if (double.IsNaN(decimalVal)) decimalStr = "0.0/0.0";
                    cmd.CommandText = $"INSERT INTO decimal_table(decimal) VALUES ({decimalStr})";
                    cmd.ExecuteNonQuery();
                };
                insertQuery();//OK

                decimalVal = 31415926535897932384626.4338327950288;//指数表記になる
                insertQuery();//OK

                decimalVal = double.PositiveInfinity;
                insertQuery();//OK

                decimalVal = double.NegativeInfinity;
                insertQuery();//OK

                decimalVal = double.NaN;
                insertQuery();//OK→ただしNULLになる

                cmd.CommandText = "SELECT * FROM decimal_table WHERE decimal > 10";
                var reader = cmd.ExecuteReader();
                Console.WriteLine(reader.DumpQuery());

SQLクエリ内で±Infを作り出すには、指数表記の桁を8バイト浮動小数点数の範囲外の値にしてしまえばいいので。NaNを手っ取り早く作るには.NETだと0で割るのですが、SQLite3で同様の計算をするとNaNではなくNULLで格納されるようです。したがって、NaNを許容するようなカラムを作りたい場合は、NOT NULL制約を外す必要があります。実行結果は次の通り。

id      decimal
================
2       3.14159265358979E+22
3       +∞

sqlite6.png

追記:プリペアドステートメントを使うとそこまで特殊な値に苦労しません。

5.LINQ to SQL

3.の例をLINQ to SQLを使って.NETっぽく書き直してみます。自分の環境では、NuGetパッケージは相変わらずSystem.Data.SQLite.Coreのみで十分で、プロジェクトの「参照の追加」からSystem.Data.Linqのアセンブリを追加します。usingはSystem.Data.SQLiteの他に、System.Data.LinqとSystem.Data.Linq.Mappingが必要です。(System.Data.SQLite.Linqの必要性とは一体)

using System.Data.SQLite;
using System.Data.Linq;
using System.Data.Linq.Mapping;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                //テーブル作成
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS denco(" +
                    "no INTEGER NOT NULL PRIMARY KEY," +
                    "name TEXT NOT NULL," +
                    "type TEXT NOT NULL," +
                    "attribute TEXT NOT NULL," +
                    "maxap INTEGER NOT NULL," +
                    "maxhp INTEGER NOT NULL," +
                    "skill TEXT)";
                cmd.ExecuteNonQuery();

                using (var context = new DataContext(cn))
                {
                    var table = context.GetTable<Denco>();

                    //データ追加
                    table.InsertOnSubmit(new Denco(2, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち"));
                    table.InsertOnSubmit(new Denco(3, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー"));
                    table.InsertOnSubmit(new Denco(4, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス"));
                    table.InsertOnSubmit(new Denco(8, "天下さや", "アタッカー", "cool", 400, 240));
                    table.InsertOnSubmit(new Denco(13, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁"));
                    table.InsertOnSubmit(new Denco(31, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ"));

                    context.SubmitChanges();

                    //MaxAPが300以上のでんこをMaxHPで降順ソート(LINQ to SQL版)
                    foreach(var q in table.Where(x => x.MaxAp >= 300).OrderByDescending(x => x.MaxHp))
                    {
                        Console.WriteLine(q.Dump());
                    }
                    Console.WriteLine();

                    //新居浜~で始まる名前を選択
                    foreach(var q in table.Where(x => x.Name.StartsWith("新居浜")))
                    {
                        Console.WriteLine(q.Dump());
                    }

                    //アタッカーを選択(ダメな例)
                    /*
                    foreach (var q in table.Where(x => x.Type == "アタッカー"))
                    {
                        Console.WriteLine(q.Dump());//NotSupportedException
                    }
                    //x.Type == "アタッカー" を string.Compare(x.Type, "アタッカー") == 0で置き換える → 同様にダメ
                    // 同様に string.Equals(x.Type, "アタッカー"))で置き換える → ダメ
                        */

                    //アタッカーを選択(一応通るけど怪しい例 多分遅い)
                    foreach (var q in table.Where(x => x.Type.StartsWith("アタッカー") && x.Type.EndsWith("アタッカー")))
                    {
                        Console.WriteLine(q.Dump());
                    }

                    //↑多分こっちのほうが無難
                    foreach(var q in table.ToArray().Where(x => x.Type == "アタッカー"))
                    {
                        Console.WriteLine(q.Dump());
                    }
                }
            }
        }
    }
}

//テーブル構造定義クラス
[Table(Name = "denco")]
public class Denco
{
    [Column(Name ="no", CanBeNull =false, DbType = "INT", IsPrimaryKey =true)]
    public int No { get; set; }
    [Column(Name ="name", CanBeNull =false, DbType ="TEXT")]
    public string Name { get; set; }
    [Column(Name ="type", CanBeNull =false, DbType ="TEXT")]
    public string Type { get; set; }
    [Column(Name ="attribute", CanBeNull =false, DbType ="TEXT")]
    public string Attribute { get; set; }
    [Column(Name ="maxap", CanBeNull =false, DbType ="INT")]
    public int MaxAp { get; set; }
    [Column(Name ="maxhp", CanBeNull =false, DbType ="INT")]
    public int MaxHp { get; set; }
    [Column(Name ="skill", CanBeNull =true, DbType ="TEXT")]
    public string Skill { get; set; }

    public Denco() { }
    public Denco(int no, string name, string type, string attribute, int maxap, int maxhp, string skill = null)
    {
        No = no;
        Name = name; Type = type; Attribute = attribute;
        MaxAp = maxap; MaxHp = maxhp;
        Skill = skill;
    }

    public string Dump()
    {
        return $"{No}\t{Name}\t{Type}\t{Attribute}\t{MaxAp}\t{MaxHp}\t{Skill}";
    }
}

しかしこれには罠があって、1.で前述の通り、SQLiteのTEXT型のカラムに対してLINQ to SQLで=比較すると「NotSupportedException」の例外が発生します

ハンドルされていない例外: System.NotSupportedException: SQL Server では、NText、
Text、Xml、または Image の各データ型の比較はハンドルされません。
   場所 System.Data.Linq.SqlClient.ValidateNoInvalidComparison.VisitBinaryOperat
or(SqlBinary bo)
   場所 System.Data.Linq.SqlClient.SqlVisitor.Visit(SqlNode node)
   場所 System.Data.Linq.SqlClient.SqlVisitor.VisitExpression(SqlExpression exp)

string.Compareやstring.Equalsを使っても不可能。StartsWithやEndsWithは通ります。なので、StartsWithとEndsWithのあわせ技で擬似的に(怪しい)Equalsを作るということはできますが、余計な判定をやっているので多分遅いです。さらに「aaaHOGEaaa」のように前後で回文になっている場合、aaaと等しいと誤検出されますので、おすすめしません(インデックスである程度絞ったのちToArray等で一旦SQLから分離し、そこで文字列比較をするのが無難かも?)。また、3.でやったように、SQLクエリを直接入力するのはもちろんOKです。結果は次のようになります。

4       恋浜みろく      トリックスター  heat    300     360     ダブルアクセス
2       為栗メロ        アタッカー      eco     310     300     きゃのんぱんち
8       天下さや        アタッカー      cool    400     240

13      新居浜いずな    ディフェンダー  heat    290     336     重連壁
31      新居浜ありす    ディフェンダー  heat    270     350     ハッピーホリデイ

2       為栗メロ        アタッカー      eco     310     300     きゃのんぱんち
8       天下さや        アタッカー      cool    400     240

テーブルをファイルに保存し、DB Browser for SQLiteで見てみると、
sqlite7.png
SkillがNULLのところはちゃんとNULLで記録されています。データのInsertでは変に意識しなくて良くていいかもしれませんね。

4.の例でもやってみます。

using System.Data.SQLite;
using System.Data.Linq;
using System.Data.Linq.Mapping;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS decimal_table(" +
                    "id INTEGER PRIMARY KEY AUTOINCREMENT," +
                    "decimal REAL)";
                cmd.ExecuteNonQuery();

                using (var context = new DataContext(cn))
                {
                    var table = context.GetTable<DecimalTableItem>();

                    table.InsertOnSubmit(new DecimalTableItem(1, 3.14));//AUTOINCREMENTがうまく動かないのでIDを直に指定する
                    table.InsertOnSubmit(new DecimalTableItem(2, 31415926535897932384626.4338327950288));
                    table.InsertOnSubmit(new DecimalTableItem(3, double.PositiveInfinity));
                    table.InsertOnSubmit(new DecimalTableItem(4, double.NegativeInfinity));
                    table.InsertOnSubmit(new DecimalTableItem(5, double.NaN));
                    context.SubmitChanges();

                    foreach(var q in table.Where(x => x.Decimal > 10))
                    {
                        Console.WriteLine(q.Dump());
                    }
                }
            }
        }
    }
}

[Table(Name ="decimal_table")]
public class DecimalTableItem
{
    [Column(Name = "id", DbType = "INT", IsPrimaryKey =true)]//IsDbGeneratedやAutoSyncをオンにしてIdを省略するとエラー
    public int Id { get; set; }
    [Column(Name ="decimal", DbType ="REAL", CanBeNull =true)]
    public double? Decimal { get; set; }

    public DecimalTableItem() { }
    public DecimalTableItem(int id, double val)
    {
        Id = id;
        Decimal = val;
    }

    public string Dump()
    {
        return $"{Id}\t{Decimal}";
    }
}

AUTOINCREMENTがAUTOではないじゃないか!?と突っ込まれそうですが、そうです。Idを省略して実行しようとしたらエラーになって投げました。Idを直に指定したら特にエラーもなく実行されました。NaNは相変わらずNULLに変換されます(なのでNOT NULL制約はダメです)。±∞やNaNのような特別な値に対して、事前に手動で処理を施さなくてよいのがメリットですが、相変わらずAUTOINCREMENTができないのが釈然としない…。実行結果とファイルへの書き出しは次のようになります。

2       3.14159265358979E+22
3       +∞

sqlite8.png

以上、LINQ to SQLになります。個人的にはSQLiteで使うとなると相当癖のある機能だなと思いました。実直にSQLクエリを書いたほうがいいかはちゃんと見極めたほうがよさそうです。

6.トランザクションを使う

6.1 トランザクションと速度

SQLiteで大量にデータを扱うときは、トランザクションを使ったほうが高速になることが一般的に多いです。ロールバックの機能もあるのですが、わかりやすい速度面から見てみます。

ただし、速度について考える場合、トランザクション以外の要素も切り離して考えなければなりません。

  • 書き出し先がメモリかHDDか?→明らかにメモリのほうが高速になりそう
  • LINQ to SQLを使うか?直にSQLクエリを書くか?→直に書いたほうが速そうだけど何とも言えなそう
  • 複数行を一気に追加するか?一行ずつ追加するか?→複数行一気のほうが速そう
  • トランザクションを明示的にbegin~commitで囲むか?→囲んだほうが速そう

ざっと4点、つまり条件は2^4=16です。3点目については、LINQ to SQLの場合は1個ずつInsertするInsertOnSubmitか、複数InsertするInsertAllOnSubmitか、SQLクエリの場合は1行のINSERT文で1行追加するか、複数の行を追加するかでそれぞれ直交できます。それぞれ、

  • ケースA:LINQ to SQLかつInsertOnSubmit(1行ずつ)で(メモリ,HDD)×(トランザクションあり、なし)の4条件
  • ケースB:LINQ to SQLかつInsertAllOnSubmit(複数行)で同4条件
  • ケースC:SQLクエリかつ1行ずつINSERTで同4条件
  • ケースD:SQLクエリかつ複数行INSERTで同4条件

みんな大好き(?)ぐるぐるループで勝負します。HDDかメモリかの部分は省略しているけど別にいいよね。IDを変えて6000レコード追加します。

ケースA
                //ケースA:LINQ to SQL、InsertOnSubmitで1文ずつ
                using (var context = new DataContext(cn))
                {
                    var table = context.GetTable<Denco>();

                    var sw = new Stopwatch();
                    sw.Start();

                    cmd.Transaction = cn.BeginTransaction();//ここのコメントアウト有無

                    for (var i = 0; i < 1000; i++)
                    {
                        table.InsertOnSubmit(new Denco(2 + i * 100, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち"));
                        table.InsertOnSubmit(new Denco(3 + i * 100, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー"));
                        table.InsertOnSubmit(new Denco(4 + i * 100, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス"));
                        table.InsertOnSubmit(new Denco(8 + i * 100, "天下さや", "アタッカー", "cool", 400, 240));
                        table.InsertOnSubmit(new Denco(13 + i * 100, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁"));
                        table.InsertOnSubmit(new Denco(31 + i * 100, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ"));
                        context.SubmitChanges();
                    }

                    cmd.Transaction.Commit();//ここのコメントアウト有無

                    sw.Stop();
                    Console.WriteLine(sw.Elapsed);
                }

ケースA

LINQ to SQL / InsertOnSubmit トランザクションあり トランザクションなし
メモリ 00:00:25.6148342 00:00:22.8112187
HDD 00:00:22.4527976 00:02:10.6239305

メモリに書き出すときはほぼ変わらないが、HDDの場合はトランザクション使わないと明らかに遅い。

ケースB
                //ケースB:LINQ to SQL、InsertAllOnSubmitで6文一気
                using (var context = new DataContext(cn))
                {
                    var table = context.GetTable<Denco>();

                    var sw = new Stopwatch();
                    sw.Start();

                    cmd.Transaction = cn.BeginTransaction();//ここのコメントアウト有無

                    for (var i = 0; i < 1000; i++)
                    {
                        var array = new Denco[6];
                        array[0] = new Denco(2 + i * 100, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち");
                        array[1] = new Denco(3 + i * 100, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー");
                        array[2] = new Denco(4 + i * 100, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス");
                        array[3] = new Denco(8 + i * 100, "天下さや", "アタッカー", "cool", 400, 240);
                        array[4] = new Denco(13 + i * 100, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁");
                        array[5] = new Denco(31 + i * 100, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ");
                        table.InsertAllOnSubmit<Denco>(array);
                        context.SubmitChanges();
                    }

                    cmd.Transaction.Commit();//ここのコメントアウト有無

                    sw.Stop();
                    Console.WriteLine(sw.Elapsed);
                }

ケースB

LINQ to SQL / InsertAllOnSubmit トランザクションあり トランザクションなし
メモリ 00:00:22.5663193 00:00:22.5614933
HDD 00:00:22.6124017 00:02:05.3266343

ケースAのとほぼ変わらない。InsertOnSubmitだろうがInsertAllOnSubmitだろうが、要はSubmitChanges()の段階で反映されているから差がないという間抜けな結果に。

次は、直にSQLクエリを実行させた場合の結果です。ほとんど変わり映えしないソースですが、LINQ to SQLは一切使っていません。

ケースC
//ケースC:SQLクエリ、1行INSERT
class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                //テーブル作成
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS denco(" +
                    "no INTEGER NOT NULL PRIMARY KEY," +
                    "name TEXT NOT NULL," +
                    "type TEXT NOT NULL," +
                    "attribute TEXT NOT NULL," +
                    "maxap INTEGER NOT NULL," +
                    "maxhp INTEGER NOT NULL," +
                    "skill TEXT)";
                cmd.ExecuteNonQuery();

                var sw = new Stopwatch();
                sw.Start();

                cmd.Transaction = cn.BeginTransaction();//ここのコメントアウト有無

                //データ追加
                for (var i=0;i<1000;i++)
                {
                    cmd.InsertOne(new Denco(2 + i * 100, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち"));
                    cmd.InsertOne(new Denco(3 + i * 100, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー"));
                    cmd.InsertOne(new Denco(4 + i * 100, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス"));
                    cmd.InsertOne(new Denco(8 + i * 100, "天下さや", "アタッカー", "cool", 400, 240));
                    cmd.InsertOne(new Denco(13 + i * 100, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁"));
                    cmd.InsertOne(new Denco(31 + i * 100, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ"));
                }

                cmd.Transaction.Commit();//ここのコメントアウト有無
                sw.Stop();

                Console.WriteLine(sw.Elapsed);
            }
        }
    }
}

public static class SQLiteExtension
{
    public static int InsertOne(this SQLiteCommand command, Denco d)
    {
        var skillstr = d.Skill == null ? "null" : $"'{d.Skill}'";
        command.CommandText = "INSERT INTO denco(no, name, type, attribute, maxap, maxhp, skill) VALUES(" +
            $"{d.No}, '{d.Name}', '{d.Type}', '{d.Attribute}', {d.MaxAp}, {d.MaxHp}, {skillstr})";
        return command.ExecuteNonQuery();
    }
}

public class Denco
{
    public int No { get; private set; }
    public string Name { get; private set; }
    public string Type { get; private set; }
    public string Attribute { get; private set; }
    public int MaxAp { get; private set; }
    public int MaxHp { get; private set; }
    public string Skill { get; private set; }

    public Denco(int no, string name, string type, string attr,
        int maxap, int maxhp, string skill = null)
    {
        No = no; Name = name; Type = type; Attribute = attr;
        MaxAp = maxap; MaxHp = maxhp; Skill = skill;
    }
}

ケースC

SQLクエリ / 1行INSERT トランザクションあり トランザクションなし
メモリ 00:00:00.0915406 00:00:00.0982675
HDD 00:00:00.2792699 00:10:38.2711959

突っ込み満載の結果になりました。まず、HDD-トランザクションなしの結果に注目。堂々の10分半です。こんなウンコード絶対書いちゃいけません。
メモリに書き出した場合は、トランザクションの有無にかかわらず似たような結果になっていて(これはSQLクエリ、LINQ to SQL関わらずそう)、LINQ to SQLの場合は22秒もかかっていましたが、直接クエリを書いた場合は0.09秒になりました。メモリに書き出したときの時間は、いくらいいSSD使おうが下回ることができない(転送速度が2桁ぐらい違う)ので、IOの軸で見た場合の極小値になります。この極小値が、LINQ to SQLを切り捨てたことで22秒から0.09秒、すなわち1/250になったわけです。本当LINQ to SQLの存在意義とは何なんでしょう?実はさらに速くなります。

ケースD
//ケースD:SQLクエリ、複数行INSERT
class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                //テーブル作成
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS denco(" +
                    "no INTEGER NOT NULL PRIMARY KEY," +
                    "name TEXT NOT NULL," +
                    "type TEXT NOT NULL," +
                    "attribute TEXT NOT NULL," +
                    "maxap INTEGER NOT NULL," +
                    "maxhp INTEGER NOT NULL," +
                    "skill TEXT)";
                cmd.ExecuteNonQuery();

                var sw = new Stopwatch();
                sw.Start();

                cmd.Transaction = cn.BeginTransaction();//ここのコメントアウト有無

                //データ追加
                for(var i=0;i<1000;i++)
                {
                    var array = new Denco[6];
                    array[0] = new Denco(2 + i * 100, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち");
                    array[1] = new Denco(3 + i * 100, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー");
                    array[2] = new Denco(4 + i * 100, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス");
                    array[3] = new Denco(8 + i * 100, "天下さや", "アタッカー", "cool", 400, 240);
                    array[4] = new Denco(13 + i * 100, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁");
                    array[5] = new Denco(31 + i * 100, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ");

                    cmd.InsertAll(array);
                }

                cmd.Transaction.Commit();//ここのコメントアウト有無
                sw.Stop();

                Console.WriteLine(sw.Elapsed);
            }
        }
    }
}

public static class SQLiteExtension
{
    public static int InsertAll(this SQLiteCommand command, IEnumerable<Denco> dencos)
    {
        //SQLite3.7.11以降可能な書き方
        var sb = new StringBuilder();
        sb.Append("INSERT INTO denco(no, name, type, attribute, maxap, maxhp, skill) VALUES");
        var n = dencos.Count();
        var i = 0;
        foreach (var d in dencos)
        {
            var skillstr = d.Skill == null ? "null" : $"'{d.Skill}'";
            sb.Append($"({d.No}, '{d.Name}', '{d.Type}', '{d.Attribute}', {d.MaxAp}, {d.MaxHp}, {skillstr})");
            if (i != n - 1) sb.AppendLine(",");
            i++;
        }
        command.CommandText = sb.ToString();
        return command.ExecuteNonQuery();
    }
}

6行まとめてINSERTします。SQLite3.7.11以降この「INSERT INTO table(column) VALUES (row1),(row2),(row3)」のように、わかりやすく複数行INSERTできるようになりました。1.で調べた通り、現在、NuGet経由で最新版のSystem.Data.SQLite.Coreをインストールすると、SQLite3.21.0が落ちてくるのでこの表記は使うことができます。

もうやめて!LINQ to SQLのライフはゼロよ!という感はありますが、
ケースD

SQLクエリ / 複数行INSERT トランザクションあり トランザクションなし
メモリ 00:00:00.0635890 00:00:00.0628868
HDD 00:00:00.2324467 00:01:37.9394088

メモリの場合は、複数行(6行)INSERTすることで、0.09秒から0.06秒と約2/3になりました。LINQ to SQLを使ってINSERTした例と比べると約1/360です。HDDでうっかりbegin~commitを忘れちゃったとしても、1分半とケースA,Bのトランザクションなしの場合の2分オーバーより、30秒短くて済みます。現実的には、HDDに書き出すことが多いので、ケースA,Bのトランザクションあり、HDDのケースと比較しますと、22.5秒対0.23秒でざっと1/100を達成できました。LINQ to SQLの存在意義とは一体……。
一応、LINQ to SQLの名誉のために補足しておきますが、今まで比較したのはINSERTのパフォーマンスのみです。置き換えや検索のパフォーマンス、LINQとのつなげやすさ等は全く考慮していません。なので、ここまで多くINSERTしない場合や普段使いでは、まだまだLINQ to SQLの出番はあると思いますよ。自分は使う気失せましたが。

かなり脱線してしまいましたが、ここまでの結果をまとめます。

  • 書き出し先がメモリ/HDDでは、当たり前だがメモリのほうが圧倒的に速い。ただし、HDDでもbegin~commitのトランザクション管理をきちんとすると、メモリに近い速度を実現することができる。
  • LINQ to SQLの使用有無はかなり速度に影響が出る。INSERTの場合、使わずにダイレクトにSQLクエリを直に書いたほうが速い。
  • 複数行/1行追加は、ダイレクトにSQLクエリを書く場合は複数行でやったほうが速い。ただし、LINQ to SQLを使う場合は、追加のタイミングがSubmitChanges()で決まるので、InsertOnSubmit/InsertAllOnSubmitのどっちで追加しても変わらない。
  • トランザクションの明示的な管理は、メモリに書き出す場合は気にしなくて良い。HDDにあり大量にINSERTする場合は、絶対にbegin~commitで囲むべき。多少乱暴なINSERTをしてもメモリに近い速度になる、保険的な立ち位置になる。

6.2 トランザクションのロールバック

トランザクションを明示的に管理する大きなメリットとして、データ挿入/変更時にエラーが発生した場合、ロールバック(巻き戻し)ができるようになります。具体例を見てみます。

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                //テーブル作成
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS denco(" +
                    "no INTEGER NOT NULL PRIMARY KEY," +
                    "name TEXT NOT NULL," +
                    "type TEXT NOT NULL," +
                    "attribute TEXT NOT NULL," +
                    "maxap INTEGER NOT NULL," +
                    "maxhp INTEGER NOT NULL," +
                    "skill TEXT)";
                cmd.ExecuteNonQuery();

                //データ追加
                var array = new Denco[6];
                array[0] = new Denco(2, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち");
                array[1] = new Denco(3, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー");
                array[2] = new Denco(4, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス");
                array[3] = new Denco(8, "天下さや", "アタッカー", "cool", 400, 240);
                array[4] = new Denco(13, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁");
                array[5] = new Denco(31, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ");
                cmd.InsertAll(array);

                //UPDATE
                cmd.Transaction = cn.BeginTransaction();
                try
                {
                    //新居浜ありすのMaxAPを280にする→OK
                    cmd.CommandText = "UPDATE denco SET maxap = 280 WHERE no = 31";
                    cmd.ExecuteNonQuery();
                    cmd.Transaction.Commit();
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    cmd.Transaction.Rollback();
                }

                cmd.Transaction = cn.BeginTransaction();
                try
                {
                    //新居浜いずなの属性をnullにする→NOT NULL制約に引っかかるのでNG
                    cmd.CommandText = "UPDATE denco SET attribute = null WHERE no = 13";
                    cmd.ExecuteNonQuery();
                    cmd.Transaction.Commit();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    cmd.Transaction.Rollback();
                }

                //DumpALL
                cmd.CommandText = "SELECT * FROM denco";
                Console.WriteLine(cmd.ExecuteReader().DumpQuery());//最初のUPDATE完了時にロールバックされている
            }
        }

UPDATEを2回かけ、それぞれのトランザクションに対してエラー時にロールバックするようにさせています。1回目のUPDATEは、新居浜ありすのmaxapを270から280に変えるもので、データベース上これは何も問題ありません。2回目のUPDATEは、新居浜いずなのattributeをnullに変えるものですが、テーブル定義からattributeカラムは「NOT NULL制約」があるので、これは失敗します。したがってロールバックが発動し、1回目のUPDATE完了時のデータに戻ります。最後に"SELECT * FROM denco"で参照しているのは、ロールバック後のデータです。実行結果は次のようになります。

constraint failed
NOT NULL constraint failed: denco.attribute
no      name    type    attribute       maxap   maxhp   skill
========================================================
2       為栗メロ        アタッカー      eco     310     300     きゃのんぱんち
3       新阪ルナ        ディフェンダー  cool    220     360     ナイトライダー
4       恋浜みろく      トリックスター  heat    300     360     ダブルアクセス
8       天下さや        アタッカー      cool    400     240
13      新居浜いずな    ディフェンダー  heat    290     336     重連壁
31      新居浜ありす    ディフェンダー  heat    280     350     ハッピーホリデイ

7. C#で作ったSQLiteのDBファイルをPHPで読む

ここからは他言語間との連携を見てみます。その例として、SQLiteと親和性の高いPHP(SQLiteが付属でついてくる)との連携を考えます。主に共用サーバーなど、PHPは使えるものの、MySQLが使えなかったり数に制限があったりする場合にSQLiteは重宝することが多いです。

例はこれまでさんざん出したdencoのテーブルを使います。C#側で「denco.db」として保存しておきます(何度も出ているのでソースは省略)。これをPHPのプログラム(index.phpとします)と同一のディレクトリに置きます。PHPの実行環境はPHP7.2.1、WindowsのXAMPPで起動し、ブラウザからPHPファイルを読み込ませています。

index.php
<?php
$db = new SQLite3("denco.db");
$query = $db->query("SELECT * FROM denco");
while($result = $query->fetchArray(SQLITE3_ASSOC))
{
    var_dump($result);
    echo "<br/>";
}
$db->close();

この実行結果は以下の通り。特に問題なく読み込めています。

array(7) { ["no"]=> int(2) ["name"]=> string(12) "為栗メロ" ["type"]=> string(15) "アタッカー" ["attribute"]=> string(3) "eco" ["maxap"]=> int(310) ["maxhp"]=> int(300) ["skill"]=> string(21) "きゃのんぱんち" } 
array(7) { ["no"]=> int(3) ["name"]=> string(12) "新阪ルナ" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "cool" ["maxap"]=> int(220) ["maxhp"]=> int(360) ["skill"]=> string(21) "ナイトライダー" } 
array(7) { ["no"]=> int(4) ["name"]=> string(15) "恋浜みろく" ["type"]=> string(21) "トリックスター" ["attribute"]=> string(4) "heat" ["maxap"]=> int(300) ["maxhp"]=> int(360) ["skill"]=> string(21) "ダブルアクセス" } 
array(7) { ["no"]=> int(8) ["name"]=> string(12) "天下さや" ["type"]=> string(15) "アタッカー" ["attribute"]=> string(4) "cool" ["maxap"]=> int(400) ["maxhp"]=> int(240) ["skill"]=> NULL } 
array(7) { ["no"]=> int(13) ["name"]=> string(18) "新居浜いずな" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "heat" ["maxap"]=> int(290) ["maxhp"]=> int(336) ["skill"]=> string(9) "重連壁" } 
array(7) { ["no"]=> int(31) ["name"]=> string(18) "新居浜ありす" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "heat" ["maxap"]=> int(270) ["maxhp"]=> int(350) ["skill"]=> string(24) "ハッピーホリデイ" } 

8.PHPで作ったSQLiteのDBファイルをC#で読む

今度は7.の逆です。PHPでDBファイルを作ってから、C#に読み込ませます。同種のDBをPHPで作ると次のようになります。

index.php
<?php
class DencoDb extends SQLite3
{
    function __construct()
    {
        $this->open("denco_php.db");
        //テーブルを作る
        $this->exec("CREATE TABLE IF NOT EXISTS denco(
            no INTEGER NOT NULL PRIMARY KEY,
            name TEXT NOT NULL,
            type TEXT NOT NULL,
            attribute TEXT NOT NULL,
            maxap INTEGER NOT NULL,
            maxhp INTEGER NOT NULL,
            skill TEXT)");
    }

    function insertAll()
    {
        $data = [
            [2, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち"],
            [3, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー"],
            [4, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス"],
            [8, "天下さや", "アタッカー", "cool", 400, 240, null],
            [13, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁"],
            [31, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ"]
        ];

        $stmt = $this->prepare("INSERT INTO denco(no, name, type, attribute, maxap, maxhp, skill) 
            VALUES(?, ?, ?, ?, ?, ?, ?)");
        foreach($data as $params)
        {
            $stmt->bindParam(1, $params[0], SQLITE3_INTEGER);
            $stmt->bindParam(2, $params[1], SQLITE3_TEXT);
            $stmt->bindParam(3, $params[2], SQLITE3_TEXT);
            $stmt->bindParam(4, $params[3], SQLITE3_TEXT);
            $stmt->bindParam(5, $params[4], SQLITE3_INTEGER);
            $stmt->bindParam(6, $params[5], SQLITE3_INTEGER);
            $stmt->bindParam(7, $params[6], SQLITE3_TEXT);

            $stmt->execute();
        }
    }

    function dumpAll()
    {
        $query = $this->query("SELECT * FROM denco");
        while($result = $query->fetchArray(SQLITE3_ASSOC))
        {
            var_dump($result);
            echo "<br/>";
        }
    }
}

$db = new DencoDb();
$db->insertAll();
$db->dumpAll();
$db->close();

ファイル名は"denco_php.db"として保存します。ダンプ結果は以下の通り。

array(7) { ["no"]=> int(2) ["name"]=> string(12) "為栗メロ" ["type"]=> string(15) "アタッカー" ["attribute"]=> string(3) "eco" ["maxap"]=> int(310) ["maxhp"]=> int(300) ["skill"]=> string(21) "きゃのんぱんち" } 
array(7) { ["no"]=> int(3) ["name"]=> string(12) "新阪ルナ" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "cool" ["maxap"]=> int(220) ["maxhp"]=> int(360) ["skill"]=> string(21) "ナイトライダー" } 
array(7) { ["no"]=> int(4) ["name"]=> string(15) "恋浜みろく" ["type"]=> string(21) "トリックスター" ["attribute"]=> string(4) "heat" ["maxap"]=> int(300) ["maxhp"]=> int(360) ["skill"]=> string(21) "ダブルアクセス" } 
array(7) { ["no"]=> int(8) ["name"]=> string(12) "天下さや" ["type"]=> string(15) "アタッカー" ["attribute"]=> string(4) "cool" ["maxap"]=> int(400) ["maxhp"]=> int(240) ["skill"]=> NULL } 
array(7) { ["no"]=> int(13) ["name"]=> string(18) "新居浜いずな" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "heat" ["maxap"]=> int(290) ["maxhp"]=> int(336) ["skill"]=> string(9) "重連壁" } 
array(7) { ["no"]=> int(31) ["name"]=> string(18) "新居浜ありす" ["type"]=> string(21) "ディフェンダー" ["attribute"]=> string(4) "heat" ["maxap"]=> int(270) ["maxhp"]=> int(350) ["skill"]=> string(24) "ハッピーホリデイ" } 

sqlite9.png
INSERT時のNULLの扱い方がC#より全然楽そうですね。ここでは扱いませんでしたが、PHPのSQLiteでももちろんトランザクション管理はできます。
これをC#のプログラムのあるディレクトリにコピーして読み込ませます。読み込むだけじゃつまらないので、MaxAp降順でソートしてみましょうか。

Program.cs
using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "denco_php.db" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "SELECT * FROM denco ORDER BY maxap desc";
                Console.WriteLine(cmd.ExecuteReader().DumpQuery());
            }
        }
    }
}

DumpQuery()は以前定義した拡張メソッドです。ダンプしたときに5行目と6行目の間に謎のスペースが入っていますが、DB上は特に問題ないようです。

no      name    type    attribute       maxap   maxhp   skill
========================================================
8       天下さや        アタッカー      cool    400     240
2       為栗メロ        アタッカー      eco     310     300     きゃのんぱんち
4       恋浜みろく      トリックスター  heat    300     360     ダブルアクセス
13      新居浜いずな    ディフェンダー  heat    290     336     重連壁
31      新居浜ありす    ディフェンダー  heat    270     350     ハッピーホリデイ

3       新阪ルナ        ディフェンダー  cool    220     360     ナイトライダー

無事読み込むことができました。

これで、C#←→PHPのSQLiteを通じた(擬似的な)相互のデータなやり取りができるようになったので、例えばPHPプログラム=サーバー、C#プログラムをクライアントとして、データのやり取りをSQLiteファイルで行うということもできるようになります。実は似たようなことを考えている方が既にいました。

参考:アプリとサーバーの通信にJSONではなくSQLiteを使うと幸せになれるかも知れない条件まとめ http://animane.hatenablog.com/entry/2015/10/12/152533

今回はこれで終わりにしますが、C#でSQLiteを使う、もといSQLiteの日本語情報がまだまだ少ないので、もっとSQLite使う人増えたらいいかなと思います。

大変長い投稿になってしまいましたが、ここまで読んでいただきありがとうございました。

9.(追記)C#でプリペアドステートメントを使う

8.のPHPプログラムでSQLite3::prepare(…)と使ったの、プリペアドステートメント(PreparedStatement)という機能です。調べたらC#のSystem.Data.SQLiteでも使えました。これを使うとnullやInfなど特殊な値について苦労することが減りそうです。例を示します。

using System.Data.SQLite;

class Program
{
    static void Main(string[] args)
    {
        var sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "decimal2.db" };

        using (var cn = new SQLiteConnection(sqlConnectionSb.ToString()))
        {
            cn.Open();

            using (var cmd = new SQLiteCommand(cn))
            {
                cmd.CommandText = "CREATE TABLE IF NOT EXISTS decimal(" +
                    "id INTEGER NOT NULL PRIMARY KEY," +
                    "value REAL," +
                    "str TEXT)";
                cmd.ExecuteNonQuery();

                var values = new double[] { 3.14, 31415926535897932384626.4338327950288, double.PositiveInfinity, double.NegativeInfinity, double.NaN };
                var strs = new string[] { "pi", "exppi", "infinity", "-infinity", null };

                cmd.Transaction = cn.BeginTransaction();

                cmd.CommandText = "INSERT INTO decimal VALUES (?,?,?)";//プリペアドステートメント
                foreach(var i in Enumerable.Range(0, values.Length))
                {
                    cmd.Parameters.Clear();//必須

                    var id = new SQLiteParameter { DbType = System.Data.DbType.Int32, Value = i + 1 };
                    cmd.Parameters.Add(id);
                    var value = new SQLiteParameter { DbType = System.Data.DbType.Double, Value = values[i] };
                    cmd.Parameters.Add(value);
                    var str = new SQLiteParameter { DbType = System.Data.DbType.String, Value = strs[i] };
                    cmd.Parameters.Add(str);

                    cmd.ExecuteNonQuery();
                }

                cmd.Transaction.Commit();
            }
        }
    }
}

Parametersを追加のたびにClearすることに気をつけていれば特に難しいことはなさそうです。以下のdecimal2.dbが出来ます。
sqlite10.png
特に事前処理を施すことなく、nullや±∞, NaNの値もそのままInsertすることができました(相変わらずNaNはnullに置き換えられます)。

パフォーマンスはどうでしょうか?6.1で比較した複数行の例(ケースD)と比較します。ケースDのコードとほとんど変わらないので、コードは一部のみで。トランザクションはどうせ切ったところでHDDで激遅になるのがわかりきっているので、使う(独自に管理する)ものとします。

                var sw = new Stopwatch();
                sw.Start();

                cmd.Transaction = cn.BeginTransaction();

                //データ追加
                for (var i = 0; i < 1000; i++)
                {
                    var array = new Denco[6];
                    array[0] = new Denco(2 + i * 100, "為栗メロ", "アタッカー", "eco", 310, 300, "きゃのんぱんち");
                    array[1] = new Denco(3 + i * 100, "新阪ルナ", "ディフェンダー", "cool", 220, 360, "ナイトライダー");
                    array[2] = new Denco(4 + i * 100, "恋浜みろく", "トリックスター", "heat", 300, 360, "ダブルアクセス");
                    array[3] = new Denco(8 + i * 100, "天下さや", "アタッカー", "cool", 400, 240);
                    array[4] = new Denco(13 + i * 100, "新居浜いずな", "ディフェンダー", "heat", 290, 336, "重連壁");
                    array[5] = new Denco(31 + i * 100, "新居浜ありす", "ディフェンダー", "heat", 270, 350, "ハッピーホリデイ");

                    cmd.InsertAllWithPrepared(array);//ここがInsertAllからInsertAllWithPreparedに変更
                }

                cmd.Transaction.Commit();
                sw.Stop();

                Console.WriteLine(sw.Elapsed);

ケースDのInsertAllをInsertAllWithPreparedに変えただけです。このInsertAllWithPreparedは独自に定義した拡張メソッドで、

    public static int InsertAllWithPrepared(this SQLiteCommand command, IEnumerable<Denco> dencos)
    {
        //SQLite3.7.11以降可能な書き方+プリペアドステートメント

        //プリペアドステートメント
        var sb = new StringBuilder();
        sb.Append("INSERT INTO denco(no, name, type, attribute, maxap, maxhp, skill) VALUES");
        var n = dencos.Count();
        var i = 0;
        foreach (var d in dencos)
        {
            sb.Append("(");
            sb.Append(string.Join(",", Enumerable.Repeat("?", 7)));
            sb.Append(")");
            if (i != n - 1) sb.AppendLine(",");
            i++;
        }
        command.CommandText = sb.ToString();

        //パラメーター追加
        command.Parameters.Clear();
        foreach (var denco in dencos)
        {
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.Int32, Value = denco.No });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.String, Value = denco.Name });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.String, Value = denco.Type });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.String, Value = denco.Attribute });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.Int32, Value = denco.MaxAp });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.Int32, Value = denco.MaxHp });
            command.Parameters.Add(new SQLiteParameter { DbType = System.Data.DbType.String, Value = denco.Skill });
        }
        return command.ExecuteNonQuery();
    }

これをHDD、メモリの両方で試し、ケースD(プリペアドステートメント無)の場合と処理速度を比較します。以下のプリペアドステートメント無しの値はケースDの再掲です。

複数行INSERT プリペアドステートメント有 プリペアドステートメント無
メモリ 00:00:00.1298457 00:00:00.0635890
HDD 00:00:00.2325673 00:00:00.2324467

メモリに書き出した場合のみ、処理時間がおよそ倍になっています。プリペアドステートメントの内部実装までは知らないのですが、素人目的に見てライブラリ側のParametersコレクションに追加したり、プリペアドステートメントの文字列結合で遅くなっているのかなと想像。HDDに書き出した場合ではほぼ誤差レベルなので、この程度ならIOの転送速度の差で吸収できるレベルだと思います(追加するレコードの件数によりけりですが)。プリペアドステートメントを使うと、nullや∞など特殊な値について事前処理を施さなくてよいメリットを考えると、これはかなりアリなのではないでしょうか。全く同じ目的でLINQ to SQLを使うよりかははるかに速いです。