在C#中扩展DataTable

13
一个名为SourceManager的类有一个静态构造方法,它会遍历所有模块和类并发现实现了ISource接口的类。它会实例化每个类,并将它们作为一个静态属性IEnumerable<ISource> Sources暴露出来。为简单起见,ISource只有两个属性:DataTable Table { get; }string UniqueName { get; }。每个不同的ISource在被实例化时负责从SQL、MDX等中填充其自身的Table。对于我目前编写的所有ISource,在实例化时用所有的DataRow填充Table已经足够了。然而现在我遇到了这样一种情况:我想用延迟加载的方式加载Table中的DataRow,而不是一次性加载所有的数据。我该怎么做呢?下面我将通过一个示例进行讲解。 PermissionSource实现了ISource接口。它的Table属性是私有设置的,并赋值为new PermissionDataTable()。它的UniqueName"Permissions"。目前为止,还没有从数据库中加载任何权限到这个Table属性中。
ISource permissionSource = SourceManager.Sources.
    Where(s => "Permission".Equals(s.UniqueName)).First();

现在我们已经通过接口获取了PermissionSource。让我们获取权限。

DataRow row = permissionSource.Table.Rows.Cast<DataRow>().
    Where(r => r["PermissionName"].Equals("PermissionName")).First()
我已经在 PermissionDataTable 中重写了 Rows 属性,这样就可以以某种方式获取与数据库中的 "PermissionName" 相关联的权限值。其他权限不会被加载。
我无法选择权限系统,也无法选择不使用 DataTable编辑: 在我的示例中,我需要重写 DataTableRows 属性。然而,Rows 是一个 DataRowCollection,它是 sealed 的。因此,在创建像我想要的最小自定义 DataTable 实现方面,实际上没有什么可以做的。

6
DataTable 不会从数据库加载任何数据,你应该使用 Entity Framework。 - SLaks
1
你的问题需要更加明确。DataTable 总是一个内存对象,没有延迟执行或流式处理的能力。你可能想要使用 DataReader 或者在数据库中完成所有操作。 - Tim Schmelter
8
听起来你正在询问如何编写自己的查询提供程序。那可不是一项轻松的任务。 - Servy
你的语句中问号在哪里? - Jodrell
那么,在当前实现中,您的permissionSource.Table是何时加载的?是否有在permissionSource上进行加载的命令? - Nenad
显示剩余6条评论
4个回答

2
我不确定我是否理解您在使用DataTable时的限制,但是在过去,当我需要“刷新” DataTable 或使用不同的条件重新填充它时,我的做法之一是创建一个派生自DataTable的新类,其中包括对DataAdapter的引用,该DataAdapter具有最初用于填充DataTable的连接和选择信息。
例如, DataTable 子类可能如下所示:LazyDataTable 代码。请注意,我添加了访问行的几种不同方法。在查看本文末尾附近的PermissionSource和主Program代码之后,它们可能会更有意义。此外,请注意,我并没有包含与在每种情况下正确打开和关闭数据库连接相关的所有细节。如何处理这个问题将取决于您的数据库访问模型(例如,连接池,共享连接等)。
//using System.Data.Common;
public class LazyDataTable : DataTable {
    protected DbDataAdapter Adapter { get; set; }

    public LazyDataTable(DbDataAdapter a) {
        Adapter = a;
    }
    /// <summary>
    /// Save changes back to the database, using the DataAdapter
    /// </summary>
    public void Update() {
        Adapter.Update(this);
    }
    /// <summary>
    /// Fill this datatable using the SelectCommand in the DataAdapter
    /// The DB connection and query have already been set.
    /// </summary>
    public void Fill() {
        Adapter.Fill(this);
    }

    /// <summary>
    /// read and return one row at a time, using IEnumerable syntax
    /// (this example does not actually add the row to this table, 
    /// but that can be done as well, if desired.
    /// </summary>
    public IEnumerable<DataRow> LazyReadRows() {
        using (var reader = OpenReader()) {
            //Get the schema from the reader and copy it to this table.
            var schema = reader.GetSchemaTable();
            var values = new object[schema.Columns.Count];
            while (reader.Read()) {
                reader.GetValues(values);
                var row = schema.NewRow();
                row.ItemArray = values;
                yield return row;
            }
        }
    }

    /// <summary>
    /// Fill one row at a time, and return the new row.
    /// </summary>
    public DataRow ReadRow() {
        if (_reader == null || _reader.IsClosed) 
            _reader = OpenReader();
        //Get the schema from the reader and copy it to this table.
        if (this.Columns.Count == 0) 
            this.Columns.AddRange(_reader.GetSchemaTable().Columns.Cast<DataColumn>().ToArray());
        if (!_reader.Read()) {
            _reader.Dispose();
            return null;
        }
        var values = new object[_reader.FieldCount];
        _reader.GetValues(values);
        return this.Rows.Add(values);
    }
    private DbDataReader _reader = null;

