常常我們有一些撈報表資料的需求
我們需要從資料庫裡撈出一些符合某種情況的資料 (觸發點)
然後再根據這些資料 去衍生出更多資料 (來自更多其他的資料表)
再把所有資料拿來做一些計算
最後組裝成我們要的結果
並且以不同的方式進行呈現 (也許寫出成檔案)
這個例子我會分成四種情境講解
剛好對應到寫報告程式時 最常見的四種型態
這篇是1. 單一事件
完整程式碼請見 https://gitlab.com/jesperlai/Report
通常一個典型的報表程式流程會長得像這樣
1.【決定觸發點】我要從DB裡撈出 2001/1/1 ~ 2001/2/1 的生產記錄 (TXN)
2.【根據觸發點的資料,再串出其他資料】根據這些生產記錄 ( TXN ) 再找出其他資訊,例如: 找出客人當初下單時的一些指示 (SO) (當初說要訂購多少數量、交期是哪天)
3.【一次取全部】為了不要多次查詢 DB,所以一開始就要撈出所有資料,接著在程式裡自己每回合篩選出此回合符合的資料
4.【客製邏輯】開始組裝成客戶要的報表欄位
5.【共用函式】當中有些資料也許需經過較複雜的運算 ( 常是可以被其他程式 reuse 的 ),我想要特別獨立出來
6.【輸出】將結果輸出為 TXT、CSV、EXCEL ...
Program.cs
using System;
using System.Linq;
namespace Sample_1
{
class Program
{
static void Main(string[] args)
{
{
//為了方便講解 (通常這兩個值會從外面傳進來)----------------------
var sTime = DateTime.Parse("2001/1/1");
var eTime = DateTime.Parse("2001/2/20");
//---------------------------------------------------------------
//組裝資料
Controller controller = new Controller(sTime, eTime);
var result = controller.GetData();
//寫出檔案
RptService myRpt = new RptService();
result = result.OrderBy(q => q.Txn_Dt).ToList(); //最後輸出資料要以過站時間排序
myRpt.GenFile(result, @"D:\sample.csv");
}
}
}
}
Controller.cs
using Sample_1.Extensions;
using Sample_1.Helper;
using Sample_1.Models;
using Sample_1.Utility;
using Sample_1.ViewModels;
using System;
using System.Collections.Generic;
namespace Sample_1
{
public class Controller
{
private DataAccessHelper _helper;
public Controller(DateTime pDataStartTime, DateTime pDataEndTime)
{
_helper = new DataAccessHelper(pDataStartTime, pDataEndTime);
}
public List<Report> GetData()
{
var src = _helper.InitializeData();
var result = GetData(src);
return result;
}
private List<Report> GetData(TriggerPointAndDataSetVM src)
{
var result = new List<Report>();
//我把Trigger Point 觸發點 縮寫成tp
foreach (var tp in src.Tp)
{
//該回合需用到的資料 我把this round 縮寫成 tRound
var tRound = DataAccessHelper.GetThisRoundData(src.DataSet, tp);
//Controller 盡量扁平 如果有複雜的運算可以放到 Utility 做
var item = new Report
{
Txn_Dt = tp.Txn_Dt,
Lot_No = tp.Lot_No,
Stage_Id = tp.Stage_Id,
Qty = tp.Qty.Trim(), //有些轉型或是常用的運算 寫成extension會比較易用
Po_No = tRound.So.Po_No,
Order_Qty = tRound.So.Qty,
Sod = DataUtility.GetSod(tRound.So), //假設這個欄位有很複雜的邏輯 放到Utitlity做
};
result.Add(item);
}
return result;
}
}
}
Utility資料夾
DataUtility.GetSod.cs
using Sample_1.Models;
using System;
namespace Sample_1.Utility
{
/// <summary>
/// 假設某些計算是很複雜的 (通常是可以被他人 reuse 的) 把他獨立出來
/// </summary>
public static partial class DataUtility
{
public static DateTime GetSod(Sales_Order so)
{
//假設這裡要處理一些很複雜的邏輯
return so.Rsod.HasValue ? so.Rsod.Value : so.Sod;
}
}
}
Helper資料夾內
0_Repository.cs
using Sample_1.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sample_1.Helper
{
public class Repository
{
public List<Lot_Txn> GetTxn(DateTime sTime, DateTime eTime)
{
//為了方便講解 (實際應該在DB中)-------------------------------------------------------------------------------
var txns = new List<Lot_Txn>
{
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/1"), Lot_No = "貨批1", Txn_Code = "進站", Stage_Id = "A站", Qty = 10 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/2"), Lot_No = "貨批1", Txn_Code = "出站", Stage_Id = "A站", Qty = 10 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/3"), Lot_No = "貨批1", Txn_Code = "進站", Stage_Id = "B站", Qty = 10 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/1/4"), Lot_No = "貨批1", Txn_Code = "出站", Stage_Id = "B站", Qty = 10 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/1"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "A站", Qty = 20 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/2"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "A站", Qty = 20 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/3"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "B站", Qty = 20 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/4"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "B站", Qty = 20 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/5"), Lot_No = "貨批2", Txn_Code = "進站", Stage_Id = "C站", Qty = 20 },
new Lot_Txn { Txn_Dt = DateTime.Parse("2001/2/6"), Lot_No = "貨批2", Txn_Code = "出站", Stage_Id = "C站", Qty = 20 },
};
//------------------------------------------------------------------------------------------------------------
return txns.Where(q => q.Txn_Dt >= sTime && q.Txn_Dt < eTime && q.Txn_Code == "出站").ToList();
}
public List<Sales_Order> GetSo(List<string> lotNos)
{
//為了方便講解 (實際應該在DB中)-------------------------------------------------------------------------------
var sos = new List<Sales_Order>
{
new Sales_Order { Lot_No = "貨批1", Po_No = "訂單1", Qty = 50, Sod = DateTime.Parse("2001/1/7") },
new Sales_Order { Lot_No = "貨批2", Po_No = "訂單2", Qty = 80, Sod = DateTime.Parse("2001/2/7"), Rsod = DateTime.Parse("2001/2/10") },
};
//------------------------------------------------------------------------------------------------------------
return sos.Where(q => q.Lot_No != null && lotNos.Contains(q.Lot_No)).ToList();
}
}
}
1_DataAccessHelper.cs
using System;
namespace Sample_1.Helper
{
public partial class DataAccessHelper
{
private Repository _repo;
private DateTime _dataStartTime, _dataEndTime;
/// <summary>
/// 通常會傳入一些參數 為了在資料庫裡撈出一些符合的結果來寫出成報表 ex: 撈某一個區間的資料
/// </summary>
public DataAccessHelper(DateTime pDataStartTime, DateTime pDataEndTime)
{
_repo = new Repository();
_dataStartTime = pDataStartTime;
_dataEndTime = pDataEndTime;
}
}
}
2_DataAccessHelper.TriggerPoint.cs
using Sample_1.Models;
using Sample_1.ViewModels;
using System.Collections.Generic;
namespace Sample_1.Helper
{
public partial class DataAccessHelper
{
public TriggerPointAndDataSetVM InitializeData()
{
var data = new TriggerPointAndDataSetVM();
data.Tp = GetTriggerPoints();
data.DataSet = ConcreteDataSet(data.Tp);
return data;
}
private List<Lot_Txn> GetTriggerPoints()
{
return _repo.GetTxn(_dataStartTime, _dataEndTime);
}
}
}
3_DataAccessHelper.DataSet.cs
using Sample_1.Models;
using Sample_1.ViewModels;
using System.Collections.Generic;
using System.Linq;
namespace Sample_1.Helper
{
public partial class DataAccessHelper
{
/// <summary>
/// 其他資料都是based on 你觸發點撈出來的資料有什麼 再去補撈其他資料
/// 例如: 先撈出過站記錄,再透過這段時間內有哪些貨批在動 (表示要報) 再去撈出這些貨批的客戶訂單資料
/// </summary>
public DataSetVM ConcreteDataSet(List<Lot_Txn> triggerPoints)
{
DataSetVM dataSet = new DataSetVM();
dataSet.So = GetSo(triggerPoints);
return dataSet;
}
private List<Sales_Order> GetSo(List<Lot_Txn> triggerPoints)
{
var lotNos = triggerPoints.Select(q => q.Lot_No).Distinct().ToList();
var result = _repo.GetSo(lotNos);
return result;
}
}
}
4_DataAccessHelper.ThisRound.cs
using Sample_1.Models;
using Sample_1.ViewModels;
using System.Linq;
namespace Sample_1.Helper
{
public partial class DataAccessHelper
{
/// <summary>
/// 因為希望不要一直頻繁開關DB 所以前面一次把所有批號的資料都撈出來了
/// 接下來得篩選出只屬於這回合的資料 (例如 根據這回合的過站記錄的批號 從剛剛的DataSet中找出訂單 => 不是每跑一批就查一次DB )
/// </summary>
public static ThisRoundDataVM GetThisRoundData(DataSetVM src, Lot_Txn tp)
{
ThisRoundDataVM tRound = new ThisRoundDataVM();
tRound.So = GetSo(src, tp);
return tRound;
}
private static Sales_Order GetSo(DataSetVM src, Lot_Txn tp)
{
var Sos = src.So.Where(q => q.Lot_No == tp.Lot_No).ToList();
return Sos.Any() ? Sos.First() : new Sales_Order();
}
}
}
Extensions 資料夾
MyExtensions.cs
using System;
namespace Sample_1.Extensions
{
/// <summary>
/// 有時後把一些函式包裝成擴充方法使用起來會比較直覺,程式碼看起來也比較乾淨
/// </summary>
public static class MyExtensions
{
public static int Trim(this double? value)
{
return Convert.ToInt32(value);
}
}
}
RptCsvService.cs
using Sample_1.Models;
using System.Collections.Generic;
namespace Sample_1
{
public class RptService
{
public void GenFile(List<Report> data, string fullPath)
{
try
{
//寫一些你的程式碼來把資料寫出成檔案
//或是使用ServiceStack來做序列化
//File.WriteAllText(fullPath, data, new UTF8Encoding(true));
}
catch
{
}
}
}
}
Models資料夾
0_Lot_Txn.cs
using System;
namespace Sample_1.Models
{
/// <summary>
/// 交易過站資料表
/// </summary>
public class Lot_Txn
{
/// <summary>
/// 過站時間
/// </summary>
public DateTime Txn_Dt { get; set; }
/// <summary>
/// 批號
/// </summary>
public string Lot_No { get; set; }
/// <summary>
/// 站碼
/// </summary>
public string Txn_Code { get; set; }
/// <summary>
/// 站別
/// </summary>
public string Stage_Id { get; set; }
/// <summary>
/// 過站數量
/// </summary>
public double? Qty { get; set; }
}
}
1_Sales_Order.cs
using System;
namespace Sample_1.Models
{
/// <summary>
/// 訂單資料表
/// </summary>
public class Sales_Order
{
/// <summary>
/// 批號
/// </summary>
public string Lot_No { get; set; }
/// <summary>
/// 原交期
/// </summary>
public DateTime Sod { get; set; }
/// <summary>
/// 新交期
/// </summary>
public DateTime? Rsod { get; set; }
/// <summary>
/// 訂單編號
/// </summary>
public string Po_No { get; set; }
/// <summary>
/// 訂單數量
/// </summary>
public int Qty { get; set; }
}
}
2_Report.cs
using System;
namespace Sample_1.Models
{
/// <summary>
/// 輸出結果
/// </summary>
public class Report
{
/// <summary>
/// 假設報表中有個欄位是輸出生產公司是誰 (固定值)
/// </summary>
public string Company
{
get
{
return "很棒棒工廠";
}
}
/// <summary>
/// 訂單編號
/// </summary>
public string Po_No { get; set; }
/// <summary>
/// 批號
/// </summary>
public string Lot_No { get; set; }
/// <summary>
/// 過站時間
/// </summary>
public DateTime Txn_Dt { get; set; }
/// <summary>
/// 站別
/// </summary>
public string Stage_Id { get; set; }
/// <summary>
/// 過站數量
/// </summary>
public int Qty { get; set; }
/// <summary>
/// 交期
/// </summary>
public DateTime? Sod { get; set; }
/// <summary>
/// 訂單數量
/// </summary>
public int Order_Qty { get; set; }
}
}
ViewModels
0_TriggerPointAndDataSetVM.cs
using MySample.Console.Models;
using System.Collections.Generic;
namespace MySample.Console.ViewModels
{
public class TriggerPointAndDataSetVM
{
public List<LOT_TXN> Tp { get; set; }
public DataSetVM DataSet { get; set; }
}
}
1_DataSetVM.cs
using MySample.Console.Models;
using System.Collections.Generic;
namespace MySample.Console.ViewModels
{
public class DataSetVM
{
public List<SO> So { get; set; }
}
}
2_ThisRoundDataVM.cs
using MySample.Console.Models;
namespace MySample.Console.ViewModels
{
public class ThisRoundDataVM
{
public SO So { get; set; }
}
}
希望透過這樣的架構
可以解決大家一不小心就把這種到處串資料的程式寫成幾萬行
並且常找不到哪一行壞掉的問題