利用泛型和现有业务类的 C# 数据导入缓存解决方案






4.86/5 (7投票s)
利用现有类和泛型功能,加速和简化将非规范化数据导入到您的系统中。
引言
从外部系统导入数据是一项几乎所有程序员都必须处理的常见任务。这项任务中最令人沮丧的方面之一是使外部数据的结构与您自己的数据结构相匹配。通常,数据来自一个没有关系概念的旧系统,并且只是以扁平格式转储数据。本文演示了一种在您拥有一个具有独立业务类的既有系统时可以使用的一种技术。
背景
在导入数据时,我总是发现自己缓存某些信息以加快导入过程并为自己提供安全网。例如,许多系统不仅导出活动记录,还导出已“删除”或更准确地说,已停用的记录。这可能导致大量数据重复,并给尝试导入数据的人带来混乱。如果外部源中的一条记录对应于您数据库中的一条记录,那将是很好的,但这很少会发生。
为了简单起见,我将假设外部数据源是 CSV 文件。这是在所有其他方法都失败时的事实标准,并且根据我的经验,它是被滥用且通常格式错误的格式的一个主要例子。我还将使用在许多数据讨论中常见的标准客户 -> 订单 -> 订单明细项示例。虽然使用这些简单的标准似乎是偷懒,但我从经验中知道它们不仅代表了极其常见的场景,而且足够复杂以验证该解决方案。请注意,代码示例为简洁起见已被截断。
步骤 1 - 处理基本情况
好的,让我们从非常简单的事情开始。我们将假设我们正在导入客户。我们的业务类如下所示:
// assume your favorite ORM attributes are in place
public class Customer
{
public int CustomerID { get; set; } // primary key
public string CustomerName { get; set; }
}
我们的数据文件如下所示:
ID,Name,City,State,Country,IsHQ,Employees
42,Acme Inc,Boston,MA,USA,0,50
28,Acme Inc,Denver,CO,USA,1,100
87,Foo Corp,Topeka,KS,USA,1,20
99,Acme Inc,Albany,NY,USA,0,50
31,Foo Corp,Newark,NJ,USA,0,10
我敢肯定,很多人立刻就看到了问题。我们的文件中包含每个公司的多个公司分支机构,但我们只关心一次存储公司名称。这意味着我们不能简单地逐行导入。由于可能会有几十甚至几百个分支机构,我们不想麻烦处理所有重复的记录。由于无法预测文件中将有多少唯一公司和分支机构,我们唯一的选择是每次都读取客户名称。显然,这成为我们想要缓存的数据;一旦“Acme”被处理并存储,我们就可以跳过其余的“Acme”记录。一个简单的解决方案如下所示:
List<string> customerNames = new List<string>();
IDataReader reader = ... // open data source
while (reader.Read())
{
string custName = reader.GetString(1);
if (!customerNames.Contains(custName))
{
// ...create and store new customer in DB...
customerNames.Add(custName);
}
}
当您只导入几个唯一实体(即类)时,这种方法都可以正常工作。但是,如果每条记录有 5、10 甚至 20 条数据呢?保留 20 个 `List` 来缓存所有不同的名称并不是很实用。这就是泛型和我 `DataLoadCache` 类发挥作用的地方。
public static class DataLoadCacheV1<T>
{
private static List<string> _names = new List<string>();
public static bool ContainsName(string name)
{
return _names.Contains(name);
}
public static void StoreName(string name)
{
_names.Add(name);
}
}
乍一看,似乎我们并没有取得多少成就,但请想想我们将使用哪种“T”(泛型类型);是的,我们现有的业务类。因为我们定义了唯一的类并且 `DataLoadClass` 是 static
的,所以我们不必担心为要导入的每个业务类创建唯一的缓存实例。编译器将在每次我们使用新的“T
”时创建一个唯一的 `DataLoadCache` 类。我们的简单示例如下:
IDataReader reader = ... // open data source
while (reader.Read())
{
string custName = reader.GetString(1);
if (!DataLoadCacheV1<Customer>.ContainsName(custName))
{
// ...create and store new customer in DB...
DataLoadCacheV1<Customer>.StoreName(custName);
}
}
请注意,我们没有声明 `List` 或任何本地缓存实例。好的,现在我们已经解决了基本问题,可以继续处理重要的事情了;请跟我来,我保证会变得更好。
步骤 2 - 将订单加入进来
考虑到我们假设处理的是扁平的旧数据格式,现在我们将着手导入订单。我们导入文件中的相关部分现在是:
ID,Name,OrderNumber
42,Acme Inc,1234
87,Foo Corp,5555
31,Foo Corp,1234
28,Acme Inc,5678
正如您所看到的,订单号 1234 被重复显示,表明订单号不是唯一的。当我们向系统中添加这些订单时,我们希望确保我们能够快速访问新数据库的订单 ID,而不仅仅是订单号。假设我们的系统通过简单的外键将 `Order` 与 `Customer` 关联起来。
public class Order
{
public int CustomerID { get; set; } // foreign key
public int OrderID { get; set; } // primary key
public string OrderNumber { get; set; }
}
既然我们需要存储关联,我们就必须扩展 `DataLoadCache` 类来处理 ID 查找。
public static class DataLoadCacheV2<T>
{
private static Dictionary<string, int> _nameIDMaps =
new Dictionary<string, int>();
public static bool ContainsIDByName(string name)
{
return _nameIDMaps.ContainsKey(name);
}
public static void StoreIDByName(string name, int ID)
{
_nameIDMaps.Add(name, ID);
}
public static int GetIDByName(string name)
{
return _namesIDMaps[name];
}
}
现在,仅存储客户名称已无关紧要,因此 `DataLoadCacheV1` 中的方法已被替换。我们现在存储已添加的 `Customer` 的实际数据库 ID。现在我们知道哪些客户名称已被处理以及对应的 ID 是什么。在将 `Order` 添加到数据库时,这一点很有用。
IDataReader reader = ... // open data source
while (reader.Read())
{
string custName = reader.GetString(1);
if (!DataLoadCacheV2<Customer>.ContainsIDByName(custName))
{
int newCustID = // ...create, store, and retrieve new customer in DB...
DataLoadCacheV2<Customer>.StoreIdByName(custName, newCustID);
}
int custID = DataLoadCacheV2<Customer>.GetIDByName(custName);
Order order = new Order()
{
CustomerID = custID,
OrderNumber = ...
};
// Add order to DB....
}
步骤 3 - 添加一对多关系
最后一个示例说明了存储我们正在导入的实体的 ID 而不仅仅是它已被处理的事实的重要性。由于我们知道 `Order` 有 `OrderLineItem`,因此我们需要添加一种方法来查找我们刚刚添加的 `OrderID`。
public static class DataLoadCacheV3<T, U>
{
private static Dictionary<int, Dictionary<string, int>> _foreignIDsByName =
new Dictionary<int, Dictionary<string, int>>();
public static void StoreForeignIDByName(int primaryID,
string foreignName, int foreignID)
{
_foreignIDsByName[primaryID].Add(foreignName, foreignID);
}
public static bool ContainsForeignIDByName(int primaryID, string foreignName)
{
return _foreignIDsByName.ContainsKey(primaryID)
&& _foreignIDsByName[primaryID].ContainsKey(foreignName);
}
public static int GetForeignIDByName(int primaryID, string foreignName)
{
return _foreignIDsByName[primaryID][foreignName];
}
}
我们在这里所做的只是跟踪我们拥有的数据(`CustomerName` 和 `OrderNumber`)的字符串表示形式与我们创建的新数据库 ID 之间的关联。下面的示例将使其更加清晰:
IDataReader reader = ... // open data reader
while (reader.Read())
{
string custName = reader.GetString(1);
if (!DataLoadCacheV3<Customer>.ContainsIDByName(custName))
{
int newCustID = ... // add customer to DB, get ID
DataLoadCacheV3<Customer>.StoreIDByName(custName, newCustID);
}
int custID = DataLoadCacheV3<Customer>.GetIDByName(custName);
string orderNumber = reader.GetString(2);
if (!DataLoadCacheV3<Customer, Order>.ContainsForeignIDByName(custID, orderNumber))
{
int newOrderID = ... // add order to DB, get ID...
DataLoadCacheV3<Customer, Order>.StoreForeignIDByName(custID,
orderNumber, newOrderID);
}
int orderID = DataLoadCacheV3<Customer, Order>.GetForeignIDByName(custID, orderNumber);
}
通过向 `DataLoadCache` 添加 `Dictionary` 的 `Dictionary`,我们允许多层映射发生。`DataLoadCache` 类不必担心我们传递的名称和 ID 的类型,因为泛型类型参数确保我们将获得每种类型组合的唯一静态类。
快速说明
我意识到到目前为止,这很多内容都很枯燥乏味。如果您坚持到现在并且感到困惑,我鼓励您从头开始,并尝试了解最后一个示例中的泛型类型如何打开了无限的可能性,并减轻了您自己维护各种缓存 `Dictionary` 实例的负担。只需编写代码行:`DataLoadCachev3<Foo,Bar>`,您就可以创建两个自定义查找机制(以及更多即将推出的)。不仅如此,代码也非常易读,并且不像本地变量那样可能存在混淆,您可以清楚地知道您正在将 Foo 与 Bar 相关联。
步骤 4 - 跟踪 ID 集合
前面的示例假设给定的 `OrderNumber` 每个客户只出现一次。当我们在扁平数据格式中开始考虑 `OrderLineItem` 时,我们发现这个假设无效。再次,仅显示数据文件的相关部分:
ID,Name,OrderNumber,LineItemNumber,Quantity
42,Acme Inc,1234,1,53
42,Acme Inc,1234,2,91
31,Foo Corp,1234,2,62
31,Foo Corp,1234,1,88
42,Acme Inc,1234,3,57
以及我们现有的 `OrderLineItem` 类:
public class OrderLineItem
{
public int OrderID { get; set; } // foreign key
public int OrderLineItemID { get; set; } // primary key
public string LineItemNumber { get; set; }
public int Quantity { get; set; }
public double Discount {get; set; }
}
让我们暂时假设需要对 `OrderLineItem` 进行某种进一步的处理。这可能是一个导入文件,或者只是设置一些导入后数据。为了正确地进行,我们需要跟踪哪些 `OrderLineItem` ID 属于哪个订单。通过向 `DataLoadCache` 添加处理 ID `List` 的能力,可以轻松解决这个问题。
public static class DataLoadCacheV4<T, U>
{
private static Dictionary<int, List<int>> _foreignIDLists =
new Dictionary<int, List<int>>();
public static void StoreForeignIDInList(int primaryID, int foreignID)
{
if (!_foreignIDLists.ContainsKey(primaryID))
{
_foreignIDLists.Add(primaryID, new List<int>());
}
_foreignIDLists[primaryID].Add(foreignID);
}
public static bool ContainsForeignIDInList(int primaryID, int foreignID)
{
return _foreignIDLists.ContainsKey(primaryID) &&
_foreignIDLists[primaryID].Contains(foreignID);
}
public static List<int> GetForeignIDsInList(int primaryID)
{
return _foreignIDLists[primaryID];
步骤 5 - 完整示例
希望到目前为止,我已经足够清楚地传达了我的意思。这个最后的示例展示了 `DataLoadCache` 类的完整实现。显然,类中的方法是针对本文专门设计的,但您应该看到如何轻松创建新方法来处理您的数据类之间的几乎任何关系。
该示例使用两个导入源来展示如何轻松解决复杂情况。假设第二个文件包含折扣,但它们按公司分支机构分解,而我们并未跟踪(请参阅示例 1)。此外,我们按行项目跟踪折扣,因此我们需要将折扣金额在行项目之间平均分配。为了更有趣,假设他们没有给我们公司名称,只给了我们分支 ID。
First Data File
ID,Name,OrderNumber,LineItemNumber,Quantity
42,Acme Inc,1234,1,53
42,Acme Inc,1234,2,91
31,Foo Corp,1234,2,62
31,Foo Corp,1234,1,88
42,Acme Inc,1234,3,57
Second Data File
ID,OrderNumber,Discount
42,1234,50
99,5678,67
31,1234,31
28,1234,10
-
public static class DataLoadCacheV5<T, U>
{
// Simple text name to database ID mappings
private static Dictionary<string, int> _namesIDMaps =
new Dictionary<string, int>();
public static bool ContainsIDByName(string name)
{
return _namesIDMaps.ContainsKey(name);
}
public static void StoreIDByName(string name, int id)
{
_namesIDMaps.Add(name, id);
}
public static int GetIDByName(string name)
{
return _namesIDMaps[name];
}
// Simple external to local ID mapping
private static Dictionary<int, int> _externalIDMaps =
new Dictionary<int, int>();
public static void StoreIDByExternalID(int externalID, int primaryID)
{
_externalIDMaps[externalID] = primaryID;
}
public static bool ContainsIDByExternalID(int externalID)
{
return _externalIDMaps.ContainsKey(externalID);
}
public static int GetIDByExternalID(int externalID)
{
return _externalIDMaps[externalID];
}
// Mapping a "foreign name" within the context a unique primary entity
// (primaryID = CustomerID, foreignName = OrderNumber, foreignID = OrderID)
private static Dictionary<int, Dictionary<string, int>>
_foreignIdsByName = new Dictionary<int, Dictionary<string, int>>();
public static void StoreForeignIDByName(int primaryID,
string foreignName, int foreignID)
{
_foreignIdsByName[primaryID].Add(foreignName, foreignID);
}
public static bool ContainsForeignIDByName(int primaryID, string foreignName)
{
return _foreignIdsByName.ContainsKey(primaryID)
&& _foreignIdsByName[primaryID].ContainsKey(foreignName);
}
public static int GetForeignIdByName(int primaryID, string foreignName)
{
return _foreignIdsByName[primaryID][foreignName];
}
// Track lists of foreign keys so we don't have to do the DB lookup later
private static Dictionary<int, List<int>> _foreignIDLists =
new Dictionary<int, List<int>>();
public static void StoreForeignIDInList(int primaryID, int foreignID)
{
if (!_foreignIDLists.ContainsKey(primaryID))
{
_foreignIDLists.Add(primaryID, new List<int>());
}
_foreignIDLists[primaryID].Add(foreignID);
}
public static bool ContainsForeignIDInList(int primaryID, int foreignID)
{
return _foreignIDLists.ContainsKey(primaryID) &&
_foreignIDLists[primaryID].Contains(foreignID);
}
public static List<int> GetForeignIDsInList(int primaryID)
{
return _foreignIDLists[primaryID];
}
}
-
public void TestData5()
{
IDataReader reader = ... // open 1st file
while (reader.Read())
{
int branchID = reader.GetInt32(0);
string custName = reader.GetString(1);
if (!DataLoadCacheV5<Customer,Customer>.ContainsIDByName(custName))
{
int newCustID = ... // add customer to DB, get ID...
DataLoadCacheV5<Customer,Customer>.StoreIDByName(custName, newCustID);
}
int custID = DataLoadCacheV5<Customer,Customer>.GetIDByName(custName);
if (!DataLoadCacheV5<Customer,Customer>.ContainsIDByExternalID(branchID))
{
DataLoadCacheV5<Customer,Customer>.StoreIDByExternalID(branchID, custID);
}
string orderNumber = reader.GetString(5);
if (!DataLoadCacheV5<Customer, Order>.ContainsForeignIDByName(custID, orderNumber))
{
int newOrderID = ... // add order to DB, get ID...
DataLoadCacheV5<Customer,Order>.StoreForeignIDByName(
custID, orderNumber, newOrderID);
}
int orderID = DataLoadCacheV5<Customer,Order>.GetForeignIdByName(custID, orderNumber);
string orderLineItemNumber = reader.GetString(6);
if (!DataLoadCacheV5<Order,OrderLineItem>.ContainsForeignIDByName(
orderID, orderLineItemNumber))
{
int newOrderLiID = ... // add order line item to DB, get ID...
DataLoadCacheV5<Order, OrderLineItem>.StoreForeignIDByName(
orderID, orderLineItemNumber, newOrderLiID);
}
int orderLineItemID =
DataLoadCacheV5<Order, OrderLineItem>.GetForeignIdByName(
orderID, orderLineItemNumber);
DataLoadCacheV5<Order, OrderLineItem>.StoreForeignIDInList(
orderID, orderLineItemID);
}
// Here is where you will see the payoff (about time, I know)
IDataReader reader2 = ... // open 2nd file
while (reader2.Read())
{
int branchID = reader2.GetInt32(0);
int custID = DataLoadCacheV5<Customer, Customer>.GetIDByExternalID(branchID);
string orderNumber = reader2.GetString(1);
int orderID =
DataLoadCacheV5<Customer, Order>.GetForeignIdByName(custID, orderNumber);
List<int> orderLineItemIDs = DataLoadCacheV5<Order,
OrderLineItem>.GetForeignIDsInList(orderID);
double totalDiscount = reader.GetDouble(3);
double discountPart = totalDiscount / (double)orderLineItemIDs.Count;
foreach (int orderLineItemID in orderLineItemIDs)
{
// ...update DB or ORM object with discountPart...
}
}
}
结论
需要注意的是,每次使用不同类型的 `DataLoadCache` 类时,您实际上都在创建一个全新的类。这基本上相当于您手工输入了所有所需的专用版本(例如 `Customer`-cache、`Order`-cache 等)。如果您进行任何类型的数据导入,甚至进行顺序处理,我希望您觉得本文有用。
如果您问自己“有什么意义,为什么不直接将数据放入数据库并在需要时查询它?”,我请您考虑一下该解决方案是否具有可扩展性,以及在慢速网络连接上它的效果如何。我的解决方案尝试利用客户端机器上的资源(即处理能力和 RAM),以最大限度地减少数据库调用。