改进数据访问层选择方法 Pattern

Posted

技术标签:

【中文标题】改进数据访问层选择方法 Pattern【英文标题】:Improve data access layer select method Pattern 【发布时间】:2009-01-12 16:39:26 【问题描述】:

最近我发现自己正在编写数据访问层选择方法,其中代码都采用这种通用形式:

public static DataTable GetSomeData( ... arguments)

    string sql = " ... sql string here:  often it's just a stored procedure name ... ";

    DataTable result = new DataTable();

    // GetOpenConnection() is a private method in the class: 
    // it manages the connection string and returns an open and ready connection
    using (SqlConnection cn = GetOpenConnection())
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    
        // could be any number of parameters, each with a different type
        cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function

        using (SqlDataReader rdr = cmd.ExecuteReader())
        
            result.Load(rdr);
        
    

    return result;

或者像这样:

public static DataRow GetSomeSingleRecord( ... arguments)

    string sql = " ... sql string here:  often it's just a stored procedure name ... ";

    DataTable dt = new DataTable();

    // GetOpenConnection() is a private method in the class: 
    // it manages the connection string and returns an open and ready connection
    using (SqlConnection cn = GetOpenConnection())
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    
        // could be any number of parameters, each with a different type
        cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function

        using (SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SingleRow))
        
            dt.Load(rdr);
        
    

    if (dt.Rows.Count > 0)
         return dt.Rows[0];
    return null;

这些方法将由业务层代码调用,然后将基本 DataTable 或 DataRecord 转换为表示层可以使用的强类型业务对象。

由于我反复使用类似的代码,我想确保这段代码是最好的。那么如何改进呢?而且,是否值得尝试将通用代码从这里移到它自己的方法中。如果是这样,该方法会是什么样子(特别是关于传入 SqlParameter 集合)?

【问题讨论】:

你和我一样做这件事——虽然我真的很喜欢你如何堆叠你的双重 using 语句以避免深度嵌套。做得好!我感兴趣的一件事是你如何在逻辑上/物理上设置你的 DAL 与你的 BIZ - 你把它放在自己的项目中吗?或作为命名空间?还是什么? 如果没有参数,您也可以堆叠数据读取器。 DAL 在它自己的程序集中,DAL + BL 将共享一个公共父命名空间。 是的,我现在就是这样做的。您的业​​务对象和 DAL 对象之间的映射是什么?即你去 1:1 吗?我看到有趣的一件事是有人将 DAL 拉入业务对象并使用 CodeSmith 将其全部生成。很有趣 这不是 1:1,但这是这里系统的一个特点。我使用的数据库是来自大型机的快照转储,我们看到的表不是您所说的“通常”。 好的,但理想情况下,在您自己的规范化系统中,您会采用 1:1 的比例,您是否会像整合业务对象一样整合 dal 对象?即特定于一个业务对象的查找表 - 顺便说一句,这是一个很棒的话题,我也想在自己的工作中改进这一点 【参考方案1】:

必须添加我自己的:Return DataReader from DataLayer in Using statement

新模式使我一次只能在内存中保存一条记录,但仍将连接包含在一个不错的“使用”语句中:

public IEnumerable<T> GetSomeData(string filter, Func<IDataRecord, T> factory)

    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter";

    using (SqlConnection cn = new SqlConnection(GetConnectionString()))
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    
        cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter;
        cn.Open();

        using (IDataReader rdr = cmd.ExecuteReader())
        
            while (rdr.Read())
            
                yield return factory(rdr);
            
            rdr.Close();
        
    

【讨论】:

您的异常处理在哪里? @msfanboy - 它所属的地方,在程序中的更高级别 无论如何,这已经过时了。从那以后,我进一步改进了这种模式:***.com/questions/2862428/… @JoelCoehoorn 你能用你的新方法更新这个问题吗?或提供相关链接本身? @nawfal 这将违反已建立的 SO 协议。【参考方案2】:

就客户端代码而言,我喜欢的一种模式如下所示:

        DataTable data = null;
        using (StoredProcedure proc = new StoredProcedure("MyProcName","[Connection]"))
        
            proc.AddParameter("@LoginName", loginName);
            data = proc.ExecuteDataTable();
        

我通常将连接设为可选,并且我会以某种方式进行编码,以便从 ConnectionStrings 配置部分中提取它或将其视为实际的连接字符串。这让我可以在一个场景中重用 dal,并且在某种程度上是我使用对象构造属性存储连接字符串时 COM+ 时代的习惯。

我喜欢这个,因为它易于阅读并且对我隐藏了所有 ADO 代码。

【讨论】:

我对此确实有一些问题,但要投赞成票,因为它给了我一个想法:我打算将通用代码作为 Functor 隐藏起来,而不是隐藏在方法中。 有趣的请分享你的想法。我不是在这种模式上出售的,就我在 2.0 上的情况而言。它减少了混乱 请注意,我说的是“玩”:我认为我最终不会走那条路。如果我确实想出了一些有趣的东西,我会回帖。【参考方案3】:

和我发的here类似

public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer, 
                             Func<IDataRecord, S> selector)

    using (var conn = new T()) //your connection object
    
        using (var cmd = conn.CreateCommand())
        
            if (parameterizer != null)
                parameterizer(cmd);
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = _connectionString;
            cmd.Connection.Open();
            using (var r = cmd.ExecuteReader())
                while (r.Read())
                    yield return selector(r);
        
    

我有这些简单的扩展方法来帮助调用:

public static void Parameterize(this IDbCommand command, string name, object value)

    var parameter = command.CreateParameter();
    parameter.ParameterName = name;
    parameter.Value = value;
    command.Parameters.Add(parameter);


public static T To<T>(this IDataRecord dr, int index, T defaultValue = default(T),
                      Func<object, T> converter = null)

    return dr[index].To<T>(defaultValue, converter);


