如何在 .NET 中使用 Dapper 处理数据库连接?

Posted

技术标签:

【中文标题】如何在 .NET 中使用 Dapper 处理数据库连接?【英文标题】:How do I handle Database Connections with Dapper in .NET? 【发布时间】:2012-03-02 09:28:48 【问题描述】:

我一直在使用 Dapper,但我不确定处理数据库连接的最佳方式。

大多数示例显示了在示例类中,甚至在每个方法中创建的连接对象。但是我觉得在每个 clss 中引用一个连接字符串是错误的,即使它是从 web.config 中提取的。

我的经验是使用 DbDataContextDbContext 与 Linq to SQL 或实体框架,所以这对我来说是新的。

当使用 Dapper 作为我的数据访问策略时,我如何构建我的 Web 应用程序?

【问题讨论】:

为时已晚,但是;我是这样实现的:***.com/a/45029588/5779732 using-dapper-asynchronously-in-asp-net-core-2 - exceptionnotfound.net/… 【参考方案1】:

更新:MarredCheese 评论的澄清:

"不需要使用using语句。Dapper会自动打开, 关闭,并为您处理连接。”这是不正确的。 Dapper 会自动打开关闭的连接,它会 自动关闭连接that it auto-opened,但不会 自动处理连接。 Marc Gravell 和 Eric Lippert 两者都提倡与 Dapper 一起使用 here。

Microsoft.AspNetCore.All:v2.0.3 | 小巧玲珑:v1.50.2

我不确定我是否正确使用了最佳实践,但我这样做是为了处理多个连接字符串。

如果你只有 1 个连接字符串很容易

Startup.cs

using System.Data;
using System.Data.SqlClient;

namespace DL.SO.Project.Web.UI

    public class Startup
    
        public IConfiguration Configuration  get; private set; 

        // ......

        public void ConfigureServices(IServiceCollection services)
        
            // Read the connection string from appsettings.
            string dbConnectionString = this.Configuration.GetConnectionString("dbConnection1");
            
            // Inject IDbConnection, with implementation from SqlConnection class.
            services.AddTransient<IDbConnection>((sp) => new SqlConnection(dbConnectionString));

            // Register your regular repositories
            services.AddScoped<IDiameterRepository, DiameterRepository>();

            // ......
        
    

DiameterRepository.cs

using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories

    public class DiameterRepository : IDiameterRepository
    
        private readonly IDbConnection _dbConnection;

        public DiameterRepository(IDbConnection dbConnection)
        
            _dbConnection = dbConnection;
        

        public IEnumerable<Diameter> GetAll()
        
            const string sql = @"SELECT * FROM TABLE";

            // No need to use using statement. Dapper will automatically
            // open, close and dispose the connection for you.
            return _dbConnection.Query<Diameter>(sql);
        

        // ......
    

如果您有超过 1 个连接字符串会出现问题

由于Dapper 使用IDbConnection,你需要想办法区分不同的数据库连接。

我尝试创建多个接口,从IDbConnection“继承”,对应于不同的数据库连接,并在Startup上注入具有不同数据库连接字符串的SqlConnection

失败是因为SqlConnection 继承自DbConnection,而DbConnection 不仅补充了IDbConnection,还补充了Component 类。因此,您的自定义接口将无法仅使用 SqlConnection 实现。

我还尝试创建自己的DbConnection 类,它采用不同的连接字符串。这太复杂了,因为您必须实现 DbConnection 类中的所有方法。你失去了来自SqlConnection 的帮助。

我最终做了什么

    Startup 期间,我将所有连接字符串值加载到字典中。我还为所有数据库连接名称创建了一个enum,以避免使用魔法字符串。 我将字典作为 Singleton 注入。 我没有注入IDbConnection,而是创建了IDbConnectionFactory,并将其作为瞬态注入所有存储库。现在所有存储库都采用IDbConnectionFactory 而不是IDbConnection。 何时选择正确的连接?在所有存储库的构造函数中!为了使事情变得干净,我创建了存储库基类并让存储库从基类继承。正确的连接字符串选择可以发生在基类中。

