新米エンジニアの失敗再発防止メモ

自分そしてこの世界の皆が、同じ失敗をしないためのメモ

Twitterやってます!@rakuton_t
欲しいものリストのブタメンを送ってくれた方、ありがとうございます!

SqlBulkCopyの使い方 ~Linqによる遅延評価とIDataReaderによるメモリの節約~

以前投稿したこちらの記事では、SqlBulkCopyを使用することによって、数十万件のデータを短時間で一括insertする方法を紹介した。
maitakeramen.hatenablog.com

しかし、例えば数十万行とある郵便番号のデータなどをinsertしようとした場合、
約100MBほどのメモリを占有することになった。(計測した。)
これは一度に大量のデータをメモリに格納する必要があるためだ。
これを回避するための方法を紹介する。

今回、例としてやってみる流れ

テキストファイルを読み込むための列挙子オブジェクト(Enumerable型オブジェクト)を取得
Enumerable型は、一度に全てのデータを読み込まず、1行ずつ反復処理することをサポートする。

当記事では前準備として、EnumerableクラスをIDataReaderを使える拡張メソッドを実装する。
この拡張メソッドの返り値を引数とし、SqlBulkCopyする。
IDataReaderインターフェースは順方向専用ストリームを読み取る手段を提供する。

前準備

IDataReaderインターフェースを利用したEnumerableクラスの拡張メソッドを作成する。

using System;
using System.Data;
using System.Collections.Generic;
using System.Reflection;

namespace BulkTest
{
    /// 
    /// Enumerable型の拡張クラス
    /// 
    static class EnumerableExtention
    {
        /// 
        /// IDataReader型に変換して取得
        /// 
        /// 変換元の型
        /// 変換元
        /// IDataReader型に変換されたオブジェクト
        public static IDataReader AsDataReader(this IEnumerable source)
        {
            return new EnumerableDataReader(source);
        }
    }

    /// 
    /// Enumerable型のオブジェクトにIDataReaderの機能を実装
    /// 
    /// 
    class EnumerableDataReader : IDataReader
    {
        /// IDataReaderの機能を実装するオブジェクト
        private readonly IEnumerator m_source;
        /// プロパティ名の一覧
        private readonly List m_listProp = new List();

        /// 
        /// コンストラクタ
        /// 
        /// IDataReaderの機能を実装するオブジェクト
        internal EnumerableDataReader(IEnumerable source)
        {
            m_source = source.GetEnumerator();

            m_listProp.AddRange(typeof(TSource).GetProperties());
        }

        /// 
        /// 要素を取得し、ポインタを次に進める
        /// 
        /// 
        public bool Read()
        {
            return m_source.MoveNext();
        }

        /// 
        /// フィールド数を取得
        /// 
        public int FieldCount
        {
            get { return m_listProp.Count; }
        }

        /// 
        /// 値を取得する
        /// 
        /// 
        /// 
        public object GetValue(int i)
        {
            return m_listProp[i].GetValue(m_source.Current, null);
        }

        /* 以下、IDataReaderで必要なメソッドを実装する、こんな感じで */
        /// 
        /// IDataReaderの実装、例外を返すだけ
        /// 
        public int Depth
        {
            get { throw new NotImplementedException(); }
        }
    }
}

上記で作成した拡張メソッドAsDataReaderを利用し、SqlBulkCopy

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections.Generic;
using System.Reflection;
using System.IO;
using System.Linq;

namespace BlogTest
{
	class Program
	{
		static void Main(string[] args)
		{
			var lines = File.ReadLines("data.txt");

			// Linqで反復して読み込むデータの構造を定義する。
			var insertData = lines.Select(s =>
			{
				// テーブルと同じ構造のモデルにデータを詰め込む(テーブルのカラムとプロパティは順番が同じであること)
				var columns = s.Split(',');
				var tableModel = new TableModel
					{
						Id = columns[0],
						ColumnA = columns[1],
						ColumnB = columns[2] + "接尾辞" // 無加工じゃ味気ないので
					};
				return tableModel;
			});

			var connectionString = "接続文字列";
			using (var bulkCopy = new SqlBulkCopy(connectionString))
			{
				bulkCopy.DestinationTableName = "テーブル名"; // テーブル名をSqlBulkCopyに教える
				bulkCopy.WriteToServer(insertData.AsDataReader()); // bulkCopy実行
			}
		}
	}

	/// 
	/// テーブルのモデルクラス
	/// 
	class TableModel
	{
		public string Id { get; set; }
		public string ColumnA { get; set; }
		public string ColumnB { get; set; }
	}
}

この方法ならば、データを1行ずつ読み込むので、1行分のメモリしか消費しない。
データの加工はLinqの式の中で自由にできる。

注意点

上述した方法は、行単位でデータを加工することはできるが、行をまたいだ処理はできない。
例えば、distinctで重複削除するなどということをすると、全てのデータを読み込む必要があるのでその時点でメモリを消費してしまう。
グルーピングなどしても同じである。
本末転倒ではあるが、メモリを気にしないならなんでもできる。
まぁ、モデルクラスを使って出来るので、メモリ云々を抜きにしてもコードがスッキリするかもしれない。

私の記事が役に立ったら、どうぞ何か買ってください!→ Amazon欲しいものリスト