Bawiłem się z Dapperem, ale nie jestem pewien, jak najlepiej obsłużyć połączenie z bazą danych.
Większość przykładów przedstawia obiekt połączenia tworzony w klasie przykładowej lub nawet w każdej metodzie. Ale wydaje mi się niewłaściwe odwoływanie się do parametrów połączenia w każdym pliku clss, nawet jeśli jest ono pobierane z pliku web.config.
Moje doświadczenie dotyczy używania DbDataContext
lub DbContext
z Linq to SQL lub Entity Framework, więc jest to dla mnie nowość.
Jak ustrukturyzować moje aplikacje internetowe, gdy używam Dapper jako strategii dostępu do danych?
Odpowiedzi:
Microsoft.AspNetCore.All : v2.0.3 | Dapper : v1.50.2
Nie jestem pewien, czy prawidłowo używam najlepszych praktyk, czy nie, ale robię to w ten sposób, aby obsłużyć wiele parametrów połączenia.
Jest to łatwe, jeśli masz tylko 1 ciąg połączenia
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); } // ...... } }
Problemy, jeśli masz więcej niż 1 ciąg połączenia
Ponieważ
Dapper
wykorzystujeIDbConnection
, musisz wymyślić sposób na rozróżnienie różnych połączeń z bazą danych.Próbowałem stworzyć wiele interfejsów „dziedziczonych” z
IDbConnection
, odpowiadających różnym połączeniom z bazą danych i wstrzyknąćSqlConnection
różne parametry połączenia z bazą danychStartup
.To się nie udało, ponieważ
SqlConnection
dziedziczyDbConnection
iDbConnection
uzupełnia nie tylko,IDbConnection
ale takżeComponent
klasę. Więc twoje niestandardowe interfejsy nie będą mogły korzystać tylko zSqlConnection
implementacji.Próbowałem również stworzyć własną
DbConnection
klasę, która przyjmuje różne parametry połączenia. To zbyt skomplikowane, ponieważ musisz zaimplementować wszystkie metody zDbConnection
klasy. Straciłeś pomoc odSqlConnection
.Co w końcu robię
Startup
załadowania wszystkich wartości parametrów połączenia do słownika. Utworzyłem równieżenum
dla wszystkich nazw połączeń bazy danych, aby uniknąć magicznych ciągów.IDbConnection
, utworzyłemIDbConnectionFactory
i wstrzyknąłem to jako przejściowe dla wszystkich repozytoriów. Teraz wszystkie repozytoria biorąIDbConnectionFactory
zamiastIDbConnection
.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 - własne wdrożenie fabryczne
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); } } }
Następnie dla innych repozytoriów, które muszą komunikować się z innymi połączeniami, możesz utworzyć dla nich inną klasę bazową repozytorium.
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); } // ...... } }
Mam nadzieję, że wszystkie te pomogą.
źródło
Utworzyłem metody rozszerzające z właściwością, która pobiera parametry połączenia z konfiguracji. Dzięki temu dzwoniący nie muszą nic wiedzieć o połączeniu, czy jest otwarte, czy zamknięte, itp. Ta metoda ogranicza Cię nieco, ponieważ ukrywasz część funkcji Dapper, ale w naszej dość prostej aplikacji działa dobrze dla nas i gdybyśmy potrzebowali większej funkcjonalności od Dappera, zawsze moglibyśmy dodać nową metodę rozszerzenia, która ją ujawnia.
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); } }
źródło
buffered: false
?Zapytano o to jakieś 4 lata temu ... ale tak czy owak, może komuś tutaj przyda się odpowiedź:
Robię to tak we wszystkich projektach. Najpierw tworzę klasę bazową, która zawiera kilka metod pomocniczych, takich jak ta:
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; } }
Mając taką klasę bazową mogę łatwo tworzyć prawdziwe repozytoria bez żadnego standardowego kodu:
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... }
Tak więc cały kod związany z Dapper, SqlConnection-s i innymi elementami dostępu do bazy danych znajduje się w jednym miejscu (BaseRepository). Wszystkie rzeczywiste repozytoria to czyste i proste metody 1-liniowe.
Mam nadzieję, że komuś to pomoże.
źródło
BaseRepository
jest niepotrzebnym dziedziczeniem, ponieważ nie zapewnia żadnych publicznych ani abstrakcyjnych metod ani właściwości. Zamiast tego mogłaby to byćDBHelper
klasa.CreateConnection
do własnej klasy?services.AddScoped<IDbConnection>(p => new SqlConnection(connString)
wtedy po prostu poproś o to w razie potrzebyRobię to tak:
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 }); } } }
Następnie, gdziekolwiek łączę moje zależności (np .: Global.asax.cs lub Startup.cs), robię coś takiego:
var connectionFactory = new Func<IDbConnection>(() => { var conn = new SqlConnection( ConfigurationManager.ConnectionStrings["connectionString-name"]; conn.Open(); return conn; });
źródło
IEnumerable<T>
jest już zmaterializowany. Jeśli zdaszbuffered: false
, tak, będziesz musiał skonsumować dane wyjściowe przed wyjściem zusing
bloku.Najlepsza praktyka to naprawdę obciążony termin. Podoba mi się
DbDataContext
pojemnik w stylu, który promuje Dapper.Rainbow . Pozwala na połączenieCommandTimeout
pomocników, transakcji i innych.Na przykład:
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(); } } }
źródło
Spróbuj tego:
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; } }
źródło
Wygląda na to, że wszyscy otwierają swoje połączenia całkowicie za wcześnie? Miałem to samo pytanie i po przekopaniu się tutaj przez Źródło - https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper/SqlMapper.cs
Przekonasz się, że każda interakcja z bazą danych sprawdza połączenie, aby zobaczyć, czy jest zamknięte, i otwiera je w razie potrzeby. Z tego powodu po prostu używamy takich instrukcji, jak powyżej, bez funkcji conn.open (). W ten sposób połączenie jest otwierane jak najbliżej interakcji. Jeśli zauważysz, natychmiast zamyka połączenie. Będzie to również szybsze niż automatyczne zamykanie podczas usuwania.
Jeden z wielu przykładów tego z powyższego repozytorium:
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(); } }
Poniżej znajduje się mały przykład tego, jak używamy Wrappera dla Dappera o nazwie DapperWrapper. To pozwala nam zawrzeć wszystkie metody Dapper i Simple Crud do zarządzania połączeniami, zapewniania bezpieczeństwa, rejestrowania itp.
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; } } }
źródło
Owijam połączenie z klasą pomocniczą:
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 }
Przykłady użycia:
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 }); }); } }
Nie muszę więc za każdym razem jawnie otwierać połączenia. Dodatkowo możesz to wykorzystać w ten sposób ze względu na wygodę przyszłej refaktoryzacji:
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 }); });
Co
TableName<T>()
można znaleźć w tej odpowiedzi .źródło
Cześć @donaldhughes, też jestem nowy i używam do tego: 1 - Utwórz klasę, aby uzyskać parametry połączenia 2 - Wywołaj klasę parametrów połączenia w Using
Popatrz:
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); } } }
I działa dobrze.
źródło