数据库连接名称.cs

namespace DL.SO.Project.Domain.Repositories

    public enum DatabaseConnectionName
    
        Connection1,
        Connection2
    

IDbConnectionFactory.cs

using System.Data;

namespace DL.SO.Project.Domain.Repositories

    public interface IDbConnectionFactory
    
        IDbConnection CreateDbConnection(DatabaseConnectionName connectionName);
    

DapperDbConenctionFactory - 我自己的工厂实现

namespace DL.SO.Project.Persistence.Dapper

    public class DapperDbConnectionFactory : IDbConnectionFactory
    
        private readonly IDictionary<DatabaseConnectionName, string> _connectionDict;

        public DapperDbConnectionFactory(IDictionary<DatabaseConnectionName, string> connectionDict)
        
            _connectionDict = connectionDict;
        

        public IDbConnection CreateDbConnection(DatabaseConnectionName connectionName)
        
            string connectionString = null;
            if (_connectDict.TryGetValue(connectionName, out connectionString))
            
                return new SqlConnection(connectionString);
            

            throw new ArgumentNullException();
        
    

Startup.cs

namespace DL.SO.Project.Web.UI

    public class Startup
    
        // ......
         
        public void ConfigureServices(IServiceCollection services)
        
            var connectionDict = new Dictionary<DatabaseConnectionName, string>
            
                 DatabaseConnectionName.Connection1, this.Configuration.GetConnectionString("dbConnection1") ,
                 DatabaseConnectionName.Connection2, this.Configuration.GetConnectionString("dbConnection2") 
            ;

            // Inject this dict
            services.AddSingleton<IDictionary<DatabaseConnectionName, string>>(connectionDict);

            // Inject the factory
            services.AddTransient<IDbConnectionFactory, DapperDbConnectionFactory>();

            // Register your regular repositories
            services.AddScoped<IDiameterRepository, DiameterRepository>();

            // ......
        
    

DiameterRepository.cs

using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories

    // Move the responsibility of picking the right connection string
    //   into an abstract base class so that I don't have to duplicate
    //   the right connection selection code in each repository.
    public class DiameterRepository : DbConnection1RepositoryBase, IDiameterRepository
    
        public DiameterRepository(IDbConnectionFactory dbConnectionFactory)
            : base(dbConnectionFactory)  

        public IEnumerable<Diameter> GetAll()
        
            const string sql = @"SELECT * FROM TABLE";

            // No need to use using statement. Dapper will automatically
            // open, close and dispose the connection for you.
            return base.DbConnection.Query<Diameter>(sql);
        

        // ......
    

DbConnection1RepositoryBase.cs

using System.Data;
using DL.SO.Project.Domain.Repositories;

namespace DL.SO.Project.Persistence.Dapper

    public abstract class DbConnection1RepositoryBase
    
        public IDbConnection DbConnection  get; private set; 

        public DbConnection1RepositoryBase(IDbConnectionFactory dbConnectionFactory)
        
            // Now it's the time to pick the right connection string!
            // Enum is used. No magic string!
            this.DbConnection = dbConnectionFactory.CreateDbConnection(DatabaseConnectionName.Connection1);
        
    

然后对于需要与其他连接进行通信的其他存储库,您可以为它们创建不同的存储库基类。

using System.Data;
using DL.SO.Project.Domain.Repositories;

namespace DL.SO.Project.Persistence.Dapper

    public abstract class DbConnection2RepositoryBase
    
        public IDbConnection DbConnection  get; private set; 

        public DbConnection2RepositoryBase(IDbConnectionFactory dbConnectionFactory)
        
            this.DbConnection = dbConnectionFactory.CreateDbConnection(DatabaseConnectionName.Connection2);
        
    


using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories

    public class ParameterRepository : DbConnection2RepositoryBase, IParameterRepository
    
        public ParameterRepository (IDbConnectionFactory dbConnectionFactory)
            : base(dbConnectionFactory)  

        public IEnumerable<Parameter> GetAll()
        
            const string sql = @"SELECT * FROM TABLE";
            return base.DbConnection.Query<Parameter>(sql);
        

        // ......
    

