ADO.NET使用DataReader构建复杂对象

时间:2014-09-17 20:02:19

标签: c# ado.net

我正在尝试创建一种方法来对数据库进行独特搜索,并根据我的需要构建正确的对象。我的意思是,我使用SQL查询返回很多行,然后根据该数据库行构建集合。 E.g:

我们有一个名为People的表和另一个名为Phones的表。

我们假设这是我的SQL查询,并将返回以下内容:

SELECT 
   P.[Id], P.[Name], PH.[PhoneNumber]
FROM
   [dbo].[People] P
INNER JOIN 
   [dbo].[Phones] PH ON PH.[Person] = P.[Id]

这就是结果:

1   NICOLAS    (123)123-1234
1   NICOLAS    (235)235-2356

所以,我的班级将是:

public interface IModel {
    void CastFromReader(IDataReader reader);
}

public class PhoneModel : IModel {
    public string PhoneNumber { get; set; }

    public PhoneModel() {  }
    public PhoneModel(IDataReader reader) : this() {
         CastFromReader(reader);
    }

    public void CastFromReader(IDataReader reader) {
        PhoneNumber = (string) reader["PhoneNumber"];
    }
}

public class PersonModel : IModel {
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<PhoneModel> Phones { get; set; }

    public PersonModel() {  
        Phones = new List<PhoneModel>();
    }

    public PersonModel(IDataReader reader) : this() {
         CastFromReader(reader);
    }

    public void CastFromReader(IDataReader reader) {
        Id = Convert.ToInt32(reader["Id"]);
        Name = (string) reader["Name"];

        var phone = new PhoneModel();
        phone.CastFromReader(reader);
        Phones.Add(phone);

        // or
        Phones.Add(new PhoneModel {
            PhoneNumber = (string) reader["PhomeNumber"]
        });
    }
}

此代码将生成一个包含两个电话号码的PersonModel对象。到目前为止这很好。

然而,当我想用​​这个过程管理更多表时,我正在努力做出一些好的方法。

我们假设,我有一个名为Appointments的新表。它将用户的约会存储在日程表中。

因此,将此表添加到查询中,结果将为:

1   NICOLAS   (123)123-1234    17/09/2014
1   NICOLAS   (123)123-1234    19/09/2014
1   NICOLAS   (123)123-1234    27/09/2014
1   NICOLAS   (235)235-2356    17/09/2014
1   NICOLAS   (235)235-2356    19/09/2014
1   NICOLAS   (235)235-2356    17/09/2014

正如大家们所看到的,问题是以这种方式管理手机和约会。你能想到任何可以解决这个问题的东西吗?

谢谢大家的意见!

2 个答案:

答案 0 :(得分:1)

如果您想为查询产生的每个列组合手动编写数据类型,那么您需要做很多工作,最终会遇到很多非常相似但略有不同的类很难命名。另请注意,这些数据类型不应被视为超出它们的内容:Data Transfer Objects (DTOs)。它们不是具有特定于域的行为的真实域对象;它们应该只包含和传输数据,没有别的。

以下是两个建议或想法。我只会在这里划伤,而不是进入太多的细节;既然你没有问过一个非常具体的问题,我就不会提供一个非常具体的答案。

1。更好的方法可能是确定您拥有的域实体类型(例如,人员,约会)以及您拥有的域值类型(例如电话号码),然后从中构建一个对象模型:

struct PhoneNumber { … }

partial interface Person
{
    int Id { get; }
    string Name { get; }
    PhoneNumber PhoneNumber { get; }
}

partial interface Appointment
{
    DateTime Date { get; }
    Person[] Participants { get; }
}

然后让您的数据库代码映射到这些。例如,如果某个查询返回人员ID,人名,电话号码和约会日期,则每个属性都必须放入正确的实体类型,并且必须将它们链接在一起(例如通过{{ 1}})正确。相当多的工作。如果您不想手动执行此操作,请查看LINQ to SQL,Entity Framework,NHibernate或任何其他ORM。如果您的数据库模型和您的域模型太不相同,那么即使这些工具也可能无法进行翻译。

2。如果您想手动编码将数据转换为域模型的数据查询图层,您可能希望以这样的方式设置查询:如果它们返回一个属性A实体X和实体X具有其他属性B,C和D,然后查询也应返回这些属性,这样您始终可以从查询结果中构建完整的域对象。例如,如果查询返回了人员ID和人员电话号码,但没有返回人员姓名,则无法从查询中构建Participants个对象(如上所述),因为名称丢失。

这第二个建议至少可以部分地避免您必须定义许多非常相似的DTO类型(每个属性组合一个)。这样,您可以为个人记录设置DTO,为电话号码记录设置另一个DTO,为约会记录设置另一个,也许(如果需要)另一个用于人员和电话号码的组合;但您不需要区分PersonPersonWithAllAttributesPersonWithIdButWithoutNameOrPhoneNumber等类型。您只需要PersonWithoutIdButWithPhoneNumber包含所有属性。

答案 1 :(得分:1)

如果没有先定义这些对象的类型,则无法将查询结果传输到强类型对象。如果要将查询数据保留在内存中,我建议您在某些时候将其传输到先前定义类型的对象中。

因此,以下内容并非我实际建议做的事情。但我想向你展示一种可能性。自己判断。

正如我在a previous comment中所建议的那样,你可以使用动态语言运行时(DLR)来模仿强类型的DTO,它已经可以在.NET 4中使用了。

以下是自定义DynamicObject类型的示例,该类型为IDataReader提供看似强烈类型的外观。

using System.Data;
using System.Dynamic; // needs assembly references to System.Core & Microsoft.CSharp
using System.Linq;

public static class DataReaderExtensions
{
    public static dynamic AsDynamic(this IDataReader reader)
    {
        return new DynamicDataReader(reader);
    }

    private sealed class DynamicDataReader : DynamicObject
    {
        public DynamicDataReader(IDataReader reader)
        {
            this.reader = reader;
        }

        private readonly IDataReader reader;

        // this method gets called for late-bound member (e.g. property) access   
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            int index = reader.GetOrdinal(binder.Name);
            result = index >= 0 ? reader.GetValue(index) : null;
            return index >= 0;
        }
    }
}

然后你可以像这样使用它:

using (IDataReader reader = someSqlCommand.ExecuteReader(…))
{
    dynamic current = reader.AsDynamic(); // façade representing the current record
    while (reader.Read())
    {
        // the magic will happen in the following two lines:
        int id = current.Id; // = reader.GetInt32(reader.GetOrdinal("Id"))
        string name = current.Name; // = reader.GetString(reader.GetOrdinal("Name"))
        …
    }
}

但请注意,通过此实现,您获得的只是当前记录的外观。如果要将多条记录的数据保存在内存中,这种实现方式将无济于事。为此,您可以考虑其他几种可能性:

  • 使用匿名对象:cachedRecords.Add(new { current.Id, current.Name });。如果您在构建它的同一方法中访问cachedRecords,这只会有任何好处,因为使用的匿名类型不能在方法之外使用。

  • ExpandoObject中缓存current的数据。