天天看点

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的仓储模型设计的很精巧,并且代码量非常少,如果想迁移到自己的项目中,只需要把上述相关的几个文件,拷贝到自己项目中,即可实现整个数据库访问层。