希望所有这些帮助。

【讨论】:

正是我正在寻找的。我遇到了同样的问题并以同样的方式解决了它,我仍然不知道这是否是一个好习惯,但在我看来,我认为是。 为 IServiceProvider 范围注册 IDbConnection 会更好吗?可以创建服务并注册为具有不同连接并使用 var scope = factory.CreateNonDefaultScope(); 的单例范围工厂;使用 var connection = scope.ServiceProvider.GetRequiredService() 您将获得非默认连接。更少的继承也将有助于扩展性...... 这就是我要找的。很棒的工作@David。谢谢 "无需使用using 语句。Dapper 将自动为您打开、关闭和处理连接。"这是不正确的。 Dapper 会自动打开关闭的连接,它会自动关闭连接that it auto-opened,但不会自动释放连接。 Marc Gravell 和 Eric Lippert 都提倡使用 using 和 Dapper here。 @DavidLiang 那么请您更新您说“无需使用 using 语句...”的部分我也对此感到困惑,直到我随机扩展评论部分,幸运的是我看到了 MarredCheese 的评论,顺便说一句,感谢您的有用解释!【参考方案2】:

这是大约 4 年前被问到的……但无论如何,也许答案对这里的人有用:

我在所有项目中都这样做。 首先,我创建了一个基类,其中包含一些这样的辅助方法:

public class BaseRepository

    protected T QueryFirstOrDefault<T>(string sql, object parameters = null)
    
        using (var connection = CreateConnection())
        
            return connection.QueryFirstOrDefault<T>(sql, parameters);
        
    

    protected List<T> Query<T>(string sql, object parameters = null)
    
        using (var connection = CreateConnection())
        
            return connection.Query<T>(sql, parameters).ToList();
        
    

    protected int Execute(string sql, object parameters = null)
    
        using (var connection = CreateConnection())
        
            return connection.Execute(sql, parameters);
        
    

    // Other Helpers...

    private IDbConnection CreateConnection()
    
        var connection = new SqlConnection(...);
        // Properly initialize your connection here.
        return connection;
    

有了这样一个基类,我可以轻松地创建真正的存储库,而无需任何样板代码:

public class AccountsRepository : BaseRepository

    public Account GetById(int id)
    
        return QueryFirstOrDefault<Account>("SELECT * FROM Accounts WHERE Id = @Id", new  id );
    

    public List<Account> GetAll()
    
        return Query<Account>("SELECT * FROM Accounts ORDER BY Name");
    

    // Other methods...

所以所有与 Dapper、SqlConnection-s 和其他数据库访问相关的代码都位于一个地方(BaseRepository)。所有真正的存储库都是干净简单的 1 行方法。

我希望它会对某人有所帮助。

【讨论】:

BaseRepository 是不必要的继承,因为它不提供任何公共或抽象方法或属性。这可能是一个 DBHelper 类。 CreateConnection移动到自己的班级会更好吗? 可能是......但我个人喜欢保持一切简单。如果您在 CreateConnection(...) 中有很多逻辑,那可能是个好主意。在我的项目中,这个方法就像“return new Connection(connectionString)”一样简单,所以它可以在没有单独的 CreateConnection(...) 方法的情况下内联使用。 另外,正如 nick-s 所指出的,在最新版本的 Dapper 中,您不需要手动打开数据库连接。 Dapper 会自动为您打开它。更新了帖子。 顺便说一句,如果我们使用一些 DI 框架,我们并不总是需要使用接口。如果我们编写单元测试并模拟我们的存储库,我们肯定需要接口。如果没有,我们可以使用没有接口的类。此外,通常我们不需要使用 AddScoped 或 AddTransient,因为存储库没有任何状态(不是静态变量),我们可以使用 AddSingleton 代替并节省一些 CPU 周期以进行更有用的工作。 (这部分与@FelixOuttaSpace 评论有关)【参考方案3】:

