追記
2018.05.10 皆様、いろいろとコメントいただきありがとうございます。
アドバイスの結果できあがったより最適な実装をコメント欄で書いております。
経緯がわかるように元記事はそのまま残していますが、正しい実装としてはコメント欄のほうをご参考ください。
下記、@tobigitsuneさんの記事を参考に、1行ずつCSVファイルを取り込む場合を想定した実装についてまとめてました。
https://qiita.com/tobigitsune/items/10d6ef4a1f0682c01889
当方、プログラミング初級者の上、Qiita初投稿となりますのでお見苦しい点多々あるかと思いますが、色々とご意見いただけますと幸いです。
環境
- windows10
- VisualStudio2017 Community
想定するケース
EntityFrameworkを用いたWebアプリケーションにて、アップロードされるCSVファイルの行数が非常に多いため、一行ずつDBに取り込むという状況を想定します。
また、アップロードされるCSVファイルはいくつか異なる種類があるため(日々の売り上げデータ、顧客のアクセス情報、在庫管理etc...)、できるだけ再利用性の高い実装を目指すという前提です。
アプリケーションの処理フロー (ざっくり)
- アップロードするファイルを選択する (Views/Index.cshtml)
- 選択したファイルがpostでコントローラに渡される (Controller.cs ActionResult Upload())
- ファイルのストリームを、CSVファイルを読み込むクラスに渡す (Models/CsvReaderOneLine.cs)
- ↑でジェネリックを二つ指定する(CSVファイルを既定の型でバインドするクラス、EntityFrameworkが持っているDBのModelクラス)
-
コントローラからMoveNext()メソッドを実行
- CSVreaderが1行データを読み込み、指定したジェネリックにバインドしたModelクラスのインスタンスをCurrentに代入する
コントローラーからCurrentを読み込む
4,5をMoveNext()がfalseになるまで繰り返す
行末まで読み込んだらUploadのViewを表示する (Views/Upload.cshtml)
CSVファイルを読み込むクラスを定義しておく
ここは@tobigitsuneさんの実装をそのまま使わせていただきました。
まずはLineDataという抽象クラスを定義します。
public abstract class LineData
{
public LineData() { }
public abstract void SetDataFrom(string[] s);
}
次に、このLineDataを継承し、CSVデータをどのようにバインドするかを実装したクラスを作ります。
今回はDBにADDRESS_MASTERというテーブルが存在し、そのテーブルと同じデータの並びになっているCSVファイルを取り込むという状況を想定しています。
public class addressMasterLineData : LineData
{
public int id { get; set; }
public string name { get; set; }
public string furigana { get; set; }
public string gender { get; set; }
public string bloodType { get; set; }
public Nullable<System.DateTime> birthDay { get; set; }
public string homePhone { get; set; }
public string cellPhone { get; set; }
public string mailAddress { get; set; }
public string postalCode { get; set; }
public string homeAddress { get; set; }
public string homeFurigana { get; set; }
public override void SetDataFrom(string[] s)
{
int tmp;
if(Int32.TryParse(s[0],out tmp))
{
id = tmp;
}
else
{
id = 0;
}
name = s[1];
furigana = s[2];
gender = s[3];
bloodType = s[4];
DateTime tmpDate;
if(DateTime.TryParse(s[5],out tmpDate))
{
birthDay = tmpDate;
}
else
{
birthDay = null;
}
homePhone = s[6];
cellPhone = s[7];
mailAddress = s[8];
postalCode = s[9];
homeAddress = s[10];
homeFurigana = s[11];
}
}
ちなみにこのAddressMasterLineDataのフィールド名は、EntityFrameworkによって自動生成されるModelクラスのフィールド名と同じにしておいてください。後述するバインドの際に必要となります。
//------------------------------------------------------------------------------
// <auto-generated>
// このコードはテンプレートから生成されました。
//
// このファイルを手動で変更すると、アプリケーションで予期しない動作が発生する可能性があります。
// このファイルに対する手動の変更は、コードが再生成されると上書きされます。
// </auto-generated>
//------------------------------------------------------------------------------
namespace CSVpractice.Models
{
using System;
using System.Collections.Generic;
public partial class ADDRESS_MASTER
{
public int id { get; set; }
public string name { get; set; }
public string furigana { get; set; }
public string gender { get; set; }
public string bloodType { get; set; }
public Nullable<System.DateTime> birthDay { get; set; }
public string homePhone { get; set; }
public string cellPhone { get; set; }
public string mailAddress { get; set; }
public string postalCode { get; set; }
public string homeAddress { get; set; }
public string homeFurigana { get; set; }
}
}
CSVファイルを読み込み、バインドするクラス
今回の本題と言える部分です。コンストラクタでStreamを引き取り、MoveNext()というメソッドが呼ばれると1行読み込んだデータをジェネリックに従ってバインドします。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Reflection;
namespace CSVpractice.Models
{
// TはLineDataの継承(AddressMasterLineData)、UはModelのクラス(ADDRESS_MASTER)
public class CsvReaderOneLine<T, U> :IDisposable where T:LineData,new() where U:class, new()
{
private StreamReader reader;
private bool skip;
Stream fileStream;
object ret;
// コンストラクタ
public CsvReaderOneLine(Stream fileStream, bool skip=true, string filePath = "")
{
if(filePath != "")
{
if(!filePath.EndsWith(".csv", StringComparison.InvariantCultureIgnoreCase))
{
throw new FormatException("拡張子が.csvではないファイルが指定されました");
}
}
this.fileStream = fileStream;
this.skip = skip;
reader = new StreamReader(fileStream);
if (skip) reader.ReadLine();
}
public bool MoveNext()
{
string line = reader.ReadLine();
if(line != null)
{
string[] lineSplited = line.Split(',');
var lineObj = new T();
lineObj.SetDataFrom(lineSplited);
var modelObj = new U();
// ここの実装が力技すぎてダサい ----------------------
PropertyInfo[] getItems = lineObj.GetType().GetProperties();
foreach (PropertyInfo getItem in getItems)
{
PropertyInfo[] tmpItems = modelObj.GetType().GetProperties();
foreach (PropertyInfo tmpItem in tmpItems)
{
if (getItem.Name == tmpItem.Name)
{
tmpItem.SetValue(modelObj, getItem.GetValue(lineObj, null));
}
}
}
//---------------------------------------
ret = modelObj;
return true;
}
else
{
ret = null;
return false;
}
}
public object current
{
get
{
if(ret != null)
{
return ret;
}
else
{
throw new NullReferenceException();
}
}
}
public void Reset()
{
reader.BaseStream.Seek(0, SeekOrigin.Begin);
}
public void Dispose()
{
this.reader.Dispose();
}
}
}
途中、LineDataからModelへ力技でバインドしています(両者のフィールド名を順番に見ていき、一致したら代入)。もっとシンプルな実装の仕方をご存知のかたがいらっしゃいましたら、教えていただけますと幸いです。。。
全体を動かすコントローラの実装
データをIOする状況が整ったので、全体を動かすコントローラを実装します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using CSVpractice.Models;
namespace CSVpractice.Controllers
{
public class FileUploadController : Controller
{
// GET: FileUpload
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Upload(HttpPostedFileWrapper uploadFile)
{
if (uploadFile == null)
{
ViewBag.Message = "ファイルがnullでした";
return View();
}
using (var reader = new CsvReaderOneLine<addressMasterLineData,ADDRESS_MASTER>(uploadFile.InputStream, false, uploadFile.FileName))
{
while (reader.MoveNext())
{
ADDRESS_MASTER retObj = (ADDRESS_MASTER)reader.current;
using (var db = new CsvPracticeEntities())
{
var query = db.ADDRESS_MASTER.Where(x => x.id == retObj.id).FirstOrDefault();
if(query != null)
{
Debug.WriteLine("同じデータが既に存在しています");
}
else
{
db.ADDRESS_MASTER.Add(retObj);
db.SaveChanges();
}
}
}
}
ViewBag.Message = "ファイルのアップロードとDB取り込みが完了しました";
return View();
}
}
}
以上となります。(Viewは割愛)
職場の先輩から「大量のCSVデータを取り込むと想定して、1行ずつ読み込むときにどう実装したらいいいか考えながらやってみて」という指示を受けて実装してみました。自分としては下記がその答えになったかなと思っています。
- ジェネリックで指定できる
- CSVに合わせて、LineDataの継承クラスを用意する以外は大きな実装の必要がない
- 変更があった際も↑の継承を変更するだけでよい
逆に、データのフォーマットがもっとシンプル(カラムが2列しかない)だったり、種類もさほど多くないという想定であれば、上記のような実装は大げさすぎるかなとも思います。
参考にさせていただいた@tobigitsuneさんに、改めて御礼申し上げます。ありがとうございました。
今後ともよろしくお願いいたします。