【问题标题】:How can I populate a class from the results of a SQL query in C#?如何从 C# 中的 SQL 查询结果填充类?
【发布时间】:2012-07-26 01:21:51
【问题描述】:

我有这样的课程:

public class Product
{
    public int ProductId { get; private set; }
    public int SupplierId { get; private set; }

    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public int Stock { get; private set; }
    public int PendingStock { get; private set; }
}

我可以像这样从我的数据库中获取这些详细信息:

SELECT product_id, supplier_id, name, price, total_stock, pending_stock 
FROM products
WHERE product_id = ?

我不想手动运行DataSetDataTable 来设置值。

我确信有一种方法可以使用某种绑定/映射机制来填充类,但我能找到的唯一东西是绑定到 winforms 组件或使用 XAML。

是否可以将某种属性应用于我的属性/类以使该类自动从查询行填充?

【问题讨论】:

    标签: c# sql-server database winforms orm


    【解决方案1】:

    我决定提出另一个答案,它实际上是对 Alex 提供的答案的扩展(所以所有功劳归于他),但它为了 column-name-2-property-name 映射而引入了属性。

    首先需要自定义属性来保存列名:

    [AttributeUsage(AttributeTargets.Property, Inherited = true)]
    [Serializable]
    public class MappingAttribute : Attribute
    {
        public string ColumnName = null;
    }
    

    该属性必须应用于类的那些属性,这些属性将从数据库行中填充:

    public class Product
    {
        [Mapping(ColumnName = "product_id")]
        public int ProductId { get; private set; }
    
        [Mapping(ColumnName = "supplier_id")]
        public int SupplierId { get; private set; }
    
        [Mapping(ColumnName = "name")]
        public string Name { get; private set; }
        [Mapping(ColumnName = "price")]
        public decimal Price { get; private set; }
        [Mapping(ColumnName = "total_stock")]
        public int Stock { get; private set; }
        [Mapping(ColumnName = "pending_stock")]
        public int PendingStock { get; private set; }
    }
    

    其余的都按照 Alex 的建议进行,只是该属性用于检索列名:

    T MapToClass<T>(SqlDataReader reader) where T : class
    {
            T returnedObject = Activator.CreateInstance<T>();
            PropertyInfo[] modelProperties = returnedObject.GetType().GetProperties();
            for (int i = 0; i < modelProperties.Length; i++)
            {
                MappingAttribute[] attributes = modelProperties[i].GetCustomAttributes<MappingAttribute>(true).ToArray();
    
                if (attributes.Length > 0 && attributes[0].ColumnName != null)
                    modelProperties[i].SetValue(returnedObject, Convert.ChangeType(reader[attributes[0].ColumnName], modelProperties[i].PropertyType), null);
            }
            return returnedObject;
    }
    

    【讨论】:

    • 我最终写的几乎就是这个。谢谢您的帮助! :)
    • 请记住反射不是免费的,所以当映射多于几行时可能需要一点缓存;)
    • 我完全同意 madd0 (我在其他答案中也说过同样的话)。如果您希望过度使用代码 - 您需要提供一些缓存。
    • 是的,我提供了一种将属性存储在缓存中以节省反射成本的机制。
    【解决方案2】:
    select 'public ' + case DATA_TYPE  
    when 'varchar' then 'string'
    when 'nvarchar' then 'string'
    when 'DateTime' then 'DateTime'
    when 'bigint' then 'long' 
    else DATA_TYPE end +' '+ COLUMN_NAME + ' {get; set;}' 
    from INFORMATION_SCHEMA.COLUMNS  WHERE TABLE_NAME ='YOUR TABLE NAME'  ORDER BY ORDINAL_POSITION
    

    【讨论】:

    • 请编辑更多信息。不建议使用纯代码和“试试这个”的答案,因为它们不包含可搜索的内容,也没有解释为什么有人应该“试试这个”。
    • 这根本不能回答问题。
    【解决方案3】:

    一种可能的解决方案:

    在 SQL 查询中可以使用"PATH Mode with FOR XML " 子句。

    查询的结果将是一个 XML,您可以deserialize directly to C# objects

    它也适用于大型嵌套 SQL 查询。

    【讨论】:

      【解决方案4】:

      如果您不想利用 ORM 框架(实体框架等),您可以手动完成:

      T MapToClass<T>(SqlDataReader reader) where T : class
      {
              T returnedObject = Activator.CreateInstance<T>();
              List<PropertyInfo> modelProperties = returnedObject.GetType().GetProperties().OrderBy(p => p.MetadataToken).ToList();
              for (int i = 0; i < modelProperties.Count; i++)
                  modelProperties[i].SetValue(returnedObject, Convert.ChangeType(reader.GetValue(i), modelProperties[i].PropertyType), null);
              return returnedObject;
      }
      

      你可以这样使用它:

      Product P = new Product(); // as per your example
      using(SqlDataReader reader = ...)
      {
      while(reader.Read()) { P = MapToClass<Product(reader); /* then you use P */ }
      }
      

      唯一需要注意的是查询中字段的顺序(它必须与您的类中定义的属性的顺序相匹配)。

      您需要做的就是构建类,编写查询,然后它会处理“映射”。

      警告我经常使用这种方法,从来没有遇到过任何问题,但是它不适用于部分类。如果您使用部分模型,无论如何使用 ORM 框架会更好。

      【讨论】:

      • 亚历克斯,这是错误的。 GetProperties() 不保证返回的 PropertyInfo 对象按声明顺序。
      • 经常使用这种方法,而且它们总是以正确的方式出现...无论如何,执行命令很容易,我会借此机会更新代码。
      • 我也这样做了;] 但是文档对此很清楚:msdn.microsoft.com/en-us/library/aky14axb 如果您没有 column-name-2-property-name 的映射器,您将如何执行命令?
      • 你去吧,只需通过 metadatatoken 订购它们(实际上我没有将其省略,因为我没有从我的工具箱的最新版本中复制粘贴代码)。
      • 为什么不按列名检索数据? reader[modelProperties[i].Name]
      【解决方案5】:

      您需要自己映射属性或使用 ORM(对象关系映射器)。

      Microsoft 提供Entity Framework,但Dapper 需要较少的开销,并且根据您的要求可能是一个可行的选择。

      在您的情况下,Dapper 代码如下所示:

      var query = @"SELECT product_id, supplier_id, name, price, total_stock, pending_stock 
      FROM products
      WHERE product_id = @id";
      
      var product = connection.Query<Product>(query, new { id = 23 });
      

      为了完整起见,重要的是要指出我在这里谈论的是 Dapper,因为问题涉及将 SQL 结果映射到对象。 EF 和 Linq to SQL 也会这样做,但它们也会做一些额外的事情,比如将 Linq 查询转换为 SQL 语句,这可能也很有用。

      【讨论】:

        【解决方案6】:

        默认情况下,原始 .NET Framework 中没有此类功能。您可以使用实体框架,但如果它对您来说不是一个好的解决方案,那么另一种选择是反射机制。

        1. 创建一些自定义属性类,可以为类的每个公共属性保存一个列名。

        2. 从数据库中检索记录后,实例化Product 类的对象并枚举属性。对于具有您自定义属性的每个属性 - 使用 PropertyInfo 中的 SetValue 根据自定义属性中定义的列名称更改值。

        考虑以下几点:

        • 解决方案是相当简单的分配开销;只有当您有许多表和许多类(如 Product)并希望编写一个代码来自动初始化所​​有这些时,它才有意义
        • 反射本身就是一种开销 - 所以从长远来看需要一些缓存

        【讨论】:

        • 这听起来很有趣。我会看看它。另外,我怎么可能支持一个以L为头像的人的回答!? :)
        • 谢谢。 :] 请在此处查看 Alex 的回答,因为他已经为我在此处提出的建议提供了代码,所以他的回答要好得多 except 我强烈建议您在 Product 类上使用自定义属性'字段。
        • 是的,我现在正在调查。干杯:)
        • 请看我的另一个答案,它使用自定义属性扩展了 Alex 解决方案。
        【解决方案7】:

        我会使用 Linq to SQL 并按如下方式进行操作:

        public class Product
        {
            public int ProductId { get; private set; }
            public int SupplierId { get; private set; }
            public string Name { get; private set; }
            public decimal Price { get; private set; }
            public int Stock { get; private set; }
            public int PendingStock { get; private set; }
        
            public Product(int id)
            {
                using(var db = new MainContext())
                {
                    var q = (from c in product where c.ProductID = id select c).SingleOrDefault();
                    if(q!=null)
                        LoadByRec(q);           
                }
            }
            public Product(product rec)
            {
                LoadByRec(q);
            }
            public void LoadByRec(product rec)
            {
                ProductId = rec.product_id;
                SupplierID = rec.supplier_id;
                Name = rec.name;
                Price = rec.price;
                Stock = rec.total_stock;
                PendingStock = rec.pending_stock;
            }
        }
        

        【讨论】:

        • 您能对此添加一些解释吗?我没有看到dbusing 的任何地方使用,并且没有对查询的引用。有没有办法让LoadByRec 作为一组属性工作,而不是在方法中手动映射它们?
        • @Poly 你必须插入一个数据上下文:msdn.microsoft.com/en-us/library/bb384470.aspx,在这种情况下你将命名为MainContext。一定要玩它,它真的很强大,而且可以节省大量时间
        【解决方案8】:

        那么你应该使用实体框架或 Linq To SQL,如果你不想使用它,那么你需要自己映射/填充它

        更多关于实体框架的信息 http://msdn.microsoft.com/en-us/data/ef.aspx

        更多关于 Linq to SQL 的信息 http://msdn.microsoft.com/en-us/library/bb386976.aspx

        【讨论】:

        • 您能否提供一个实体框架的快速示例?我浏览了文档,但对于您实际使用它的方式非常模糊。
        • 问题是该类必须从实体创建,您不能为此使用自己的类,据我所知,除非手动映射,否则没有自动转换方式。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-06
        • 1970-01-01
        • 2014-05-11
        • 2019-12-21
        • 1970-01-01
        • 2013-08-11
        相关资源
        最近更新 更多