Як мені обробляти підключення до бази даних за допомогою Dapper у .NET?


86

Я грав з Dapper, але я не впевнений, що найкращий спосіб обробляти підключення до бази даних.

Більшість прикладів показують об’єкт підключення, який створюється в класі прикладу або навіть у кожному методі. Але мені здається неправильним посилатися на рядок з'єднання в кожному clss, навіть якщо він витягується з web.config.

Я мав досвід використання DbDataContextабо DbContextз Linq to SQL або Entity Framework, тому це для мене нове.

Як структурувати веб-програми, використовуючи Dapper як стратегію доступу до даних?


Запізно, але; Я реалізував це так: stackoverflow.com/a/45029588/5779732
Аміт Джоші

using-dapper-asynchronously-in-asp-net-core-2 - exceptionnotfound.net/…
Himalaya Garg

Відповіді:


54

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, що відповідають різним з'єднанням з базою даних, і вводити SqlConnectionз різними рядками підключення до бази даних Startup.

Це не вдалося, оскільки SqlConnectionуспадковує DbConnectionта DbConnectionдоповнює не тільки, IDbConnectionале й Componentклас. Отже, ваші користувацькі інтерфейси не зможуть використовувати лише SqlConnectionімплементацію.

Я також намагався створити власний DbConnectionклас, який приймає різні рядки з'єднання. Це занадто складно, тому що вам доведеться реалізувати всі методи з DbConnectionкласу. Ви втратили допомогу від SqlConnection.

Що я в підсумку роблю

  1. Протягом Startupя завантажив усі значення рядків підключення до словника. Я також створив enumдля всіх імен підключення до бази даних, щоб уникнути магічних рядків.
  2. Я ввів словник як Singleton.
  3. Замість того IDbConnection, IDbConnectionFactoryщоб вводити , я створив і ввів це як Перехідний для всіх сховищ. Тепер усі сховища беруть IDbConnectionFactoryзамість IDbConnection.
  4. Коли правильно вибрати зв’язок? У конструкторі всіх сховищ! Щоб зробити речі чистими, я створив базові класи репозиторію і маю сховища, що успадковуються від базових класів. Правильний вибір рядка підключення може відбуватися в базових класах.

DatabaseConnectionName.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);
        }

        // ......
    }
}

Сподіваюся, вся ця допомога.


Саме те, що я шукаю. У мене була та сама проблема, і я вирішив її однаково, я все ще не знаю, чи є це хорошою практикою, але, на мій погляд, я думаю, що це так.
Евертон,

1
Чи було б краще зареєструвати IDbConnection для сфери дії IServiceProvider? Можна створити службу та зареєструватися як фабрика одномісних областей з різними підключеннями та використовуючи var scope = factory.CreateNonDefaultScope (); використовуючи var connection = scope.ServiceProvider.GetRequiredService <IDbConnection> () ви отримаєте підключення не за замовчуванням. Менше успадкування також допоможе при розширюваності ...
теж

Це те, що я шукаю. Чудова робота @David. Дякую
Шашват Пракаш

27

Я створив методи розширення із властивістю, яка отримує рядок з'єднання з конфігурації. Це дозволяє абонентам не знати нічого про підключення, незалежно від того, відкрите воно чи закрите тощо. Цей метод вас трохи обмежує, оскільки ви приховуєте деяку функціональність 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);
        }
    }

1
Одне питання тут. Оскільки conn.Query повертає IEnumerable <T>, чи безпечно негайно утилізувати об’єкт підключення? Чи не потрібен IEnumerable з'єднання для того, щоб матеріалізувати елементи під час їх читання? Чи слід запускати ToList ()?
Адріан Насуї,

Мені довелося б повернутися до Dapper, щоб перевірити, але я впевнений, що взяв цей шаблон як із робочого виробничого коду. Це повинно бути нормально - але, звичайно, ви повинні протестувати будь-який код в Інтернеті.
Шон Хаббард,

2
Якщо ви використовуєте метод розширення Dapper Query, вам не потрібно відкривати з'єднання явно, як це робиться в самому методі.
h-rai

4
Проблема з наведеним вище кодом полягає в тому, що якщо ви передасте в буфер: true для методу Query, з’єднання буде видалено до повернення даних. Внутрішньо Dapper перетворить перелічене у список перед поверненням.
Брайан Валлелунга

