【问题标题】:Transforming DataTable to List<T> in C#在 C# 中将 DataTable 转换为 List<T>
【发布时间】:2013-10-14 02:34:03
【问题描述】:

我已经通过 StackOverflow(和其他网站)搜索了有关在 C# 中使用反射将 DataTable 转换为 List 的信息。

到目前为止,我的结果还不错:我可以在 3.5 秒内反映 200k 行(硬编码模式下为 0.5 秒)。

但我的实体(代表我的数据的类,但我想你已经知道了)遵循这种模式:

我的数据库有这样的列(我实际上并没有这样做,但你会明白的):

Table: Clients
Columns:
    ClientID, ClientName, ClientPhone, CityID[FK]

我正在使用 SqlConnection (MySqlConnection),因此我必须对我的实体进行硬编码并将数据库结果转换为该实体的列表。喜欢:

Select *, cit.* from Clients cli
Inner join Cities cit on (cit.CityID == cli.CityID)
Inner join Countries cou on (cou.CountryID == cit.CountID)

我不知道这个 SQL 是否正确,但我想你明白了。这应该返回一些像这样的字段:

ClientID, ClientName, ClientPhone, CityID, CityName, CountryID, CountryName

结果应该是List&lt;Client&gt;

问题是:我有 2 个内部连接,我在我的实体中表示这些数据,就像这样(我喜欢“像这样”的表达方式):

public class Client
{
    public int ClientID { get; set; }
    public string ClientName { get; set; }
    public string ClientPhone { get; set; }
    public City ClientCity { get; set; }
}

public class City
{
    public int CityID { get; set; }
    public string CityName { get; set; }
    public Country CityCountry { get; set; }
}

public class Country
{
    public int ContryID { get; set; }
    public string CountryName { get; set; }
}

所以,如果我有一个Client 对象,我将通过表达式client.ClientCity.CityCountry.CountryName 得到它的国家名称。我称之为 3 级属性访问器。

我想正确地反映它。这是将 DataTable 转换为 List 的主要方法。我的母语是葡萄牙语,但我尝试翻译我的 cmets 以符合我上面的描述。

这段代码的想法是:我尝试在主类中找到我必须设置的列。如果我没有找到它,我会在作为对象的属性中搜索该属性。就像 ClientCity 中的 CityName 在 Client 中一样。这段代码很乱。

 public List<T> ToList<T>(DataTable dt) where T : new()
    {

        Type type= typeof(T);
        ReflectionHelper h = new ReflectionHelper(type);
        insertPropInfo(tipo);  //a pre-reflection work, I cache some delegates, etc..
        List<T> list = new List<T>();
        DataTableReader dtr = dt.CreateDataReader();
        while (dtr.Read())
        {
            T obj = new T();
            for (int i = 0; i < dtr.FieldCount; i++)
            {
                GetObject(ref obj, tipo, dtr.GetName(i), dtr.GetValue(i));
            }
            list.Add(obj);
        }

        return lista;

    }
        //ref T obj: the object I create before calling this method
        //Type classType: the type of the object (say, Client)
        //string colName: this is the Database Column i'm trying to fill. Like ClientID or CityName or CountryName.
        //colLineData: the data I want to put in the colName.

    public void GetObject<T>(ref T obj, Type classType, string colName, object colLineData) where T : new()
    {


        //I do some caching to reflect just once, and after the first iteration, I think all the reflection I need is already done.
        foreach (PropertyInfo info in _classPropInfos[classType])
        {
            //If the current PropertyInfo is a valuetype (like int, int64) or string, and so on
            if (info.PropertyType.IsValueType || info.PropertyType == typeof(string))
            {
                //I think string.Equals is a little faster, but i had not much difference using "string" == "string"
                if (info.Name.Equals(colName)) //did I found the property?

                    if (info.PropertyType != typeof(char)) //I have to convert the type if this is a Char. MySql returns char as string.
                    {
                        _delegateSetters[info](obj, colLineData); //if it isn't a char, just set it.
                    }
                    else
                    {
                        _delegateSetters[info](obj, Convert.ChangeType(colLineData, typeof(char)));
                    }
                break;
            }
            else //BUT, if the property is a class, like ClientCity:
            {
                //I reflect the City class, if it isn't reflected yet:
                if (!_classPropInfos.ContainsKey(info.PropertyType))
                {
                    insertPropInfo(info.PropertyType);
                }
                //now I search for the property:
                Boolean foundProperty = false;
                object instance = _delegateGetters[info](obj); //Get the existing instance of ClientCity, so I can fill the CityID and CityName in the same object.

                foreach (PropertyInfo subInfo in _classPropInfos[info.PropertyType])
                {
                    if (subInfo.Name.Equals(colName))//did I found the property?
                    {
                        if (instance == null)
                        {
                            //This will happen if i'm trying to set the first property of the class, like CityID. I have to instanciate it, so in the next iteration it won't be null, and will have it's CityID filled.
                            instance = _initializers[info.PropertyType]();//A very fast object initializer. I'm worried about the Dictionary lookups, but i have no other idea about how to cache it.
                        }
                        _delegateSetters[subInfo](instance, colLineData);//set the data. This method is very fast. Search about lambda getters & setters using System.Linq.Expression.
                        foundProperty = true;
                        break;//I break the loops when I find the property, so it wont iterate anymore.
                    }

                }
                if (foundProperty)//if I found the property in the code above, I set the instance of ClientCity to the Client object.
                {
                    _delegateSetters[info](obj, instance);
                    break;
                }
            }
        }
    }

