65.9K
CodeProject 正在变化。 阅读更多。
Home

从几乎任何数据库加载任何对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (7投票s)

2010 年 2 月 6 日

CPOL

7分钟阅读

viewsIcon

22447

构建O/R映射器步骤1

引言

现在似乎每个人都在编写对象关系映射器,有些很好,有些则不那么好。抛开观点不谈,(N)Hibernate可能是最著名的;然而,有时它超出了必要的范围,有时它根本不是您所寻找的。有时您必须使用现有O/R映射器不支持的数据库。无论原因如何,本文的宗旨都是从几乎任何数据库表中填充您的对象。事实上,如果您可以使用.NET数据提供程序连接到它,并编写查询来获取数据,那么这段代码就可以用来填充您的对象。

背景

为了保持简单,我不会涉及一些事情。

以下是我不会涉及的一些内容

  • 从配置中获取连接字符串和提供程序名称,并连接到几乎任何数据库。因为我在这篇文章中已经介绍过了。
  • 继承对象
  • 创建自定义属性。这可以用来解决我将要展示的一些问题。这是因为本文只是一个起点,而不是一个完整的解决方案。
  • 连接池以提高性能
  • 线程化来填充对象,但是如果你想有一个好的开始,你需要一个线程安全的列表。所以你可能想看看我关于线程安全列表的文章,以及我关于简化ReaderWriterLocks的另一篇文章
  • 跟踪对象的更改。本文涉及的内容太多。
  • 将对象保存回数据库。本文涉及的内容太多。

首先,我真的不想使用Activator.CreateObject,所以我会创建一个泛型函数。其次,我不想将其绑定到任何特定的DataProvider,所以我的计划是使用DbDataReader,但是Resharper很友好地指出我可以使用IDataReader接口来使其更加通用。由于函数的独特性,我将首先只介绍声明。

private static BindingList<T> 
	LoadObjectsFromDataReader<T>(IDataReader dr) where T : new() 

我将函数标记为static,因为我没有接触包含类的任何成员变量。BindingList<T>将为绑定到Windows控件提供最大的灵活性。对于不经常使用泛型(或模板)的人来说,函数声明开头的<T>可能是最令人困惑的。它将允许我使用我喜欢的几乎任何对象,并以强类型BindingList的形式返回它。where T : new()告诉编译器并非任何对象都可以,而是该对象必须具有public无参数构造函数。所以让我们看看函数体。

private static BindingList<T> 
	LoadObjectsFromDataReader<T>(IDataReader dr) where T : new()
{
    //get the type of the object, without having to create one
    Type type = typeof(T);
    BindingList<T> retval = new BindingList<T>();

    //Mapping Block
    List<PropertyInfo> itemProperties = new List<PropertyInfo>();
    for (int i = 0; i < dr.FieldCount; i++)
    {
        string s = dr.GetName(i);
        var pi = type.GetProperty(s, BindingFlags.Instance | 
		BindingFlags.Public | BindingFlags.SetProperty);
        itemProperties.Add(pi);
    }

    //data block
    object[] oo = new object[itemProperties.Count];
    while (dr.Read())
    {
        dr.GetValues(oo);
        //could be threaded block                
        T item = new T();
        int fieldIndex = -1;
        foreach (var pi in itemProperties)
        {
            fieldIndex++;
            if (pi != null)
            {
                object o = oo[fieldIndex];
                if (DBNull.Value.Equals(o))
                {
                    o = null;
                }
                try
                {
                    pi.SetValue(item, o, null);
                }
                catch
                {
                    //eat data errors quietly
                }
            }
        }
        retval.Add(item);
        //end of could be threaded block
    }
    return retval;
}    

我将按注释分解它,因为代码与大多数人习惯的有点不同。

映射块

要开始这个函数,在声明一些变量之后,我遍历datareader中的所有字段,通过序数(索引位置)获取它们的名称。然后使用反射,我通过该名称找到属性。BindingFlags确保如果属性具有public设置函数并且不是static属性,我将获取该属性。无论是否找到属性,我都会将其添加到我的集合中。

数据块

现在我遍历datareader中的所有记录。我不是为每条记录遍历datareader中的每个字段,而是使用dr.GetValues(oo)将所有字段获取到我的对象数组中。此时,我创建一个正确类型的新对象。

现在到了有趣的部分,对于每条记录,我遍历属性集合,因为它们是按照datareader的序数顺序排列的,如果找不到属性,或者它没有set,我就会跳过它。这可以防止我做任何不必要的工作。

