Repository 模式
学习一下在 ASP.NET Core 中的 Repository 模式以及 Unit of Work 模式,了解为什么以及如何将 Repository 模式与 Adapter 模式相结合,以更好地实现和测试数据访问层。
应该使用Repository模式吗?
最近看了一些文章,讨论说在当今现代应用程序中,是否真的需要使用 Repository 模式,我觉得架构模式也好,设计模式也罢,最好不要下决定的好与坏定义。
每一位工程师都会基于自己的项目经历,业务场景,形成自己的看法和观点,可能因为在实际业务中用了某个模式后,导致一些问题,如开发成本增大,难以维护,性能降低等等,会认为应该避免使用一些模式。但是,问题的出现并不一定就是使用了某个模式导致的,里边有很多因素,应该把模式当作工具来看,一个完美的系统会用到很多工具,不应该因为某个工具而抱怨,需要的是改进和不断的尝试,时刻保持开发的心态。
任何模式都有正反两面,它取决于正在开发的应用程序的许多方面。应用程序的大小、数据访问逻辑的性质、所使用的编程语言、开发人员的技能等所有方面,都应该在决定是否使用该模式之前加以考虑。存储库模式也是如此。
如果你正在开发一个非常小的应用程序,它没有太多的代码或功能,而且在部署后这个应用程序不会经历很多变化,那么在这种情况下,为了保持代码简单,你可以避免使用 Repository 模式,你甚至可以把代码直接写进 Controller 里边,封装一些方法即可。
但是,对于大型和复杂的应用程序,可以通过在应用程序中实现 Repository 模式获得一些好处。Repository 模式封装了数据访问层逻辑,它与其它层解耦。自定义 Repository 添加了一个抽象层,可以很容易地对其进行 Mock,以便对组件进行单元测试。
Repository & Unit of Work Pattern
存储库是为数据实体(即数据库中的表)实现的数据访问逻辑。存储库操作通常包括 CRUD 操作和该数据实体的任何其他特殊操作。应用层使用存储库公开的 API 通过存储库执行数据库访问。
现在,应用层方面通常要求在单个事务中执行两个或多个数据实体(即数据库中的表)的数据库操作。单个事务意味着,如果对任何一个数据实体的操作失败,则该事务中的其他操作不应发生,或者如果已经执行,则应回滚,比如新增学生使用到了三个 Repository,分别是 StudentRepository,StudentInfoRepository,StudentAddressRepository,新增学生信息和详细信息都成功,而新增地址失败了,那么就应该回滚,前两个操作也不应该成功。
工作单元模式提供了单一事务特性的这种实现。工作单元将有助于将来自两个或多个存储库的多个操作合并为一个业务事务。工作单元模式将确保单个事务中的所有 CRUD 操作都是成功的,并且只提交一次。如果一个操作失败,整个事务将被回滚。
如果 ORM 使用的是 EF Core,那么它已经作为一个工作单元。因此,对于 EF Core 不需要为工作单元模式实现任何东西。唯一需要确保的是,DbContext 对象应在同一 HTTP 请求范围内的多个存储库之间共享即可。
ASP.Net Core 实现Repository模式
在 ASP.Net Core 中实现存储库模式,步骤如下:
- 创建一个
ASP.Net Core WebAPI
项目,命名为API
,用于与前端交互,接收请求。 - 创建类库,命名为
DAL
,存放Repository
实现类,DbContext
,迁移信息等。 - 创建类库,命名为
Common
,存放数据库表实体Entity
,以及存储库接口。
创建实体
首先创建API项目,并创建 Models 文件夹,并新建Student类,用于接收前端传入的表单信息
public class Student
{
public string StudentCode { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PinCode { get; set; }
public string Country { get; set; }
public int Grade { get; set; }
public string PreferredSport { get; set; }
}
学生薪资最终要保存到三个表中,分别是学生信息,地址信息,参与运动项目信息,所以需要创建三个实体类,在 Common 库下创建 DbEntities 文件夹,创建三个实体
public class StudentEntity
{
public int Id { get; set; }
public string StudentCode { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public int Grade { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime LastModifiedOn { get; set; }
public string LastModifiedBy { get; set; }
public bool IsDeleted { get; set; }
}
学生地址
public class StudentAddressEntity
{
public int Id { get; set; }
public string StudentCode { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PinCode { get; set; }
public string Country { get; set; }
public bool IsPrimary { get; set; }
public bool IsDeleted { get; set; }
}
参与运动
public class StudentSportEntity
{
public int Id { get; set; }
public string StudentCode { get; set; }
public string Sport { get; set; }
public bool IsDeleted { get; set; }
}
配置 EF Core
安装和配置 EF Core 来实现学生类的 CRUD 操作,以演示 ASP.NET Core 中的存储库模式。首先在 DAL
层下安装两个 Nuget 包,如下:
Install-Package Microsoft.EntityFrameworkCore
Install-Package Pomelo.EntityFrameworkCore.MySql
在 API 项目里边安装需要的迁移工具,如下:
Install-Package Microsoft.EntityFrameworkCore.Tools
注意:Microsoft.EntityFrameworkCore.Tools 不要安装在 DAL 层,一定要放在启动项目中,否则有可能会报错,有兴趣可以尝试了解一下原因。
添加 DbContext 类
在 DAL 层创建 DbContexts 文件夹,并新建 ApplicationDbContext 类
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<StudentEntity> Students { get; set; }
public DbSet<StudentSportEntity> StudentSport { get; set; }
public DbSet<StudentAddressEntity> StudentAddress { get; set; }
}
添加 ConnectionString
打开启动项目的 appsettings.json 文件,添加连接字符串
"ConnectionStrings": {
"DefaultConnection": "server=服务器地址;port=3306;uid=root;pwd=dcy990411@.;database=student_db"
}
注册 ApplicationDbContext 到容器
打开启动项目的 Program 文件,将 ApplicationDbContext 注册到容器内,由于 ApplicationDbContext 在 DAL 层,不在启动项目里边,所以要指定 MigrationsAssembly
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
ef => ef.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
}, ServiceLifetime.Scoped);
Add Migrations
打开程序包管理控制台,把项目切换为 DAL
,然后执行命令
add-migration FirstMigration
update-database
Create Repositories for Student
创建 Repository 接口以及实现类,接口创建在 Common 层,特定实现类创建在 DAL 层,不要耦合到一起,DAL 层是对特定数据库的实现,
在 Common 层创建 Interfaces 文件夹,首先添加通用存储库 IGenericRepository.cs,接收泛型参数,定义基本的 CRUD 操作,这样可以复用
public interface IGenericRepository<T> where T : class
{
void Add(T entity);
T GetById(int id);
void Remove(T entity);
IEnumerable<T> GetAll();
int Complete();
}
在 DAL 层创建 Repositories 文件夹,并添加 GenericRepository.cs
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly ApplicationDbContext _dbcontext;
public GenericRepository(ApplicationDbContext dbcontext)
{
_dbcontext = dbcontext;
}
public void Add(T entity)
{
_dbcontext.Set<T>().Add(entity);
}
public T GetById(int id)
{
return _dbcontext.Set<T>().Find(id);
}
public void Remove(T entity)
{
_dbcontext.Set<T>().Remove(entity);
}
public IEnumerable<T> GetAll()
{
return _dbcontext.Set<T>().ToList();
}
public int Complete()
{
return _dbcontext.SaveChanges();
}
}
继续添加对特定实体的存储库,如果有需要,将向这些学生特定存储库添加特殊的必需操作。这个 IStudentRepository 接口将派生自 IGenericRepository 接口。通过这种方式,将从通用存储库继承学生存储库的所有基本操作。
添加 Interfaces\IStudentRepository.cs
public interface IStudentRepository : IGenericRepository<StudentEntity>
{
StudentEntity GetByStudentCode(string studentCode);
}
添加实现类 Repositories\StudentRepository.cs
public class StudentRepository : GenericRepository<StudentEntity>, IStudentRepository
{
public StudentRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
public StudentEntity GetByStudentCode(string studentCode)
{
return _dbcontext.Students.Where(student => student.StudentCode.Equals(studentCode)).FirstOrDefault();
}
}
添加 Interfaces\IStudentAddressRepository.cs
public interface IStudentAddressRepository : IGenericRepository<StudentAddressEntity>
{
StudentAddressEntity GetByStudentCode(string studentCode);
}
添加 Repositories\StudentAddressRepository.cs
public class StudentAddressRepository : GenericRepository<StudentAddressEntity>, IStudentAddressRepository
{
public StudentAddressRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
public StudentAddressEntity GetByStudentCode(string studentCode)
{
return _dbcontext.StudentAddress.Where(address => address.StudentCode.Equals(studentCode)).FirstOrDefault(); ;
}
}
添加 Interfaces\IStudentSportRepository.cs
public interface IStudentSportRepository : IGenericRepository<StudentSportEntity>
{
StudentSportEntity GetByStudentCode(string studentCode);
}
添加 Repositories\StudentSportRepository.cs
public class StudentSportRepository : GenericRepository<StudentSportEntity>, IStudentSportRepository
{
public StudentSportRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
public StudentSportEntity GetByStudentCode(string studentCode)
{
return _dbcontext.StudentSport.Where(sport => sport.StudentCode.Equals(studentCode)).FirstOrDefault(); ;
}
}
OK,最后注册到容器
builder.Services.AddTransient<IStudentRepository, StudentRepository>();
builder.Services.AddTransient<IStudentAddressRepository, StudentAddressRepository>();
builder.Services.AddTransient<IStudentSportRepository, StudentSportRepository>();
添加适配器类
因为用于接收前端传入参数的类是 Student,而数据库表对应的实体是 StudentEntity,因此需要在模型和数据实体之间转换对象。
使用适配器模式在这些对象之间进行转换,因为适配器模式是一种结构设计模式,允许具有不兼容接口的对象进行协作。
在启动项目创建文件夹 Adapter,添加 IStudentAdapter.cs
public interface IStudentAdapter
{
StudentEntity Adapt(Student student);
StudentAddressEntity AdaptToStudentAddress(Student student);
StudentSportEntity AdaptToStudentSport(Student student);
Student Adapt(StudentEntity studentEntity, StudentAddressEntity studentAddressEntity, StudentSportEntity studentSportEntity);
}
实现接口,添加 StudentAdapter.cs
public class StudentAdapter : IStudentAdapter
{
public StudentEntity Adapt(Student student)
{
return new StudentEntity()
{
StudentCode = student.StudentCode,
Name = student.Name,
Grade = student.Grade,
Age = student.Age,
CreatedOn = DateTime.Now,
IsDeleted = false,
LastModifiedOn = DateTime.Now,
LastModifiedBy = "User 1"
};
}
public StudentSportEntity AdaptToStudentSport(Student student)
{
return new StudentSportEntity()
{
StudentCode = student.StudentCode,
Sport = student.PreferredSport,
IsDeleted = false
};
}
public StudentAddressEntity AdaptToStudentAddress(Student student)
{
return new StudentAddressEntity()
{
Address = student.Address,
State = student.State,
City = student.City,
Country = student.Country,
PinCode = student.PinCode,
StudentCode = student.StudentCode,
IsPrimary = true,
IsDeleted = false
};
}
public Student Adapt(StudentEntity studentEntity, StudentAddressEntity studentAddressEntity, StudentSportEntity studentSportEntity)
{
return new Student()
{
StudentCode = studentEntity.StudentCode,
Name = studentEntity.Name,
Age = studentEntity.Age,
Grade = studentEntity.Grade,
Address = studentAddressEntity.Address,
City = studentAddressEntity.City,
Country = studentAddressEntity.Country,
PinCode = studentAddressEntity.PinCode,
State = studentAddressEntity.State,
PreferredSport = studentSportEntity.Sport
};
}
}
OK,别忘了注册服务
builder.Services.AddTransient<IStudentAdapter, StudentAdapter>();
Add Services for Unit of Work & Student
工作单元服务基于工作单元模式,该模式保存可用的 Repository,还实现了保存更改的逻辑,以便我们能够通过一次提交将数据添加到多个存储库(表)中,以确保数据的完整性。比如在当前案例中,需要保存学生基本信息,地址,运动三张表,用到三个 Repository,把3个 Repository 保存在 Unit of Work 中,以确保要么将记录插入到所有 3 个表中。如果在任何一个表中插入操作失败,则回滚完整的保存操作。
在启动项目中创建 Services 文件夹,并添加 IUnitOfWorkService.cs
public interface IUnitOfWorkService
{
int Save();
IStudentRepository Student { get; set; }
IStudentAddressRepository StudentAddress { get; set; }
IStudentSportRepository StudentSport { get; set; }
}
通用,创建实现类 Services\UnitOfWorkService.cs
public class UnitOfWorkService : IUnitOfWorkService
{
private readonly ApplicationDbContext _dbContext;
public UnitOfWorkService(ApplicationDbContext dbContext, IStudentRepository studentRepository, IStudentSportRepository studentSportRepository, IStudentAddressRepository studentAddressRepository)
{
_dbContext = dbContext;
Student = studentRepository;
StudentSport = studentSportRepository;
StudentAddress = studentAddressRepository;
}
public IStudentRepository Student { get; set; }
public IStudentSportRepository StudentSport { get; set; }
public IStudentAddressRepository StudentAddress { get; set; }
public int Save()
{
return _dbContext.SaveChanges();
}
}
最后为学生添加一个 Service,将利用学生适配器和工作单元服务来保存和检索学生数据。这个服务将采用 Student 模型作为输入,通过使用 Student Adapter,它将把 Student 模型转换为 Student Entity,StudentAddressEntity 和 StudentSportEntity,因为要保存到三张表中。接下来,该服务将使用 Unit of Work 服务向所有 3 个存储库添加数据,并在一个事务中将其保存到数据库中。
在启动项目添加Services\IStudentService.cs
public interface IStudentService
{
void Add(Student student);
Student GetByStudentCode(string studentCode);
IEnumerable<Student> GetAll();
}
创建实现类 Services\StudentService.cs
/// <summary>
/// 学生服务
/// </summary>
public class StudentService : IStudentService
{
private readonly IStudentAdapter _studentAdapter;
private readonly IUnitOfWorkService _unitOfWorkService;
public StudentService(IUnitOfWorkService unitOfWorkService, IStudentAdapter studentAdapter)
{
_unitOfWorkService = unitOfWorkService;
_studentAdapter = studentAdapter;
}
/// <summary>
/// 创建学生信息
/// </summary>
/// <param name="student"></param>
public void Add(Student student)
{
if (Validate(student))
{
_unitOfWorkService.Student.Add(_studentAdapter.Adapt(student));
_unitOfWorkService.StudentSport.Add(_studentAdapter.AdaptToStudentSport(student));
_unitOfWorkService.StudentAddress.Add(_studentAdapter.AdaptToStudentAddress(student));
_unitOfWorkService.Save();
}
}
/// <summary>
/// 获取所有学生信息列表
/// </summary>
/// <returns></returns>
public IEnumerable<Student> GetAll()
{
List<Student> listStudent = new List<Student>();
foreach (StudentEntity studentEntity in _unitOfWorkService.Student.GetAll())
{
listStudent.Add(_studentAdapter.Adapt(studentEntity,
_unitOfWorkService.StudentAddress.GetByStudentCode(studentEntity.StudentCode),
_unitOfWorkService.StudentSport.GetByStudentCode(studentEntity.StudentCode)));
}
return listStudent;
}
/// <summary>
/// 根据 studentCode 获取单个学生信息
/// </summary>
/// <param name="studentCode"></param>
/// <returns></returns>
public Student GetByStudentCode(string studentCode)
{
StudentEntity studentEntity = _unitOfWorkService.Student.GetByStudentCode(studentCode);
return _studentAdapter.Adapt(studentEntity,
_unitOfWorkService.StudentAddress.GetByStudentCode(studentEntity.StudentCode),
_unitOfWorkService.StudentSport.GetByStudentCode(studentEntity.StudentCode));
}
/// <summary>
/// 删除学生
/// </summary>
/// <param name="student"></param>
public void Remove(Student student)
{
_unitOfWorkService.Student.Remove(_studentAdapter.Adapt(student));
_unitOfWorkService.Save();
}
/// <summary>
/// 验证是否有效
/// </summary>
/// <param name="student"></param>
/// <returns></returns>
public bool Validate(Student student)
{
// 校验参数
return true;
}
}
注册服务到容器
builder.Services.AddTransient<IUnitOfWorkService, UnitOfWorkService>();
builder.Services.AddTransient<IStudentService, StudentService>();
创建 StudentController
最后添加学生控制器即可,定义用于前端调用的接口
[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
private readonly IStudentService _studentService;
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
[HttpGet]
public IEnumerable<Student> Get()
{
return _studentService.GetAll();
}
[HttpGet("{studentCode}")]
public Student GetByStudentCode(string studentCode)
{
return _studentService.GetByStudentCode(studentCode);
}
[HttpPost]
[Route("Add")]
public void Add([FromBody] Student student)
{
_studentService.Add(student);
}
}
OK,现在已经在 ASP.NET Core WebAPI 中添加了存储库模式和适配器模式,可以启动项目,通过 swagger 进行调试了。
总结
在实际应用程序中,不断变化的需求、测试复杂性、维护问题、性能问题等等并不那么简单。为了便于维护和更好的可测试性,分离关注点总是有好处的。另外,不要从单一角度看待某个技术,一定要怎么样,任何技术都有正反两面,把它当作工具来看,不断的优化,满足实际业务需要即可。