問題
- 應用程式都有資料存取需求,大多使用關聯式資料庫,使用如ADO.NET, OLE DB…介面,再搭配SQL指令來操作資料
- 過往小型應用程式資料來源通常來自資料庫, 但現在提倡分散式, 雲端化的情況下, 資料來源具有多樣性
- 傳統做法:應用程式邏輯與資料存取介面緊密結合, 會提高整合資料的難度
使用時機
- 資料來源需要有任意抽換的功能
- 抽離對DB操作的功能到Repository
Repository 模式
Layered Architecture: View ⇆ Controller ⇆ Service ⇆ Repository ⇆ Model
- View:資料呈現頁面
- Controller:API的接口
- Service:處裡商業邏輯, 電商有購物、付款邏輯;金融有存錢、轉帳邏輯
- Repository:資料庫操作&資料處理
- Model:資料與DAO的Mapping
為了達到抽換的功能, 使用Interface來達到這個特性, 可以分成泛型跟客製化介面
泛型與客製化Interface
- 泛型IRepository: 所有Repository使用同一個Interface, 僅實作CRUD, 如果資料庫操作不複雜, 使用泛型方式有效減少重複撰寫的程式碼
public interface IGenericRepository<T>
{
Task Create(T entity);
Task<List<T>> GetAll();
Task Update(T entity);
Task Delete(int id);
}
- 客製化IRepository: 針對特定資料Mapping並封裝這些過程, 提高彈性
public interface IBlogRepository
{
// define interface
}
public class BlogRepository : IBlogRepository
{
// implement interfaces
}
public interface ICommentRepository
{
// define interface
}
public class CommentRepository : ICommentRepository
{
// implement interfaces
}
實快速切換Dapper與EF操作
建立通用interface
public interface IGenericRepository<T>
{
Task Create(T entity);
Task<List<T>> GetAll();
Task Update(T entity);
Task Delete(int id);
}
對通用interface繼承後新增一個初始化DB function 的接口
public interface IBlogRepository:IGenericRepository<BlogModel>
{
void InitBlogTable();
}
Dapper Repository 實作
public class DapperBlogRepository : IBlogRepository
{
private readonly ConnectionStringProvider connectionStringProvider;
private static bool isInit = false;
public DapperBlogRepository(ConnectionStringProvider connectionStringProvider)
{
this.connectionStringProvider = connectionStringProvider;
InitBlogTable();
}
public void InitBlogTable()
{
using var connection = new SQLiteConnection(connectionStringProvider.ConnectionString);
connection.Execute(@"
Create table if not exists Blog (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Title VARCHAR(50),
Content VARCHAR(50)
)");
if (!isInit)
{
var data = connection.Query<BlogModel>("select * from Blog").ToList();
if (!data.Any())
{
foreach (var blog in Util.generatorData())
{
connection.Execute("Insert into Blog(Title, Content) values (@Title, @Content)", blog);
}
}
isInit = true;
}
}
public Task Create(BlogModel entity)
{
using var connection = new SQLiteConnection(connectionStringProvider.ConnectionString);
return connection.ExecuteAsync("Insert into Blog(Title, Content) values (@Title, @Content)", entity);
}
public Task Delete(int id)
{
using var connection = new SQLiteConnection(connectionStringProvider.ConnectionString);
return connection.ExecuteAsync("DELETE FROM Blog WHERE Id=@id", new { Id = id });
}
public async Task<List<BlogModel>> GetAll()
{
using var connection = new SQLiteConnection(connectionStringProvider.ConnectionString);
return (await connection.QueryAsync<BlogModel>("select * from Blog")).ToList();
}
public Task Update(BlogModel entity)
{
using var connection = new SQLiteConnection(connectionStringProvider.ConnectionString);
return connection.ExecuteAsync("UPDATE Blog SET Title = @Title, Content = @Content WHERE Id=@Id", entity);
}
}
Controller實作
Controller端建構子注入Repository 使用function
[Route("api/[controller]")]
[ApiController]
public class BlogController : ControllerBase
{
private readonly IBlogRepository blogRepository;
public BlogController(IBlogRepository blogRepository)
{
this.blogRepository = blogRepository;
}
[HttpGet]
public Task<List<BlogModel>> GetAll()
{
return blogRepository.GetAll();
}
[HttpPost]
public Task Create(BlogModel blogModel)
{
return blogRepository.Create(blogModel);
}
[HttpPut]
public Task Update(BlogModel blogModel)
{
return blogRepository.Update(blogModel);
}
[HttpDelete("{id}")]
public Task Delete(int id)
{
return blogRepository.Delete(id);
}
}
依賴注入
在Program.cs啟動Web前依賴注入IBlogRepository
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddScoped<IBlogRepository, DapperBlogRepository>()
//...
var app = builder.Build();
//...
app.Run();
到這裡完成使用Dapper操作DB的api實作
再來加入EntityFramework的Repository實作
EF Repository 實作
public class EFBlogRepository : IBlogRepository
{
private readonly EFDbContext _context;
public EFBlogRepository(EFDbContext context)
{
_context = context;
InitBlogTable();
}
private static bool isInit = false;
public async Task Create(BlogModel entity)
{
await _context.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task Delete(int id)
{
var blog = await _context.Blog.SingleOrDefaultAsync(e => e.Id == id);
if (blog == null)
{
throw new Exception("Not found");
}
_context.Blog.Remove(blog);
await _context.SaveChangesAsync();
}
public Task<List<BlogModel>> GetAll()
{
return _context.Blog.ToListAsync();
}
public void InitBlogTable()
{
if (!isInit)
{
if (!_context.Blog.Any())
{
_context.Blog.AddRange(Common.Common.generatorData());
_context.SaveChanges();
}
isInit = true;
}
}
public async Task Update(BlogModel entity)
{
var blog = await _context.Blog.SingleOrDefaultAsync(e => e.Id == entity.Id);
if (blog == null)
{
throw new Exception("Not found");
}
blog.Title = entity.Title;
blog.Content = entity.Content;
_context.Blog.Update(blog);
await _context.SaveChangesAsync();
}
}
決定注入哪個實作
appsettings.json新增參數
{
// ...,
"DataAccessLibrary": "Dapper" //Dapper or EF
}
Program.cs 讀取appsettings的參數來決定注入哪個Repository
// ...
var dataAccessLibrary = configuration.GetSection("DataAccessLibrary").Get<string>();
if (dataAccessLibrary == "Dapper")
{
Console.WriteLine("Use Dapper");
builder.Services.AddScoped<IBlogRepository, DapperBlogRepository>();
}
else
{
Console.WriteLine("Use EF");
builder.Services.AddScoped<IBlogRepository, EFBlogRepository>();
}
// ...
這樣就完成使用外部參數的方式來快速切換資料庫實作的功能
解決了甚麼
- 上層的服務 (Service) 只要關心 Repository 的抽象化 API, 並且當資料來源變更時, 使用DI抽換掉Repository API 的實作就好
- 減少重複撰寫查詢邏輯