概述
關于資料通路這一塊,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的倉儲模型設計的很精巧,并且代碼量非常少,如果想遷移到自己的項目中,隻需要把上述相關的幾個檔案,拷貝到自己項目中,即可實作整個資料庫通路層。