概述
关于数据访问这一块,C#/.Net目前有很多ORM框架可以使用,比如EntityFramework、Dapper、NHibernate等,而NopCommerce采用了微软的EntityFramework。
阅读本文需要相关的一些知识:IOC、Autofac、EntityFramework、泛型等,并且对NopCommerce有少许了解
NopCommerce代码地址:https://github.com/nopSolutions/nopCommerce
EF访问数据,有几个必要条件,我们这里先列举一下:
- 实体模型:Nop.Core.Domain目录下
- 数据库和实体类Mapping关系,路径:Nop.Data.Mapping文件目录下
- Context类,访问数据库的上下文或者叫会话:Nop.Data.NopObjectContext
- 数据仓储管理类:Nop.Data.EfRepository
下面我们就一步步解析Nop的仓储模型
实体模型
Nop的实体类都在Nop.Core.Domain目录下面,按业务分为不同的目录,比如分类:Nop.Core.Domain.Catalog.Category,这里为了避免干扰已经删除了一部分代码,具体可以参考项目代码
public partial class Category : BaseEntity
{
public string Name { get; set; }
public string Description { get; set; }
}
我们发现所有实体类都继承至:Nop.Core.BaseEntity
public abstract partial class BaseEntity
{
public int Id { get; set; }
}
实体类实现跟数据字段的对应,实现数据从DB到程序的转换,如果我们发现基础类里面有个属性Id,如果自己数据库的主键不是Id或者是其它组合主键,这里可以删除
映射关系
Nop的映射关系都在Nop.Data.Mapping目录下面,比如:
public partial class CategoryMap : NopEntityTypeConfiguration<Category>
{
public override void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable(nameof(Category));
builder.HasKey(category => category.Id);
builder.Property(category => category.Name).HasMaxLength(400).IsRequired();
builder.Property(category => category.MetaKeywords).HasMaxLength(400);
builder.Property(category => category.MetaTitle).HasMaxLength(400);
builder.Property(category => category.PriceRanges).HasMaxLength(400);
builder.Property(category => category.PageSizeOptions).HasMaxLength(200);
builder.Ignore(category => category.AppliedDiscounts);
base.Configure(builder);
}
}
里面包含表名、主键、长度等一些说明,这里注意下,我们Map类继承至:NopEntityTypeConfiguration,这里具体怎么使用,我们稍后解析
访问数据库
Nop采用仓储模型,基于泛型实现,为避免了大量代码,减少错误概率。我们先看下仓储接口,代码位置(Nop.Core.Data.IRepository):
public partial interface IRepository<TEntity> where TEntity : BaseEntity
{
TEntity GetById(object id);
void Insert(TEntity entity);
void Insert(IEnumerable<TEntity> entities);
void Update(TEntity entity);
void Update(IEnumerable<TEntity> entities);
void Delete(TEntity entity);
void Delete(IEnumerable<TEntity> entities);
IQueryable<TEntity> Table { get; }
IQueryable<TEntity> TableNoTracking { get; }
}
通过上述接口,我们看到,仓储模型提供了基本的CRUD操作,并且提供了数据访问属性Table 和 TableNoTracking,可以方便我们使用Linq查询,TableNoTracking可以理解为NoLock查询,提高查询性能
我们再看下具体的实现类:Nop.Data.EfRepository
public partial class EfRepository<TEntity> : IRepository<TEntity> where TEntity : BaseEntity
{
#region Fields
private readonly IDbContext _context;
private DbSet<TEntity> _entities;
#endregion
#region Ctor
public EfRepository(IDbContext context)
{
this._context = context;
}
#endregion
#region Utilities
/// <summary>
/// Rollback of entity changes and return full error message
/// </summary>
/// <param name="exception">Exception</param>
/// <returns>Error message</returns>
protected string GetFullErrorTextAndRollbackEntityChanges(DbUpdateException exception)
{
//rollback entity changes
if (_context is DbContext dbContext)
{
var entries = dbContext.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified).ToList();
entries.ForEach(entry => entry.State = EntityState.Unchanged);
}
_context.SaveChanges();
return exception.ToString();
}
#endregion
#region Methods
/// <summary>
/// Get entity by identifier
/// </summary>
/// <param name="id">Identifier</param>
/// <returns>Entity</returns>
public virtual TEntity GetById(object id)
{
return Entities.Find(id);
}
/// <summary>
/// Insert entity
/// </summary>
/// <param name="entity">Entity</param>
public virtual void Insert(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
Entities.Add(entity);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
/// <summary>
/// Insert entities
/// </summary>
/// <param name="entities">Entities</param>
public virtual void Insert(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
try
{
Entities.AddRange(entities);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
/// <summary>
/// Update entity
/// </summary>
/// <param name="entity">Entity</param>
public virtual void Update(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
Entities.Update(entity);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
/// <summary>
/// Update entities
/// </summary>
/// <param name="entities">Entities</param>
public virtual void Update(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
try
{
Entities.UpdateRange(entities);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
/// <summary>
/// Delete entity
/// </summary>
/// <param name="entity">Entity</param>
public virtual void Delete(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
Entities.Remove(entity);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
/// <summary>
/// Delete entities
/// </summary>
/// <param name="entities">Entities</param>
public virtual void Delete(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
try
{
Entities.RemoveRange(entities);
_context.SaveChanges();
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
#endregion
#region Properties
/// <summary>
/// Gets a table
/// </summary>
public virtual IQueryable<TEntity> Table => Entities;
/// <summary>
/// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations
/// </summary>
public virtual IQueryable<TEntity> TableNoTracking => Entities.AsNoTracking();
/// <summary>
/// Gets an entity set
/// </summary>
protected virtual DbSet<TEntity> Entities
{
get
{
if (_entities == null)
_entities = _context.Set<TEntity>();
return _entities;
}
}
#endregion
}
这里面有几个设计比较精巧的地方,比如采用泛型实现,不用写很多仓储模型,其次DbSet的Entities,也不用随着模型的增多,写很多访问属性
仓储模型的实现,基于IDbContext,这里采用了IOC的构造函数注入
接下来我们看下IDbContext的具体实现NopObjectContext(Nop.Data.NopObjectContext),里面代码比较多,我们就注意几个点
public partial class NopObjectContext : DbContext, IDbContext
{
// 数据库的访问属性
public NopObjectContext(DbContextOptions<NopObjectContext> options) : base(options)
{
}
//模型的执行
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//dynamically load all entity and query type configurations
var typeConfigurations = Assembly.GetExecutingAssembly().GetTypes().Where(type =>
(type.BaseType?.IsGenericType ?? false)
&& (type.BaseType.GetGenericTypeDefinition() == typeof(NopEntityTypeConfiguration<>)
|| type.BaseType.GetGenericTypeDefinition() == typeof(NopQueryTypeConfiguration<>)));
foreach (var typeConfiguration in typeConfigurations)
{
var configuration = (IMappingConfiguration)Activator.CreateInstance(typeConfiguration);
configuration.ApplyConfiguration(modelBuilder);
}
base.OnModelCreating(modelBuilder);
}
}
在构造函数里面,注入了数据库连接等信息,在OnModelCreating方法里面,通过反射类型NopEntityTypeConfiguration,找到上述所有的Map类,进行模型配置注册
至此,我们已经看到,整个ORM的实现原理,项目启动的时候,启动IOC注册,Nop.Web.Framework.Infrastructure.DependencyRegistrar:
builder.Register(context => new NopObjectContext(context.Resolve<DbContextOptions<NopObjectContext>>())).As<IDbContext>().InstancePerLifetimeScope();
总结一下:
Nop的仓储模型设计的很精巧,并且代码量非常少,如果想迁移到自己的项目中,只需要把上述相关的几个文件,拷贝到自己项目中,即可实现整个数据库访问层。