我使用从配置中检索连接字符串的属性创建了扩展方法。这让调用者不必知道有关连接的任何信息,无论它是打开还是关闭等。此方法确实会限制您,因为您隐藏了一些 Dapper 功能,但在我们相当简单的应用程序中,它对我们来说工作得很好,如果我们需要 Dapper 的更多功能,我们总是可以添加一个新的扩展方法来公开它。

internal static string ConnectionString = new Configuration().ConnectionString;

    internal static IEnumerable<T> Query<T>(string sql, object param = null)
    
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        
            conn.Open();
            return conn.Query<T>(sql, param);
        
    

    internal static int Execute(string sql, object param = null)
    
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        
            conn.Open();
            return conn.Execute(sql, param);
        
    

【讨论】:

这里有一个问题。由于 conn.Query 返回 IEnumerable 立即处置连接对象是否安全? IEnumerable 不需要连接以便在读取元素时实现元素吗?我们应该运行 ToList() 吗? 我必须回到 Dapper 进行验证,但我很确定我从工作生产代码中采用了这种模式。应该没问题 - 当然,您应该测试互联网上的任何代码。 如果您使用的是 dapper Query 扩展方法,则无需像在方法本身中那样显式打开连接。 上面代码的问题是,如果你将buffered: true传递给Query方法,连接会在数据返回之前被dispose。在内部,Dapper 会在返回之前将枚举转换为列表。 @BrianVallelunga 不会是buffered: false【参考方案4】:

我是这样做的:

internal class Repository : IRepository 

    private readonly Func<IDbConnection> _connectionFactory;

    public Repository(Func<IDbConnection> connectionFactory) 
    
        _connectionFactory = connectionFactory;
    

    public IWidget Get(string key) 
        using(var conn = _connectionFactory()) 
        
            return conn.Query<Widget>(
               "select * from widgets with(nolock) where widgetkey=@WidgetKey", new  WidgetKey=key );
        
    

然后,无论我在哪里连接我的依赖项(例如:Global.asax.cs 或 Startup.cs),我都会执行以下操作:

