0%

設計模式:Repository Pattern

問題

  • 應用程式都有資料存取需求,大多使用關聯式資料庫,使用如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, 如果資料庫操作不複雜, 使用泛型方式有效減少重複撰寫的程式碼
1
2
3
4
5
6
7
public interface IGenericRepository<T>
{
Task Create(T entity);
Task<List<T>> GetAll();
Task Update(T entity);
Task Delete(int id);
}
  • 客製化IRepository:
    針對特定資料Mapping並封裝這些過程, 提高彈性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IBlogRepository
{
// define interface
}

public class BlogRepository : IBlogRepository
{
// implement interfaces
}

public interface ICommentRepository
{
// define interface
}

public class CommentRepository : ICommentRepository
{
// implement interfaces
}

實快速切換Dapper與EF操作

完整code放在github

建立通用interface

1
2
3
4
5
6
7
8
public interface IGenericRepository<T>
{
Task Create(T entity);
Task<List<T>> GetAll();
Task Update(T entity);
Task Delete(int id);

}

對通用interface繼承後新增一個初始化DB function 的接口

1
2
3
4
public interface IBlogRepository:IGenericRepository<BlogModel>
{
void InitBlogTable();
}

Dapper Repository 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[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

1
2
3
4
5
6
7
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddScoped<IBlogRepository, DapperBlogRepository>()
//...
var app = builder.Build();
//...
app.Run();

到這裡完成使用Dapper操作DB的api實作

再來加入EntityFramework的Repository實作

EF Repository 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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新增參數

1
2
3
4
{
// ...,
"DataAccessLibrary": "Dapper" //Dapper or EF
}

Program.cs 讀取appsettings的參數來決定注入哪個Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
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 的實作就好
  • 減少重複撰寫查詢邏輯