因为数据库中的NULL值在.NET中不会作为null返回,它们会作为DBNull返回。我检查是否为我的列值返回了它,如果是,我将其设置为null

此时,我通过调用pi.SetValue来分配值,传入我们要设置值的对象和要设置的值。但是,由于可能存在数据不匹配,我将代码包装在一个try/catch块中。

最后,我们将新填充的对象添加到我们的BindingList<T>中。完成所有记录后,我返回包含所有新数据的绑定列表。

Using the Code

我们首先需要一个数据库来查询。我将使用我之前一篇文章中的示例数据库,这里

接下来,我们需要一个类来表示表中的一行,因为上面的代码除了属性之外没有使用任何东西,所以这个类只不过是自动属性。

public class Issuer
{
    public Guid ID { get; set; }
    public string Name { get; set; }
}

理想情况下,使用代码会是这样,考虑到你不会想把你的数据库代码散布在你的代码中。这个块为你提供了使其工作所需的一切。因此,这段代码可能会分散在不同位置的多个函数中,例如数据库类或业务对象的基类。

Type type = typeof(Issuer); 
string name = type.Name;

string sql = "select * from " + name;
var factory = DbProviderFactories.GetFactory(providerName);
using (var connection = factory.CreateConnection())
{
    connection.ConnectionString = connectionString;
    connection.Open();
    using (var command = connection.CreateCommand())
    {
        command.CommandText = sql;
        command.CommandType = CommandType.Text;
        using (var dr = command.ExecuteReader(CommandBehavior.CloseConnection))
        {
            return LoadObjectsFromDataReader<T>(dr);
        }
    }
}    

使用性能不佳的“select * from”语句将通过使用类的名称作为表的名称来从表中提取所有数据。在构建连接和命令后,我们执行一个datareader并将其传递给我们新创建的函数。

缺点

这个函数有一些严重的缺点,所以让我们明确一下。这段代码假设列名和表名在C#中都是有效的变量名和类名,所以像“Vehicle Identification Number”这样的列名根本无法工作。我用来调用代码的块从类名派生表名,这在大多数情况下都有效,大多数数据库设计不利用模式,但这种方法也意味着像多个数据库或链接服务器这样的东西也无法使用。

将数据赋值包装在try/catch块中并不是最好的解决方案,它只是防止部分消耗datareader和意外抛出异常的最快解决方案。

select * from语句总是低效的,最好是单独选择列名。但是,由于无法知道对象的哪些属性可能是列名,或者由于不是有效属性名的列名可能需要映射。一个完美的例子是在我的示例数据库中,在Vehicle中有一个名为Vehicle Identification Number的列,因为它包含空格,所以永远找不到具有该名称的属性。

在此代码中,我没有将表名包装在正确的字符中,以防止与SQL和数据库特定关键字相关的问题。我没有包含这一点,因为可靠地获取该信息比简单地获取commandbuilder要复杂得多。您甚至可以使用我之前一篇文章中“最后一个技巧”来获取命令构建器来修复您的SQL语句,这里

这段代码会比必要的速度慢,因为我们正在设置属性。因此,我们失去了一些我们应该拥有的好处,即数据的隐藏和保护。我们真的希望用户能够覆盖主键列中的数据吗?在设置属性时跟踪对象的更改和有效值也更加困难,因为必须使用属性。这是因为没有简单的方法可以知道属性是由此函数设置的,还是由其他代码设置的。

如果我们选择填充private字段,可能来自基类,并使用自定义属性,我们可以解决部分甚至所有这些问题。

跟踪类的更改对于类能够更新数据库中正确的行是绝对必要的。

LoadObjectsfromDataReader函数中,您将看到一个标记为“可线程化块”的块,理论上该块中的所有代码都可以在单独的线程上运行,从而更快地读取datareader。这样做的问题是所有填充都必须在返回发生之前完成。不过,这是值得思考的事情。

关注点

尽管存在缺点,但这仍然是一种相对高效且非常灵活的方法,可以从数据库中获取数据并放入对象中,然后我们可以使用LINQ to Objects查询这些对象。

使用这种方法从不同系统中的多个数据源加载对象,并使用“LINQ to Objects”查询加载的数据,是使用像LINQ to SQL这样的硬编码提供程序框架的一个很好的替代方案,特别是当您需要关联来自多个不同来源的数据时。

在未来的文章中,我将开始讨论如何解决这些缺点。

历史

  • 2010年2月6日:首次发布
  • 2010年2月8日:文章更新
© . All rights reserved.