var connectionFactory = new Func<IDbConnection>(() => 
    var conn = new SqlConnection(
        ConfigurationManager.ConnectionStrings["connectionString-name"];
    conn.Open();
    return conn;
);

【讨论】:

这里有一个问题。由于 conn.Query 返回 Ienumerable 立即处理连接是否安全? IEnumerable 不需要连接以便在读取元素时具体化它们吗? @AdrianNasui:目前,Dapper 的默认行为是执行您的 SQL 并在返回时缓冲整个阅读器,因此 IEnumerable&lt;T&gt; 已经实现。如果您通过buffered: false,是的,您需要在退出using 块之前消耗输出。【参考方案5】:

最佳实践是一个真正的加载术语。我喜欢DbDataContext 风格的容器,比如Dapper.Rainbow 推广。它允许您耦合CommandTimeout、事务和其他助手。

例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;

using Dapper;

// to have a play, install Dapper.Rainbow from nuget

namespace TestDapper

    class Program
    
        // no decorations, base class, attributes, etc 
        class Product 
        
            public int Id  get; set; 
            public string Name  get; set; 
            public string Description  get; set; 
            public DateTime? LastPurchase  get; set; 
        

        // container with all the tables 
        class MyDatabase : Database<MyDatabase>
        
            public Table<Product> Products  get; set; 
        

        static void Main(string[] args)
        
            var cnn = new SqlConnection("Data Source=.;Initial Catalog=tempdb;Integrated Security=True");
            cnn.Open();

            var db = MyDatabase.Init(cnn, commandTimeout: 2);

            try
            
                db.Execute("waitfor delay '00:00:03'");
            
            catch (Exception)
            
                Console.WriteLine("yeah ... it timed out");
            


            db.Execute("if object_id('Products') is not null drop table Products");
            db.Execute(@"create table Products (
                    Id int identity(1,1) primary key, 
                    Name varchar(20), 
                    Description varchar(max), 
                    LastPurchase datetime)");

            int? productId = db.Products.Insert(new Name="Hello", Description="Nothing" );
            var product = db.Products.Get((int)productId);

            product.Description = "untracked change";

            // snapshotter tracks which fields change on the object 
            var s = Snapshotter.Start(product);
            product.LastPurchase = DateTime.UtcNow;
            product.Name += " World";

            // run: update Products set LastPurchase = @utcNow, Name = @name where Id = @id
            // note, this does not touch untracked columns 
            db.Products.Update(product.Id, s.Diff());

            // reload
            product = db.Products.Get(product.Id);


            Console.WriteLine("id: 0 name: 1 desc: 2 last 3", product.Id, product.Name, product.Description, product.LastPurchase);
            // id: 1 name: Hello World desc: Nothing last 12/01/2012 5:49:34 AM

            Console.WriteLine("deleted: 0", db.Products.Delete(product.Id));
            // deleted: True 


            Console.ReadKey();
        
    

【讨论】:

OP 不是在询问有关 SqlConnection([[CONN STRING HERE]]) 部分的更多信息吗?他说“但是在每个类中(甚至在每个方法中)引用连接字符串对我来说感觉不对”我想他想知道我们 Dapper 用户是否已经生成了一种模式(某种)围绕将事物的连接创建方面包装到干燥/隐藏该逻辑。 (除了 OP,如果您可以使用 Dapper.Rainbow,请这样做......这真的很好!)【参考方案6】:

试试这个:

public class ConnectionProvider
    
        DbConnection conn;
        string connectionString;
        DbProviderFactory factory;

        // Constructor that retrieves the connectionString from the config file
        public ConnectionProvider()
        
            this.connectionString = ConfigurationManager.ConnectionStrings[0].ConnectionString.ToString();
            factory = DbProviderFactories.GetFactory(ConfigurationManager.ConnectionStrings[0].ProviderName.ToString());
        

        // Constructor that accepts the connectionString and Database ProviderName i.e SQL or Oracle
        public ConnectionProvider(string connectionString, string connectionProviderName)
        
            this.connectionString = connectionString;
            factory = DbProviderFactories.GetFactory(connectionProviderName);
        

        // Only inherited classes can call this.
        public DbConnection GetOpenConnection()
        
            conn = factory.CreateConnection();
            conn.ConnectionString = this.connectionString;
            conn.Open();

            return conn;
        

    

【讨论】:

您如何处理解决方案中的关闭/处置连接? @JPshook - 我相信他正在使用。 (参考***.com/a/4717859/2133703)【参考方案7】:

似乎每个人都过早地打开他们的连接?我也有同样的问题,在这里挖掘了源代码后 - https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper/SqlMapper.cs

您会发现每次与数据库的交互都会检查连接以查看它是否已关闭,并在必要时打开它。因此,我们只使用上面没有 conn.open() 的 using 语句。这样,连接就会在尽可能接近交互的地方打开。如果您注意到,它也会立即关闭连接。这也将比在处置过程中自动关闭更快。

上面的 repo 中的许多例子之一:

    private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action<IDbCommand, object> paramReader)
    
        IDbCommand cmd = null;
        bool wasClosed = cnn.State == ConnectionState.Closed;
        try
        
            cmd = command.SetupCommand(cnn, paramReader);
            if (wasClosed) cnn.Open();
            int result = cmd.ExecuteNonQuery();
            command.OnCompleted();
            return result;
        
        finally
        
            if (wasClosed) cnn.Close();
            cmd?.Dispose();
        
    

