天天看點

NopCommerce倉儲模型解析

概述

關于資料通路這一塊,C#/.Net目前有很多ORM架構可以使用,比如EntityFramework、Dapper、NHibernate等,而NopCommerce采用了微軟的EntityFramework。

閱讀本文需要相關的一些知識:IOC、Autofac、EntityFramework、泛型等,并且對NopCommerce有少許了解

NopCommerce代碼位址:https://github.com/nopSolutions/nopCommerce

EF通路資料,有幾個必要條件,我們這裡先列舉一下:

  1. 實體模型:Nop.Core.Domain目錄下
  2. 資料庫和實體類Mapping關系,路徑:Nop.Data.Mapping檔案目錄下
  3. Context類,通路資料庫的上下文或者叫會話:Nop.Data.NopObjectContext
  4. 資料倉儲管理類: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的倉儲模型設計的很精巧,并且代碼量非常少,如果想遷移到自己的項目中,隻需要把上述相關的幾個檔案,拷貝到自己項目中,即可實作整個資料庫通路層。