    private DbDataReader OpenReader() {
        OpenConnect();
        return Adapter.SelectCommand.ExecuteReader();
    }

    private void OpenConnect() {
        var cn = Adapter.SelectCommand.Connection;
        if (cn.State == ConnectionState.Closed)
            cn.Open();
    }

    /// <summary>
    /// Change a Parameter in the SelectCommand, to filter which rows to retrieve.
    /// </summary>
    public void SetSelectParam(string name, object value) {
        var selparams = Adapter.SelectCommand.Parameters;
        selparams[name].Value = value;
    }
}

那么你的PermissionSource将创建一个LazyDataTable并适当设置DataAdapter(包括连接和SELECT命令)。它不会填充DataTable,而是返回一个空的DataTable,以便稍后由应用程序代码填充。因此,您的PermissionSource可能类似于以下代码。我使用了System.Data.OleDb数据对象作为示例,但您可以使用任何ADO提供程序。

interface ISource {
    public DataTable Table { get; }
    string UniqueName { get; }
}

public class PermissionSource : ISource {
    /// <summary>
    /// Loads a DataTable with all of the information to load it lazily.
    /// </summary>
    public DataTable Table { 
        get { 
            const string SELECT_CMD = "SELECT * FROM [Permissions] WHERE ([PermissionName] IS NULL OR [PermissionName]=@p1) AND [OtherProperty]=@p2";
            var conn = new OleDbConnection("...ConnectionString...");
            var selectCmd = new OleDbCommand(SELECT_CMD, conn);
            selectCmd.Parameters.AddWithValue("p1", "PermissionName");
            selectCmd.Parameters.AddWithValue("p2", 0);
            var adapter = new OleDbDataAdapter(selectCmd);
            var builder = new OleDbCommandBuilder(adapter); //used to generate the UPDATE and DELETE commands...
            adapter.UpdateCommand = builder.GetUpdateCommand(); //etc.
            //Do NOT fill the table here. Instead, let the caller fill it.
            return new LazyDataTable(adapter);
        }
    }
    public string UniqueName { get { return "Permission"; } }
}

您的主程序代码将使用以下PermissionSourceLazyDataTable

    static class Program {
    void GetPermissions() {
        ISource permissionSource = SourceManager.Sources.
            Where(s => "Permission".Equals(s.UniqueName)).First();

        var table = permissionSource.Table as LazyDataTable;
        table.SetSelectParam("PermissionName", "Admin");

        //If you want to fill ALL rows in one step:
        table.Fill(); 

        // OR If you want to fill one row at a time, and add it to the table:
        DataRow row;
        while(null != (row = table.ReadRow())) {
            //do something with each individual row. Exit whenever desired.
            Console.WriteLine(row["PermissionName"]);
        }

        // OR If you prefer IEnumerable semantics:
        DataRow row = table.LazyReadRows().FirstOrDefault(someValue.Equals(row["columnname"]));

        //OR use foreach, etc. Rows are still ONLY read one at a time, each time IEnumerator.MoveNext() is called.
        foreach (var row in table.LazyReadRows())
            if (row["someColumn"] == "someValue")
                DoSomething(row["anothercolumn"]);
    }
}

您当然可以混合和匹配这里展示的LazyDataTable的部分,以在应用程序限制内实现您想要的功能。当然,如果您必须从每个源返回一个DataTable,那么至少可以通过像我这里演示的子类化来返回更具功能性的DataTable。这样可以让您传回更多信息,您可以根据需要使用这些信息填充表格。
我仍然鼓励您研究LinqToSQL,可能尝试切换到简单地返回DbDataReader或类似于我在此处展示的LazyDataTable的其他对象,这将允许您自定义原始查询(例如使用SetSelectParam方法)并且一次读取一行数据。
希望这可以帮助到您!

0

可能已经没什么用了,但这个也许能做到。

public interface ISource
{
    DataTable Table { get; }
    string Name { get; set; }
}

public class MySource : ISource
{
    private DataTable table;
    public DataTable Table
    {
        get
        {
            if (table == null)
                // Initialize your data.
                table = new System.Data.DataTable();
            return table;
        }
        private set
        {
            this.table = value;
        }
    }
    public string Name { get; set; }
}

0
以下示例通过ExtendedProperties属性将时间戳值添加到DataTable中。
private void GetAndSetExtendedProperties(DataTable myTable){
 // Add an item to the collection.
 myTable.ExtendedProperties.Add("TimeStamp", DateTime.Now);
 // Print the item.
Console.WriteLine(myTable.ExtendedProperties["TimeStamp"]);
}

0
我认为你需要的是类似于LinqToSql的东西,每个ISource都可以返回Table而不是DataTable。这将允许您使用像例子中给出的动态查询,并且仅在需要时加载请求的数据。 我不知道您是否能够找到所有数据源的LinqToSql提供程序。如果这成为问题,您可以尝试使用之前建议的实体框架。

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