下面是一个小例子,说明我们如何为 Dapper 使用 Wrapper,称为 DapperWrapper。这允许我们包装所有 Dapper 和 Simple Crud 方法来管理连接、提供安全性、日志记录等。

  public class DapperWrapper : IDapperWrapper
  
    public IEnumerable<T> Query<T>(string query, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
    
      using (var conn = Db.NewConnection())
      
          var results = conn.Query<T>(query, param, transaction, buffered, commandTimeout, commandType);
          // Do whatever you want with the results here
          // Such as Security, Logging, Etc.
          return results;
      
    
  

【讨论】:

这真的很有用,因为 Dapper 会在连接时保持打开状态,如果它在获得连接时已经打开。我现在在通过/使用 Dapper 之前预先打开数据库连接,我获得了 6 倍的性能提升 - 谢谢! 事务由连接创建,如IDbTransaction tran = conn.BeginTransaction();。在您的代码中,包装器创建自己的连接。因此,包装器的事务参数被破坏/误导,不是吗? @MarredCheese 我已经有几年没有使用 Dapper 了。但是,我相信当我们使用这个包装器时,我们使用的 TransactionScope 类似于这里的讨论 - ***.com/a/10363978/3845625。 Dapper * 可能足够聪明,可以将来自多个连接的事务连接在一起,但我不确定这一点。这是来源 - github.com/DapperLib/Dapper/blob/… 祝你好运。【参考方案8】:

我用辅助类包装连接:

public class ConnectionFactory

    private readonly string _connectionName;

    public ConnectionFactory(string connectionName)
    
        _connectionName = connectionName;
    

    public IDbConnection NewConnection() => new SqlConnection(_connectionName);

    #region Connection Scopes

    public TResult Scope<TResult>(Func<IDbConnection, TResult> func)
    
        using (var connection = NewConnection())
        
            connection.Open();
            return func(connection);
        
    

    public async Task<TResult> ScopeAsync<TResult>(Func<IDbConnection, Task<TResult>> funcAsync)
    
        using (var connection = NewConnection())
        
            connection.Open();
            return await funcAsync(connection);
        
    

    public void Scope(Action<IDbConnection> func)
    
        using (var connection = NewConnection())
        
            connection.Open();
            func(connection);
        
    

    public async Task ScopeAsync<TResult>(Func<IDbConnection, Task> funcAsync)
    
        using (var connection = NewConnection())
        
            connection.Open();
            await funcAsync(connection);
        
    

    #endregion Connection Scopes

使用示例:

public class PostsService

    protected IConnectionFactory Connection;

    // Initialization here ..

    public async Task TestPosts_Async()
    
        // Normal way..
        var posts = Connection.Scope(cnn =>
        
            var state = PostState.Active;
            return cnn.Query<Post>("SELECT * FROM [Posts] WHERE [State] = @state;", new  state );
        );

        // Async way..
        posts = await Connection.ScopeAsync(cnn =>
        
            var state = PostState.Active;
            return cnn.QueryAsync<Post>("SELECT * FROM [Posts] WHERE [State] = @state;", new  state );
        );
    

所以我不必每次都显式打开连接。 此外,为了方便未来的重构,您可以这样使用它:

var posts = Connection.Scope(cnn =>

    var state = PostState.Active;
    return cnn.Query<Post>($"SELECT * FROM [TableName<Post>()] WHERE [nameof(Post.State)] = @nameof(state);", new  state );
);

TableName&lt;T&gt;()是什么this answer。

【讨论】:

【参考方案9】:

嗨@donaldhughes 我也是新手,我经常这样做: 1 - 创建一个类来获取我的连接字符串 2 - 在 Using 中调用连接字符串类

看:

DapperConnection.cs

public class DapperConnection


    public IDbConnection DapperCon 
        get
        
            return new SqlConnection(ConfigurationManager.ConnectionStrings["Default"].ToString());

        
    

DapperRepository.cs

  public class DapperRepository : DapperConnection
  
       public IEnumerable<TBMobileDetails> ListAllMobile()
        
            using (IDbConnection con = DapperCon )
            
                con.Open();
                string query = "select * from Table";
                return con.Query<TableEntity>(query);
            
        
     

而且效果很好。

【讨论】:

以上是关于如何在 .NET 中使用 Dapper 处理数据库连接?的主要内容,如果未能解决你的问题,请参考以下文章

dapper.net 转载

如何在 Dapper.NET 中使用事务?

.NET Dapper - 映射计算属性

.Net Core中Dapper的使用详解

Asp.net 面向接口可扩展框架之数据处理模块及EntityFramework扩展和Dapper扩展(含干货)

Dapper-小型ORM之王(C#.NET)