@BrianVallelunga не було б цього buffered: false?
Джодрелл,

26

Його запитали близько 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-рядкові методи.

Сподіваюся, це комусь допоможе.


2
BaseRepositoryє непотрібним успадкуванням, оскільки не надає жодних загальнодоступних або абстрактних методів чи властивостей. Натомість це може бути DBHelperклас.
Джош Ное

Може, краще перейти CreateConnectionдо власного класу?
hellboy

Може бути ... Але особисто мені подобається робити все простим. Якщо у вас багато логіки в CreateConnection (...), це може бути непоганою ідеєю. У моїх проектах цей метод такий же простий, як "повернення нового підключення (connectionString)", тому його можна використовувати вбудовано без окремого методу CreateConnection (...).
Павло Мельников

1
Крім того, як зазначив nick-s, в останніх версіях Dapper не потрібно відкривати підключення до бази даних вручну. Dapper відкриє його вам автоматично. оновив допис.
Павло Мельников

вколоти його imo. services.AddScoped<IDbConnection>(p => new SqlConnection(connString)тоді просто попросіть його там, де це потрібно
Сінестетичний

8

Я роблю це так:

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 повертає незліченну кількість <T>, чи безпечно негайно утилізувати з'єднання? Чи не потрібен IEnumerable з'єднання для того, щоб матеріалізувати елементи під час їх читання?
Адріан Насуї,

1
@AdrianNasui: В даний час поведінка Dapper за замовчуванням полягає у виконанні вашого SQL та буферизації всього зчитувача при поверненні, тому IEnumerable<T>це вже матеріалізовано. Якщо ви пройдете buffered: false, так, вам потрібно буде спожити вихідні дані перед виходом з usingблоку.
Jacob Krall

7

Найкраща практика - це справді завантажений термін. Мені подобається такий DbDataContextстильний контейнер, як Dapper . Це дозволяє поєднати 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();
        }
    }
}

15
Хіба OP не запитує більше про частину SqlConnection ([[CONN STRING HERE]])? Він каже: "Але мені здається неправильним посилатися на рядок з'єднання в кожному класі (навіть у кожному методі)". Я думаю, йому цікаво, чи ми, користувачі Dapper, створили шаблон (свого роду) навколо обгортання сторони речей, що створюють з'єднання, до СУХИ / приховати цю логіку. (Окрім ОП, якщо ви можете використовувати Dapper.Rainbow, зробіть це ... це дуже приємно!)
ckittel

4

Спробуйте це:

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;
        }

    }

6
Як ви обробляєте закриття / утилізацію з'єднання у своєму рішенні?
jpshook

@JPShook - Я вважаю, що він використовує використання. (ref stackoverflow.com/a/4717859/2133703 )
MacGyver

4

Здається, усі відкривають свої зв’язки зовсім рано? У мене було таке саме запитання, і після перекопування джерела тут - https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper/SqlMapper.cs

Ви побачите, що кожна взаємодія з базою даних перевіряє з’єднання, чи воно закрите, і відкриває його за необхідності. Завдяки цьому ми просто використовуємо такі оператори, як вище, без conn.open (). Таким чином зв’язок відкривається якомога ближче до взаємодії. Якщо ви помітили, це також негайно перериває зв’язок. Це також буде швидше, ніж воно автоматично закриється під час утилізації.

Один з багатьох прикладів цього з репо-файлу вище:

    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();
        }
    }

Нижче наведено невеликий приклад того, як ми використовуємо Wrapper для Dapper, який називається 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;
      }
    }
  }

1
Це дуже корисно, знаючи, що Dapper залишить з'єднання відкритим, якщо воно вже відкрите, коли воно його отримає. Зараз я попередньо відкриваю з’єднання db, перш ніж передавати / використовувати його з Dapper, і я отримав 6-кратний приріст продуктивності - дякую!
Кріс Сміт

2

Я обертаю з'єднання з допоміжним класом:

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<T>()можна знайти в цій відповіді .


0

Привіт @donaldhughes Я теж у цьому новачок, і я використовую для цього: 1 - Створіть клас, щоб отримати мій рядок підключення 2 - Зателефонуйте до класу рядка підключення в

Подивіться:

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);
            }
        }
     }

І це чудово працює.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.