这段代码有一个问题:我可以到达 CityID 和 CityName,并填写它。但是 CountryID 和 CountryName 不会。因为这段代码可以进行 2 级反射,所以我需要一些递归方法来填充我需要的许多级别。我试图这样做,但我遇到了太多堆栈溢出和空引用异常,我几乎要放弃了。

此代码将使获取数据库行变得更加容易,您是否已经找到了一些库或任何我想要的东西?如果没有,我如何实现 n 级反射以从 DataTable 中创建正确的列表?

【问题讨论】:

  • 一个简单(学习)和快速(使用)的 ORM。使用dapper
  • 获取Dapper 库来完成数据库工作,它还将处理映射。你将无法做SELECT * 的事情;但无论如何你都不应该这样做。
  • 如果您想提高反射性能,我建议您花一些时间学习 Reflection.Emit 命名空间和/或表达式树。
  • 我已经在使用表达式树,我将它们缓存在 delegate[Setters|Getters] 字典中。我不知道为什么我在改变方法时留下了这个名字。
  • Dapper 的作者是本网站最受尊敬的作者之一。提出问题并标记它Dapper

标签: c# sql list reflection datatable


【解决方案1】:

您的问题确实很常见,几乎所有流通的 ORM 都解决了这个问题。
当然,更改已经编写好的应用程序以利用 ORM 通常是不切实际的,但是有一些简单的 ORM 真的很容易添加到现有应用程序中,并且可以让您逐步替换已经编写的代码。

其中一个 ORM 是 DAPPER。它仅包含一个源文件,您可以将其与 POCO 类和存储库方法直接包含在同一个项目中(或者仅引用已编译的程序集)。考虑到要执行的工作的复杂性,它真的很容易学习,而且速度非常快。更不用说这个小宝石的作者经常在这个网站上回答关于他们工作的问题。只需使用#dapper 标签进行搜索

迄今为止,我发现的唯一麻烦是 POCO 属性和字段名称的一对一映射,以及当您的密钥未命名为 ID 时,PK 和 FK 之间有时会逃避规则。但这就是我,我还没有完全理解这些规则。

【讨论】:

  • 我最终“包装”了 dapper 以满足我的需求。我的数据库字段遵循的模式不允许我只放置一个名为ID 的字段。我的例子有点假,它们实际上就像 CID_ID(用于城市)、CLI_ID(用于客户)等等。把它放在 c# 中有点糟糕,但对于我们在我的工作中,很容易知道该属性的含义。我还看了 dapper 源码,太棒了,我能感受到其中的艺术。
【解决方案2】:

考虑使用EntityFramework。它将自动完成所有这些工作。

【讨论】:

  • 为了更广泛,我建议使用对象关系映射器 (ORM)
  • 已经考虑过了,但很遗憾,这不是我的决定。
【解决方案3】:

这是基于您获取包含 3 个表的数据集并创建正确的 DataRelation。 在您的特定情况下(200k 行)我不知道它会如何执行,但不应该那么糟糕:)。

您的调用代码可能是这样的:

List<Clients> clients = Test.CreateListFromTable<Clients>(ds.Tables["Clients"]);

请记住,正如我所说,它基于您获取数据集并创建关系。 接下来是带有相关方法的类(ClientsToCity 和 CityToCountry 是数据关系的名称,您可以自己放置):

    public class Test
    {
        // function that set the given object from the given data row
        public static void SetItemFromRow<T>(T item, DataRow row) where T : new()
        {
            foreach (DataColumn c in row.Table.Columns)
            {
                PropertyInfo prop = item.GetType().GetProperty(c.ColumnName);

                if (prop != null && row[c] != DBNull.Value)
                {
                    prop.SetValue(item, row[c], null);
                }
                else
                {
                    if (c.ColumnName == "CityID")
                    {
                        object obj = Activator.CreateInstance(typeof(City));

                        SetItemFromRow<City>(obj as City, row.GetChildRows("ClientsToCity")[0]);
                        PropertyInfo nestedprop = item.GetType().GetProperty("ClientCity");
                        nestedprop.SetValue(item, obj, null);
                    }
                    else if (c.ColumnName == "CountryID")
                    {
                        object obj = Activator.CreateInstance(typeof(Country));

                        SetItemFromRow<Country>(obj as Country, row.GetChildRows("CityToCountry")[0]);
                        PropertyInfo nestedprop = item.GetType().GetProperty("CityCountry");
                        nestedprop.SetValue(item, obj, null);
                    }
                }

            }
        }

        // function that creates an object from the given data row
        public static T CreateItemFromRow<T>(DataRow row) where T : new()
        {
            T item = new T();

            SetItemFromRow(item, row);

            return item;
        }

        // function that creates a list of an object from the given data table
        public static List<T> CreateListFromTable<T>(DataTable tbl) where T : new()
        {
            List<T> lst = new List<T>();

            foreach (DataRow r in tbl.Rows)
            {
                lst.Add(CreateItemFromRow<T>(r));
            }

            return lst;
        }
    }

【讨论】:

  • 这段代码看起来很棒,但不太适合我的情况,我没有任何 DataRelation。我会保存它以供将来参考,这是一个很好看的代码。 Brotip:由于您使用了property.SetValue,这段代码运行起来会很慢。我会使用表达式为 getter 和 setter(可能只有 setter)创建一个编译函数并缓存它。
猜你喜欢
  • 2011-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-05
  • 2011-09-30
  • 1970-01-01
相关资源
最近更新 更多