【C#】SQLiteでクエリー結果の列名を取得する

SQLを発行した結果の列名を取得する方法です。

DbDataRecordからSchemaInfoを取得して列名と型を取得しようと思います。

C#でSQLをダイレクトに実行する場合、コネクションを取得しコマンドを発行した後、SqliteDataReader で結果を読み取るのが一般的です。その後、列を foreach で DbDataRecord にして回したりしますが、結果の列名が何か気になるときが時があると思います。

この時、DbDataRecord 型を使用していますが、実際は System.Data.Common.DataRecordInternal 型が返ってきていて、この DataRecordInternal の内部にスキーマの情報が SchemaInfo型の配列として保持されています。ただ DataRecordInternal は internal 型のため通常外部からはアクセスできません。同じくスキーマ情報が記録されている SchemaInfo 型も internal なのでアクセスできません。

なのでリフレクションを使用してオブジェクトの内部データを強引に引き抜きたいと思います。

確認環境

  • VisualStudio2017
  • .NET 4.6.2
  • SQLite3

実装コード

まず、結果を入れるために SchemaInfo を以下のように定義します。

// SchemaInfo.cs
using System;

public class SchemaInfo
{
    public readonly Type Type;
    public readonly string Name = "";
    public readonly string TypeName = "";

    public SchemaInfo(Type type, string name, string typeName)
    {
        Type = type;
        Name = name;
        TypeName = typeName;
    }
}

次にデータを引き抜くための拡張メソッドを以下の通り定義します。

// DbDataRecordExtensions.cs

using System;
using System.Data.Common;
using System.Reflection;

public static class DbDataRecordExtensions
{
    public static SchemaInfo[] GetSchemaInfo(this DbDataRecord self)
    {
        // (1) System.Data.Common.SchemaInfo[] を取得する
        Type type = self.GetType();
        FieldInfo field = type.GetField("_schemaInfo",
            BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance);

        // (2) 配列を順番にアクセスしてフィールドを自作のオブジェクトに詰める
        Array schemaInfoArray = field.GetValue(self) as Array;
        SchemaInfo[] retArray = new SchemaInfo[schemaInfoArray.Length];
        Type t = null;
        for (int i = 0; i < schemaInfoArray.Length; i++)
        {
            object item = schemaInfoArray.GetValue(i);
            if (t == null) t = item.GetType();

            FieldInfo fieldName = t.GetField("name");
            string name = fieldName.GetValue(item) as string;

            FieldInfo fieldTypeName = t.GetField("typeName");
            string typeName = fieldTypeName.GetValue(item) as string;

            FieldInfo fieldType = t.GetField("type");
            Type scType = fieldType.GetValue(item) as Type;

            retArray[i] = new SchemaInfo(scType, name, typeName);
        }

        return retArray;
    }
}

実際の使用方法ですが、任意のレコードを取得する以下の実装があったとします。

internal void QueryTest(string dbPath)
{
    // テーブル定義は以下の通り
    // a (int) | b (int) | c (string)
    string query = "SELECT * FROM _table";
    
    using (var connection = new SqliteConnection(GetConnectionString(dbPath)))
    {
        connection.Open();
        
        using (SqliteCommand command = connection.CreateCommand())
        {
            command.CommandText = query;
            command.ExecuteNonQuery();

            using (SqliteDataReader reader = command.ExecuteReader())
            {
                bool once = false;
                foreach (DbDataRecord item in reader)
                {
                    if (!once) // 何度も実行しない
                    {
                        once = true;
                        // ★ここでスキーマ情報を取得する
                        SchemaInfo[] infoArray = item.GetSchemaInfo();
                        // 以下のように情報が取得できる
                        // [0] INTEGER, a
                        // [1] INTEGER, b
                        // [2] TEXT,  c
                    }
                    
                    for (int i = 0; i < item.FieldCount; i++)
                    {
                        var temp = item[i];
                        Console.Write($"{temp}, ");
                    }
                }
            }
        }
    }
}

これで列名が取得できるようになりました。