如何在C#中从SQL查询结果中填充一个类?

14

我有一个像这样的类:

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的内容。

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

9个回答

17
我将提供另一个答案,实际上是对Alex提供的答案进行扩展(所有功劳归于他),但它引入了属性,以便进行列名称到属性名称的映射。
首先需要自定义属性来保存列名:
[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;
}

我最终写了几乎完全一样的东西。感谢您的帮助! :) - Polynomial
只需记住反射不是免费的,因此在映射超过几行时可能需要一些缓存;) - madd0
我完全同意madd0的观点(我在另一个答案中也提到了同样的观点)。如果你想过度使用代码,你需要提供一些缓存。 - Kuba Wyrostek
是的,我正在提供一种机制来将属性存储在缓存中,以节省反射成本。 - Polynomial
如果您在CLR项目中使用ToArray()调用时遇到问题,请参考此问题:https://dev59.com/RWIk5IYBdhLWcg3wI7EF - High Plains Grifter

14
你需要自己映射属性或使用ORM(对象关系映射器)。 微软提供了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

如果您不想使用ORM框架(如Entity Framework等),可以手动完成:

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 框架。

我经常使用这种方法,它们总是以正确的方式出现...强制执行顺序也很容易,我会抓住机会更新代码。 - Alex
我真诚地从未遇到这种方法的任何问题(我使用它来处理相当大量的数据)。但是有一个突出的注意事项我忘记提及了,我已更新了我的答案。 - Alex
2
为什么不直接通过列名检索数据呢?reader[modelProperties[i].Name] - Andomar
当类是部分类时,由于编译器处理属性组合的方式可能会导致混淆。我遇到过几次这个问题。按列名检索数据是一个很好的建议,如果它们有相同的名称...但这通常不是我的情况。 - Alex
+1 Andomar 的评论反映了DRY的流行规则 - 当类字段与表列相同时,生活会变得更加容易(尽管这并不总是可能的)。 - Kuba Wyrostek
显示剩余5条评论

2
我会使用 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 作为一组属性工作,而不是在方法中手动映射它们? - Polynomial
@Poly 你需要插入一个数据上下文:http://msdn.microsoft.com/zh-cn/library/bb384470.aspx,在这种情况下,你会将其命名为 MainContext。一定要尝试一下,它非常强大,可以节省大量时间。 - Tom Gullen

1

在原始的.NET Framework中,默认情况下没有这样的功能。您可以使用Entity Framework,但如果它不是您的好选择,则另一种选择是反射机制。

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

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

请考虑以下内容:

  • 该解决方案对于简单的赋值而言相当繁琐;只有在您有许多表和许多类(如Product)并希望编写一个代码以自动初始化所有这些类时才有意义
  • 反射本身就是一种开销-因此长期运行需要一些缓存

这听起来很有趣。我会研究一下。此外,我怎么可能不给一个以 L 为头像的人点赞呢!? :) - Polynomial
谢谢。:] 请看Alex在这里的答案,因为他提供了我所提出的代码,所以他的答案更好,除了我强烈建议在您的Product类字段上使用自定义属性。 - Kuba Wyrostek
是的,我正在研究它。谢谢 :) - Polynomial
请查看我的另一个答案,该答案利用自定义属性扩展了Alex的解决方案。 - Kuba Wyrostek

1

0

你能提供一个Entity Framework的快速示例吗?我已经浏览了文档,但它对如何实际使用它非常模糊。 - Polynomial
问题在于该类必须从实体创建,不能使用自己的类来创建,据我所知,除非手动映射,否则没有自动转换的方法。 - JSantos

0

使用Entity Framework,您可以使用SqlQuery方法来填充不是表或视图的类: context.Database.SqlQuery(sql, parameterList)

当然,您需要使用EF。:-)


-1
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

1
请编辑更多信息。不鼓励仅提供代码或“试试这个”类回答,因为它们没有可搜索的内容,也没有解释为什么某人应该“试试这个”。 - abarisone
1
这完全没有回答问题。 - Polynomial

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接