一括インポート機能は現在、ほとんどのシステムで標準装備となっています。私たちの注文システムでも例外ではなく、店舗情報、商品情報、クーポンコードなど、様々なデータのインポートが必要です。非開発者にとっては、多くの管理画面よりもExcelの方が親しみやすいものです。ここでは、Excel一括インポート機能を実装する過程で遭遇した課題とその解決策を共有します。もしあなたより良い方法をご存知でしたら、ぜひ共有してください。また、現在同様の課題に直面している方々の一助となれば幸いです。
注:Excel操作には org.in2bits.myxls.dll を使用しました。ライセンスフリーかどうかは確認していません。
若かりし日のコードは、私たちが若かった証拠です 初めてExcel一括インポートに取り組んだ時、自然とonclickとINSERT文を使いました。ローカル環境で数件のデータしかないExcelでテストしたところ、正常に動作しました。データが正常にインポートされると、嬉々として開発・リリースし、「完了!」と思っていました。しかし、お客様が数百件のデータを含むExcelファイルをインポートしようとした際に問題が発生しました。おそらく多くの方が想像できる通り、実行タイムアウトエラーが頻発しました。そこで再度検索したところ、executionTimeoutを設定できることがわかりました。(この設定後も問題が残っていたのですが、詳細は覚えておらず、以下の内容につながりました)
努力は必ず成功とは限らないが、必ず成長につながる executionTimeout設定で問題を解決しようと試みたものの、壁にぶつかり続けた末に、通常はマルチスレッドを使用する必要があると知りました。その時、自分の知識の浅さを実感し、ブログを読むことの重要性を痛感しました。検索の結果、以下のようなコードを見つけました。
protected void executeImport_Click(object sender, EventArgs e)
{
Thread processingThread = new Thread(new ThreadStart(ImportData));
processingThread.Start();
}
public void ImportData()
{
///Excelを読み込み、ループでinsert処理
}
コード表示 こうすることで、タイムアウトの問題は減りました(発生しなくなったわけではなく、具体的な条件は詳しく調べていませんでした)。しかし、受け入れられない問題が一つありました、処理がいつ完了したかがわからず、ブラウザを閉じるタイミングが分からないという点です。当時は、データ件数が増えているかを確認し続けるという非常に原始的な方法しか思いつきませんでした。良い解決策が見つからなかったため、この問題は長くシステムに残り、私の心に重くのしかかる石のようにのしかかっていました。受け入れられないものを、お客様に受け入れさせるわけにはいかないとずっと思っていました。しばらくして、ようやくBackgroundWorkerに出会いました。その時も、知識の重要性とブログを読むことの大切さを改めて実感しました。
失敗は私たちを若さの痕跡から少しずつ解放してくれる BackgroundWorkerの詳細については、ここでは書きません。Baidu、Google、MSDNに包括的で権威的な情報があります。ここでは、この過程で私が踏んだ落とし穴を共有します:
1.DoWorkが全く実行されず、エラーログも出ない;ページにAsync="true"が設定されていない可能性があります。
2.HttpContext.Currentがnull;DoWork内で各レコードごとにQRコード画像を生成する必要があり(クラス内で生成)、HttpContext.Current.Serverが常に空でした。瞬時に「HttpContext.Currentはどこにでも存在するわけではない」というブログ記事を思い出し、ブログを読むことの大切さを再認識しました。
3.処理自体はタイムアウトしないが、データベース接続がタイムアウトする可能性。これはBackgroundWorkerとは直接関係ありませんが、一緒に発生することが多いようです。通常は単にSqlCommand.CommandTimeout = 0;と設定すれば問題ありません。
BackgroundWorkerのコードは検索すればすぐに見つかりますが、自分のコードも共有します。検索したい方はどうぞ、妥協したい方は以下のコードをご覧ください:
aspxの主なコードは、設定とアップロードコントロールの問題です
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="importmenu.aspx.cs" Inherits="importmenu"
Async="true" %>
<asp:FileUpload runat="server" ID="excelFileUploader" />
<asp:Button ID="importButton" runat="server" CssClass="button_1" OnClick="importButton_Click"
Text="インポート" OnClientClick=" showload_super();"></asp:Button>
<a href="template.xls">テンプレートダウンロード</a>
コード表示 aspx.csの主なコードは、完了メソッドの実装に焦点を当てています。これが以前実現できなかった部分です。開始時にローディングダイアログを表示し、完了時に通知を行います。詳細な内容は各自で検索してみてください。
protected void importButton_Click(object sender, EventArgs e)
{
bgw = new BackgroundWorker();
bgw.WorkerSupportsCancellation = true;
bgw.WorkerReportsProgress = true;
bgw.DoWork += new DoWorkEventHandler(ProcessImport);
bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(ImportCompleted);
bgw.RunWorkerAsync();
}
/// <summary>
/// 時間のかかる操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ProcessImport(object sender, DoWorkEventArgs e)
{
string fileExtension = "";
if (this.excelFileUploader.HasFile)
{
fileExtension = Path.GetExtension(this.excelFileUploader.PostedFile.FileName);
if (fileExtension == ".xls" || fileExtension == ".XLS")
{
// 有効な拡張子
}
else
{
statusMessage = "ファイル形式が不正です。Excelファイルをアップロードしてください";
return;
}
}
else
{
statusMessage = "Excelファイルをアップロードしてください";
return;
}
string uploadPath = "~/upload/excel/";
if (!System.IO.Directory.Exists(Server.MapPath(uploadPath)))
{
System.IO.Directory.CreateDirectory(Server.MapPath(uploadPath));
}
string newFileName = uploadPath + System.DateTime.Now.ToString("yyyyMMddHHmmssffff") + fileExtension; // アップロード後の新しいファイル名
// ファイルを保存
try
{
this.excelFileUploader.PostedFile.SaveAs(Server.MapPath(newFileName));
}
catch (Exception ex)
{
ErrorLogger.WriteLog(ex.ToString());
return;
}
// Excel形式の問題によるエラーを防ぐための必要なチェック
try
{
// ファイルを解析し、データベースに保存
XlsDocument excelDoc = new XlsDocument(Server.MapPath(newFileName));
Worksheet worksheet = excelDoc.Workbook.Worksheets[0];
// Excel形式: 1行目は説明、2行目は見出し、3行目からがデータ
for (int i = 3; i < worksheet.Rows.Count; i++) //
{
if (worksheet.Rows[ushort.Parse(i.ToString())] != null &&
worksheet.Rows[ushort.Parse(i.ToString())].CellCount >= 1 &&
worksheet.Rows[ushort.Parse(i.ToString())].GetCell(ushort.Parse("1")) != null &&
worksheet.Rows[ushort.Parse(i.ToString())].GetCell(ushort.Parse("1")).Value != null)
{
MenuItemInfo item = new MenuItemInfo();
item.ParentId = 0;
item.Status = 0;
item.IsDeleted = 0;
item.CategoryName = worksheet.Rows[ushort.Parse(i.ToString())].GetCell(ushort.Parse("1")).Value.ToString();
item.Depth = 2;
item.Priority = 0;
item.ImageUrl = worksheet.Rows[ushort.Parse(i.ToString())].GetCell(ushort.Parse("2")).Value.ToString();
item.Description = worksheet.Rows[ushort.Parse(i.ToString())].GetCell(ushort.Parse("3")).Value.ToString();
item.Path = ChineseConvertUtil.GenerateCode(item.CategoryName);
dataAccessLayer.Add(item);
importedCount++;
}
else
{
break;
}
}
}
catch (Exception ex)
{
statusMessage = "インポート中にエラーが発生しました。Excelファイルを確認してください";
ErrorLogger.WriteLog(ex.ToString());
return;
}
statusMessage = "操作完了。" + importedCount + "件のメニューをインポートしました。";
}
/// <summary>
/// 処理完了
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ImportCompleted(object sender, RunWorkerCompletedEventArgs e)
{
string notificationMessage = "";
if (importedCount == 0)
{
notificationMessage = "操作が完了していません," + statusMessage;
}
else
{
notificationMessage = statusMessage;
}
AlertScript.RegisterScript(this.Page, "alert('" + notificationMessage + "');hideload_super();");
}
コード表示 実行プロセスと完了通知の効果:
まとめ この記事はここまでです。共有は続けます。正直なところ、多くの詳細については私自身も完全には理解しておらず、誤りがある場合はご指摘いただければ幸いです。また、一部の方々の助けとなれば嬉しいです。
優れたプログラマーになるために!