static T To<T>(this object obj, T defaultValue, Func<object, T> converter)

    if (obj.IsNull())
        return defaultValue;

    return converter == null ? (T)obj : converter(obj);


public static bool IsNull<T>(this T obj) where T : class

    return (object)obj == null || obj == DBNull.Value;

所以现在我可以打电话了:

var query = Get(sql, cmd =>

    cmd.Parameterize("saved", 1);
    cmd.Parameterize("name", "abel");
, r => new User(r.To<int>(0), r.To<string>(1), r.To<DateTime?>(2), r.To<bool>(3)));
foreach (var user in query)



这是完全通用的,适合任何符合 ado.net 接口的模型。 The connection object and reader is disposed only after the collection is enumerated once.

【讨论】:

【参考方案4】:

唯一不同的是我从我自己的内部数据库辅助方法切换到实际的数据访问应用程序块http://msdn.microsoft.com/en-us/library/cc309504.aspx

使了解企业库的其他开发人员能够更加标准化/统一代码。

【讨论】:

【参考方案5】:

实现 DBAL 的方法有很多,我认为您走在正确的道路上。在您的实施中需要考虑的事项:

您正在使用类似工厂的方法来创建 SqlConnection,这是一个小问题,但您可以对 SqlCommand 执行相同的操作。 参数长度是可选的,因此您实际上可以将其排除在 Parameter.Add 调用之外。 也创建用于添加参数的方法,代码示例如下。

使用DbUtil.AddParameter(cmd, "@Id", SqlDbType.UniqueIdentifier, Id);添加参数

internal class DbUtil 

internal static SqlParameter CreateSqlParameter(
    string parameterName,
    SqlDbType dbType,
    ParameterDirection direction,
    object value
) 
    SqlParameter parameter = new SqlParameter(parameterName, dbType);

    if (value == null) 
        value = DBNull.Value;
    

    parameter.Value = value;

    parameter.Direction = direction;
    return parameter;


internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType
) 
    return AddParameter(sqlCommand, parameterName, dbType, null);


internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType,
    object value
) 
    return AddParameter(sqlCommand, parameterName, dbType, ParameterDirection.Input, value);


internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType,
    ParameterDirection direction,
    object value
) 
    SqlParameter parameter = CreateSqlParameter(parameterName, dbType, direction, value);
    sqlCommand.Parameters.Add(parameter);
    return parameter;
    

【讨论】:

【参考方案6】:

首先,我认为您已经考虑过使用 ORM 与滚动您自己的。这个我就不说了。

我对推出自己的数据访问代码的想法:

随着时间的推移,我发现不使用单独的 DAL/BL 对象,而是将它们合并为一个对象更容易(在得出这个结论后的一段时间,我发现这是一个众所周知的模式 - 即 ActiveRecord)。拥有单独的 DAL 程序集可能看起来不错且解耦,但维护成本的开销会增加。每次添加新功能时,您都必须创建更多代码/修改更多类。根据我的经验,维护应用程序的团队通常比构建应用程序的原始开发团队少得多,而且他们会讨厌所需的额外工作。 对于大型团队来说,将 DAL 分开可能是有意义的(让一个小组在其他人的时候处理它。但这会很好地刺激代码膨胀。 具体示例:如何使用生成的 DataTable?迭代行、创建类型对象并从行中获取数据?如果答案是肯定的,请考虑为在 DAL 和 BL 之间移动数据而创建的额外 DataTable。为什么不直接从 DataReader 中获取? 关于示例:如果您返回一个无类型的 DataTable,那么我猜您必须在调用代码中使用列名(SP 调用返回的结果集)。这意味着如果我必须更改数据库中的某些内容,它可能会影响两个层。

我的建议(我尝试了这两种方法 - 该建议是我提出的最新工作方法 - 它随着时间的推移而演变)。

为您的类型化业务对象创建一个基类。 在基类中保留对象状态(新建、修改等) 把主要的数据访问方法放在这个类中,作为静态方法。只需稍加努力(提示:泛型方法 + Activator.CreateInstance),您就可以为读取器中返回的每一行创建一个业务对象。 在业务对象中创建一个抽象方法来解析行数据(直接来自 DataReader!)并填充对象。 在派生业务对象中创建静态方法,这些方法准备存储的过程参数(取决于各种过滤条件)并从基类调用通用数据访问方法。

目的是最终得到以下用法:

List<MyObject> objects = MyObject.FindMyObject(string someParam);

对我的好处是我只需要更改一个文件即可应对数据库列名、类型等的更改(通常是小的更改)。使用一些经过深思熟虑的区域,您可以组织代码,以便它们在同一个对象中成为单独的“层”:)。另一个好处是基类确实可以从一个项目重用到另一个项目。而且代码膨胀很小(好吧,与好处相比。您还可以填充数据集并将它们绑定到 UI 控件:D

限制 - 每个域对象(通常每个主数据库表)都有一个类。而且您无法在现有事务中加载对象(尽管您可以考虑传递事务,如果有的话)。

如果您对更多细节感兴趣,请告诉我 - 我可以稍微扩展一下答案。

【讨论】:

【参考方案7】:

最简单的解决方案:

var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();

【讨论】:

这完全没有抓住重点。这也可能是这里最慢的答案,因为它将整个结果集加载到 RAM 中不仅一次,而是两次。

以上是关于改进数据访问层选择方法 Pattern的主要内容,如果未能解决你的问题,请参考以下文章

如何适当地设计数据访问层?

将数据访问逻辑从业务层移至数据访问层

DAL数据访问层

在 .net Project MVC 中实现数据访问层最佳实践

数据访问层DAL

访问者模式(Visitor Pattern)