【开源】OSharp框架解说系列(5.2):EntityFramework数据层实现

〇、前言

  上篇 的数据层设计中,我们主要设计了数据对对外开放的 实体基类EntityBase<TKey>,单元操作接口IUnitOfWork 和 数据仓储接口IRepository<TEntity, TKey>,下面我们来解说怎样来使用 EntityFramework 对这些数据访问需求进行实现。EntityFramework 的实现中,我们不仅要实现以上设计的两个接口,还要做以下几件事:

  1. 设计一个与 业务实体解耦的 EntityFramework数据上下文类
  2. 设计 实体加载方案,将 业务实体 加载到上下文中
  3. 设计数据迁移方案,对 EntityFramework 的运行环境进行初始化
  4. 使用 已初始化的上下文 实现 数据仓储操作

  下面,我们将逐条进行解说。

一、目录

  〇、前言

  一、目录

  二、设计与业务解耦的上下文类

  三、解耦的业务实体的加载与数据迁移

    (一)解耦的业务实体的加载

    (二)数据初始化及迁移配置

  四、EntityFramework 初始化

  五、Repository 仓储操作实现

  六、开源说明

  系列导航

  代码分布图:

二、设计与业务解耦的上下文类

  EntityFramework 的封装,首要目标就是与业务层,业务实体解耦。怎样才算是解耦呢?如果不解耦,通常我们使用 EntityFramework 需要定义如下这样一个上下文类的:

 1 public class DemoDbContext : DbContext
 2 {
 3     public DbSet<Organization> Organizations { get; set; }
 4
 5     public DbSet<Role> Roles { get; set; }
 6
 7     public DbSet<User> Users { get; set; }
 8
 9     protected override void OnModelCreating(DbModelBuilder modelBuilder)
10     {
11         modelBuilder.Entity<Organization>().HasOptional(m => m.Parent).WithMany(n => n.Children);
12         modelBuilder.Entity<Role>().HasRequired(m => m.Organization).WithMany();
13         modelBuilder.Entity<User>().HasMany(m => m.Roles).WithMany(n => n.Users);
14     }
15 }

  在上面的上下文类中,业务实体与上下文类是紧耦合的,每次实体类或者实体之间的关系有变动,都要来修改这个上下文类DemoDbContext,又是无法扩展的,是典型的违反了“开闭原则”(软件系统对于组件功能的扩展是开放的,是允许对其进行功能扩展的;对于原有代码的修改是封闭的,即不应该修改原有的代码。)。所以,我们应该让 EntityFramework 的上下文与业务实体之间进行解耦,特别是对于 OSharp 这样作为开发框架存在的基础设施,解耦更是至关重要。

  从上面的示例上下文类中得知,实体类与上下文的耦合主要发生在两个部分:

  1. EntityFramework上下文类需拥有业务实体的 DbSet<TEntity> 类型的属性,这个属性即是业务实体加载到上下文的途径,也是进行数据操作时获取相应实体数据集DbSet<TEntity>的途径。
  2. 在上下文类的 OnModelCreating 方法中配置实体间的关系细节

  要进行解耦,也需要从这两方面着手。OSharp 的 EntityFramework 上下文将定义为如下:

  1     /// <summary>
  2     /// EntityFramework-CodeFirst数据上下文
  3     /// </summary>
  4     public class CodeFirstDbContext : DbContext, IUnitOfWork, IDependency
  5     {
  6         /// <summary>
  7         /// 初始化一个<see cref="CodeFirstDbContext"/>类型的新实例
  8         /// </summary>
  9         public CodeFirstDbContext()
 10             : this(GetConnectionStringName())
 11         { }
 12
 13         /// <summary>
 14         /// 使用连接名称或连接字符串 初始化一个<see cref="CodeFirstDbContext"/>类型的新实例
 15         /// </summary>
 16         public CodeFirstDbContext(string nameOrConnectionString)
 17             : base(nameOrConnectionString)
 18         { }
 19
 20         /// <summary>
 21         /// 获取或设置 是否开启事务提交
 22         /// </summary>
 23         public bool TransactionEnabled { get; set; }
 24
 25         /// <summary>
 26         /// 获取 数据库连接串名称
 27         /// </summary>
 28         /// <returns></returns>
 29         private static string GetConnectionStringName()
 30         {
 31             string name = ConfigurationManager.AppSettings.Get("OSharp-ConnectionStringName")
 32                 ?? ConfigurationManager.AppSettings.Get("ConnectionStringName") ?? "default";
 33             return name;
 34         }
 35
 36         /// <summary>
 37         /// 提交当前单元操作的更改。
 38         /// </summary>
 39         /// <returns>操作影响的行数</returns>
 40         public override int SaveChanges()
 41         {
 42             return SaveChanges(true);
 43         }
 44
 45         /// <summary>
 46         /// 提交当前单元操作的更改。
 47         /// </summary>
 48         /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param>
 49         /// <returns>操作影响的行数</returns>
 50         internal int SaveChanges(bool validateOnSaveEnabled)
 51         {
 52             bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;
 53             try
 54             {
 55                 Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;
 56                 int count = base.SaveChanges();
 57                 TransactionEnabled = false;
 58                 return count;
 59             }
 60             catch (DbUpdateException e)
 61             {
 62                 if (e.InnerException != null && e.InnerException.InnerException is SqlException)
 63                 {
 64                     SqlException sqlEx = e.InnerException.InnerException as SqlException;
 65                     string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number);
 66                     throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx);
 67                 }
 68                 throw;
 69             }
 70             finally
 71             {
 72                 if (isReturn)
 73                 {
 74                     Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;
 75                 }
 76             }
 77         }
 78 #if NET45
 79
 80         #region Overrides of DbContext
 81
 82         /// <summary>
 83         /// 异步提交当前单元操作的更改。
 84         /// </summary>
 85         /// <returns>操作影响的行数</returns>
 86         public override Task<int> SaveChangesAsync()
 87         {
 88             return SaveChangesAsync(true);
 89         }
 90
 91         #endregion
 92
 93         /// <summary>
 94         /// 提交当前单元操作的更改。
 95         /// </summary>
 96         /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param>
 97         /// <returns>操作影响的行数</returns>
 98         internal async Task<int> SaveChangesAsync(bool validateOnSaveEnabled)
 99         {
100             bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;
101             try
102             {
103                 Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;
104                 int count = await base.SaveChangesAsync();
105                 TransactionEnabled = false;
106                 return count;
107             }
108             catch (DbUpdateException e)
109             {
110                 if (e.InnerException != null && e.InnerException.InnerException is SqlException)
111                 {
112                     SqlException sqlEx = e.InnerException.InnerException as SqlException;
113                     string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number);
114                     throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx);
115                 }
116                 throw;
117             }
118             finally
119             {
120                 if (isReturn)
121                 {
122                     Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;
123                 }
124             }
125         }
126 #endif
127         protected override void OnModelCreating(DbModelBuilder modelBuilder)
128         {
129             //移除一对多的级联删除
130             modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
131         }
132     }

  OSharp 中定义的 EntityFramework 上下文类 CodeFirstDbContext 主要做了以下几件事:

  1. 上下文类 CodeFirstDbContext 实现了 IUnitOfWork 接口(与 架构系列 的那个上下文进行对比,OSharp 的 EntityFramework 的实现在架构上进行了简化)。
  2. 默认读取 Web.Config 中AppSettings中配置的名为“OSharp-ConnectionStringName”的键获取默认数据库连接串名称,进行默认上下文的实例化。
  3. 上下文在执行提交保存之后,重置 public bool TransactionEnabled { get; set; } 属性的值为 false。
  4. 在重写 OnModelCreating 方法时移除 一对多的级联删除(OneToManyCascadeDeleteConvention),防止“删除一个主干实体,子实体全部自动删除”的误删除悲剧发生。如果需要级联删除,再在实体的 Fluent API 配置中单独开启。
  5. 当需要 业务实体TEntity 的 DbSet<TEntity>数据集时,可以通过 DbContext 的 public virtual DbSet<TEntity> Set<TEntity>() where TEntity : class{} 方法来获取。

三、解耦的业务实体的加载与数据迁移

 (一)解耦的业务实体的加载

  上述 EntityFramework 上下文进行了与业务实体的解耦,那么 业务实体 是怎样加载到 上下文中的呢?下面我们就来解决这个问题。

  在 EntityFramework 6 的版本中,EntityFramework 提供了两个动态加载 业务实体类的途径:

  1. 加载单个实体类配置: public virtual ConfigurationRegistrar Add<TEntityType>(EntityTypeConfiguration<TEntityType> entityTypeConfiguration) where TEntityType : class{}
  2. 加载单个实体类复合配置: public virtual ConfigurationRegistrar Add<TComplexType>(ComplexTypeConfiguration<TComplexType> complexTypeConfiguration) where TComplexType : class
  3. 使用反射加载程序集中的单个实体类配置或实体类复合配置: public virtual ConfigurationRegistrar AddFromAssembly(Assembly assembly){} ,此API为 EntityFramework 6 新增的

  在 OSharp 的业务实体加载设计中,将使用前两个加载方式。

  首先,设计了一个专用于 业务实体加载到上下文 的接口:

 1  /// <summary>
 2  /// 实体映射接口
 3  /// </summary>
 4  public interface IEntityMapper
 5  {
 6      /// <summary>
 7      /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
 8      /// </summary>
 9      /// <param name="configurations">实体映射配置注册器</param>
10      void RegistTo(ConfigurationRegistrar configurations);
11  } 

  分别对简单实体与复合实体实现这个接口:

 1 /// <summary>
 2 /// 数据实体映射配置基类
 3 /// </summary>
 4 /// <typeparam name="TEntity">动态实体类型</typeparam>
 5 /// <typeparam name="TKey">动态主键类型</typeparam>
 6 public abstract class EntityConfigurationBase<TEntity, TKey> : EntityTypeConfiguration<TEntity>, IEntityMapper
 7     where TEntity : EntityBase<TKey>
 8 {
 9     /// <summary>
10     /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
11     /// </summary>
12     /// <param name="configurations">实体映射配置注册器</param>
13     public void RegistTo(ConfigurationRegistrar configurations)
14     {
15         configurations.Add(this);
16     }
17 }
18
19
20 /// <summary>
21 /// 复合数据实体映射配置基类
22 /// </summary>
23 /// <typeparam name="TComplexType">动态复合实体类型</typeparam>
24 /// <typeparam name="TKey">动态主键类型</typeparam>
25 public abstract class ComplexTypeConfigurationBase<TComplexType, TKey> : ComplexTypeConfiguration<TComplexType>, IEntityMapper
26     where TComplexType : EntityBase<TKey>
27 {
28     #region Implementation of IEntityMapper
29
30     /// <summary>
31     /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中
32     /// </summary>
33     /// <param name="configurations">实体映射配置注册器</param>
34     public void RegistTo(ConfigurationRegistrar configurations)
35     {
36         configurations.Add(this);
37     }
38
39     #endregion
40 }

  每个业务实体类都要实现一个映射配置,在配置中可以通过 FluentAPI 进行实体与数据库的映射细节配置(如表名,外键关系,索引等),例如:

1 public class OrganizationConfiguration : EntityConfigurationBase<Organization, int>
2 {
3     public OrganizationConfiguration()
4     {
5         HasOptional(m => m.Parent).WithMany(n => n.Children);
6     }
7 }

  关于实体与数据库的映射,通常有两种方式:DataAnnotation 与 FluentAPI,那么,什么时候使用什么方式呢?为了实保持实体类的通用性,减少第三方依赖,通常遵从如下原则:

  • 当映射特性与 EntityFramework 无关时,使用 DataAnnotation,例如 Required、StringLength 等。
  • 当映射特性与 EntityFramework 耦合时,使用 FluentAPI,例如 ToTable、HasForeignKey、HasMany 等。

  在进行 EntityFramework 的初始化的时候,需要将所有实现了 IEntityMapper 接口的所有实体映射配置类的实例都初始化出来,存储在 DatabaseInitializer.EntityMappers 属性中,然后在初始化 EntityFramework 上下文的时候,将这些 映射配置的实例 加载到  modelBuilder.Configurations 中,即可完成 EntityFramework 上下文对 业务实体 的加载及映射。  

 1 protected override void OnModelCreating(DbModelBuilder modelBuilder)
 2 {
 3     //移除一对多的级联删除
 4     modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
 5
 6     //注册实体配置信息
 7     ICollection<IEntityMapper> entityMappers = DatabaseInitializer.EntityMappers;
 8     foreach (IEntityMapper mapper in entityMappers)
 9     {
10         mapper.RegistTo(modelBuilder.Configurations);
11     }
12 }

 (二)数据初始化及迁移配置

  种子数据初始化

  在项目开发过程中,为了项目运行和测试的需要,往往需要在数据库创建的时候,向数据库中添加初始数据(种子数据),EntityFramework 创建数据库时是支持种子数据的初始化的。

  在OSharp中,定义了一个专用于种子数据初始化的接口:

 1  /// <summary>
 2  /// 初始化种子数据接口
 3  /// </summary>
 4  public interface ISeedAction
 5  {
 6      /// <summary>
 7      /// 获取 操作排序,数值越小越先执行
 8      /// </summary>
 9      int Order { get; }
10
11      /// <summary>
12      /// 定义种子数据初始化过程
13      /// </summary>
14      /// <param name="context">数据上下文</param>
15      void Action(DbContext context);
16  }

  在各个模块中,如果需要进行种子数据的初始化,需要实现这个接口,并在 Action 方法中使用上下文参数 context 实现数据的初始化:

 1 public class IdentitySeedAction : ISeedAction
 2 {
 3     /// <summary>
 4     /// 获取 操作排序,数值越小越先执行
 5     /// </summary>
 6     public int Order { get { return 0; } }
 7
 8     /// <summary>
 9     /// 定义种子数据初始化过程
10     /// </summary>
11     /// <param name="context">数据上下文</param>
12     public void Action(DbContext context)
13     {
14         List<Organization> organizations = new List<Organization>()
15         {
16             new Organization(){Name = "系统", Remark = "系统根节点", },
17         };
18         context.Set<Organization>().AddOrUpdate(m => new { m.Name }, organizations.ToArray());
19     }
20 }

  当数据库不存在时,可以使用 EntityFramework 中定义的 CreateDatabaseIfNotExists<TDbContext> 来创建数据库。OSharp 中从这个类派生了一个支持种子数据初始化的类型,来处理在创建数据库的时候初始化种子数据:

 1 /// <summary>
 2 /// 在数据库不存在时使用种子数据创建数据库
 3 /// </summary>
 4 public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext>
 5 {
 6     static CreateDatabaseIfNotExistsWithSeed()
 7     {
 8         SeedActions = new List<ISeedAction>();
 9     }
10
11     /// <summary>
12     /// 获取 数据库创建时的种子数据操作信息集合,各个模块可以添加自己的初始化数据
13     /// </summary>
14     public static ICollection<ISeedAction> SeedActions { get; private set; }
15
16     protected override void Seed(CodeFirstDbContext context)
17     {
18         IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order);
19         foreach (ISeedAction seedAction in seedActions)
20         {
21             seedAction.Action(context);
22         }
23     }
24 }

  在 EntityFramework 初始化的时候,只需要将 ISeedAction 接口的派生类的实例添加到 SeedActions 属性中即可: CreateDatabaseIfNotExistsWithSeed.SeedActions.Add(new IdentitySeedAction());

  数据迁移配置

  OSharp 在数据迁移方面的考虑很简单,默认开启了 自动迁移 的迁移方式。自动迁移的启用主要涉及两个属性:

  1. AutomaticMigrationsEnabled = true;获取或设置指示迁移数据库时是否可使用自动迁移的值。
  2. AutomaticMigrationDataLossAllowed = true;获取或设置指示是否可接受自动迁移期间的数据丢失的值。 如果设置为 false,则将在数据丢失可能作为自动迁移一部分出现时引发异常。

  迁移配置类中同样定义了种子数据的初始化,需要注意的是:这里的种子数据与 CreateDatabaseIfNotExistsWithSeed 类中的有个不太一样的地方, CreateDatabaseIfNotExistsWithSeed 类中的种子数据,只在创建数据库的时候运行一次,而这里的,将会在每次进行迁移的时候,都会对种子数据进行还原。

 1 /// <summary>
 2 /// 在数据库不存在时使用种子数据创建数据库
 3 /// </summary>
 4 public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext>
 5 {
 6     static CreateDatabaseIfNotExistsWithSeed()
 7     {
 8         SeedActions = new List<ISeedAction>();
 9     }
10
11     /// <summary>
12     /// 获取 数据库创建时的种子数据操作信息集合,各个模块可以添加自己的初始化数据
13     /// </summary>
14     public static ICollection<ISeedAction> SeedActions { get; private set; }
15
16     protected override void Seed(CodeFirstDbContext context)
17     {
18         IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order);
19         foreach (ISeedAction seedAction in seedActions)
20         {
21             seedAction.Action(context);
22         }
23     }
24 }

四、EntityFramework 初始化

  OSharp 中的 EntityFramework 数据存储运行环境的初始化,主要包含如下几个方面:

  1. 将 业务实体 的 EntityTypeConfiguration<TEntity> 实体映射类所在程序集加载到数据层中,并获取所有 IEntityMapper 接口的派生类的实例以备创建数据上下文 CodeFirstDbContext 实例时使用
  2. 将所有 ISeedAction 接口派生的种子数据初始化类的实例加载到相应的EntityFramework初始化策略中
  3. 设置 EntityFramework 初始化策略,当数据库不存在时,使用 CreateDatabaseIfNotExistsWithSeed 进行初始化;当数据库存在时,使用 MigrateDatabaseToLatestVersion 数据迁移策略
  4. EntityFramework 数据库架构的预热

  初始化的一个示例如下,在主程序入口中(例如Global的Application_Start):

1 private static void DatabaseInitialize()
2 {
3     Assembly assembly = Assembly.GetExecutingAssembly();
4     DatabaseInitializer.AddMapperAssembly(assembly);
5     CreateDatabaseIfNotExistsWithSeed.SeedActions.Add(new IdentitySeedAction());
6
7     DatabaseInitializer.Initialize();
8 }

  初始化类 DatabaseInitializer 实现如下:

 1 /// <summary>
 2 /// 数据库初始化操作类
 3 /// </summary>
 4 public class DatabaseInitializer
 5 {
 6     private static readonly ICollection<Assembly> MapperAssemblies = new List<Assembly>();
 7
 8     /// <summary>
 9     /// 获取 数据实体映射配置信息集合
10     /// </summary>
11     public static ICollection<IEntityMapper> EntityMappers { get { return GetAllEntityMapper(); } }
12
13     /// <summary>
14     /// 设置数据库初始化,策略为自动迁移到最新版本
15     /// </summary>
16     public static void Initialize()
17     {
18         CodeFirstDbContext context = new CodeFirstDbContext();
19         IDatabaseInitializer<CodeFirstDbContext> initializer;
20         if (!context.Database.Exists())
21         {
22             initializer = new CreateDatabaseIfNotExistsWithSeed();
23         }
24         else
25         {
26             initializer = new MigrateDatabaseToLatestVersion<CodeFirstDbContext, MigrationsConfiguration>();
27         }
28         Database.SetInitializer(initializer);
29
30         //EF预热
31         ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
32         StorageMappingItemCollection mappingItemCollection = (StorageMappingItemCollection)objectContext.ObjectStateManager
33             .MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
34         mappingItemCollection.GenerateViews(new List<EdmSchemaError>());
35         context.Dispose();
36     }
37
38     /// <summary>
39     /// 添加需要搜索实体映射的程序集到检索集合中
40     /// </summary>
41     public static void AddMapperAssembly(Assembly assembly)
42     {
43         assembly.CheckNotNull("assembly");
44         if (MapperAssemblies.Any(m => m == assembly))
45         {
46             return;
47         }
48         MapperAssemblies.Add(assembly);
49     }
50
51     private static ICollection<IEntityMapper> GetAllEntityMapper()
52     {
53         Type baseType = typeof(IEntityMapper);
54         Type[] mapperTypes = MapperAssemblies.SelectMany(assembly => assembly.GetTypes())
55             .Where(type => baseType.IsAssignableFrom(type) && type != baseType && !type.IsAbstract).ToArray();
56         ICollection<IEntityMapper> result = mapperTypes.Select(type => Activator.CreateInstance(type) as IEntityMapper).ToList();
57         return result;
58     }
59 }

  同时,数据层的核心API  IUnitOfWork , IRepository<TEntity, TKey> 的实例化,是将由 Autofac 的 IoC 组件来完成的,需要在 IoC 初始化时进行注册:

builder.RegisterGeneric(typeof(Repository<,>)).As(typeof(IRepository<,>));

五、Repository 仓储操作实现

  Repository 的实现类需要一个 IUnitOfWOrk 参数,这个参数是由 IoC 组件进行注入的,并给 UnitOfWork 与 Entities 属性进行赋值。

 1 /// <summary>
 2 /// EntityFramework的仓储实现
 3 /// </summary>
 4 /// <typeparam name="TEntity">实体类型</typeparam>
 5 /// <typeparam name="TKey">主键类型</typeparam>
 6 public class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : EntityBase<TKey>
 7 {
 8     private readonly DbSet<TEntity> _dbSet;
 9     private readonly IUnitOfWork _unitOfWork;
10
11     public Repository(IUnitOfWork unitOfWork)
12     {
13         _unitOfWork = unitOfWork;
14         _dbSet = ((DbContext)unitOfWork).Set<TEntity>();
15     }
16
17     /// <summary>
18     /// 获取 当前单元操作对象
19     /// </summary>
20     public IUnitOfWork UnitOfWork { get { return _unitOfWork; } }
21
22     /// <summary>
23     /// 获取 当前实体类型的查询数据集
24     /// </summary>
25     public IQueryable<TEntity> Entities { get { return _dbSet; } }
26
27     ...
28
29 }

  在操作后台进行提交保存的时候,将根据 UnitOfWork.TransactionEnabled 的值决定是否执行保存。

 1         private int SaveChanges()
 2         {
 3             return _unitOfWork.TransactionEnabled ? 0 : _unitOfWork.SaveChanges();
 4         }
 5
 6 #if NET45
 7
 8         private async Task<int> SaveChangesAsync()
 9         {
10             return _unitOfWork.TransactionEnabled ? 0 : await _unitOfWork.SaveChangesAsync();
11         }
12
13 #endif

其他 API 的实现,下面将分类进行解说:

1.普通 增、改、删 业务操作API

   普通业务操作API主要是对单个或多个实体进行的单个或批量操作API:

 1 /// <summary>
 2 /// 插入实体
 3 /// </summary>
 4 /// <param name="entity">实体对象</param>
 5 /// <returns>操作影响的行数</returns>
 6 public int Insert(TEntity entity)
 7 {
 8     entity.CheckNotNull("entity");
 9     _dbSet.Add(entity);
10     return SaveChanges();
11 }
12
13 /// <summary>
14 /// 批量插入实体
15 /// </summary>
16 /// <param name="entities">实体对象集合</param>
17 /// <returns>操作影响的行数</returns>
18 public int Insert(IEnumerable<TEntity> entities)
19 {
20     entities = entities as TEntity[] ?? entities.ToArray();
21     _dbSet.AddRange(entities);
22     return SaveChanges();
23 }
24
25 /// <summary>
26 /// 更新实体对象
27 /// </summary>
28 /// <param name="entity">更新后的实体对象</param>
29 /// <returns>操作影响的行数</returns>
30 public int Update(TEntity entity)
31 {
32     entity.CheckNotNull("entity");
33     ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity);
34     return SaveChanges();
35 }
36
37 /// <summary>
38 /// 删除实体
39 /// </summary>
40 /// <param name="entity">实体对象</param>
41 /// <returns>操作影响的行数</returns>
42 public int Delete(TEntity entity)
43 {
44     entity.CheckNotNull("entity");
45     _dbSet.Remove(entity);
46     return SaveChanges();
47 }
48
49 public virtual int Delete(TKey key)
50 {
51     CheckEntityKey(key, "key");
52     TEntity entity = _dbSet.Find(key);
53     return entity == null ? 0 : Delete(entity);
54 }
55
56 /// <summary>
57 /// 删除所有符合特定条件的实体
58 /// </summary>
59 /// <param name="predicate">查询条件谓语表达式</param>
60 /// <returns>操作影响的行数</returns>
61 public int Delete(Expression<Func<TEntity, bool>> predicate)
62 {
63     predicate.CheckNotNull("predicate");
64     TEntity[] entities = _dbSet.Where(predicate).ToArray();
65     return entities.Length == 0 ? 0 : Delete(entities);
66 }
67
68 /// <summary>
69 /// 批量删除删除实体
70 /// </summary>
71 /// <param name="entities">实体对象集合</param>
72 /// <returns>操作影响的行数</returns>
73 public int Delete(IEnumerable<TEntity> entities)
74 {
75     entities = entities as TEntity[] ?? entities.ToArray();
76     _dbSet.RemoveRange(entities);
77     return SaveChanges();
78 }

  上面的 Update 更新代码,是通过一个扩展方法来完成的,这个扩展方法基本能解决在 EntityFramework 数据更新中会遇到的问题(详见架构系列的《数据更新》篇)

 1 /// <summary>
 2 /// 更新上下文中指定的实体的状态
 3 /// </summary>
 4 /// <typeparam name="TEntity">实体类型</typeparam>
 5 /// <typeparam name="TKey">主键类型</typeparam>
 6 /// <param name="dbContext">上下文对象</param>
 7 /// <param name="entities">要更新的实体类型</param>
 8 public static void Update<TEntity, TKey>(this DbContext dbContext, params TEntity[] entities) where TEntity : EntityBase<TKey>
 9 {
10     dbContext.CheckNotNull("dbContext");
11     entities.CheckNotNull("entities");
12
13     foreach (TEntity entity in entities)
14     {
15         DbSet<TEntity> dbSet = dbContext.Set<TEntity>();
16         try
17         {
18             DbEntityEntry<TEntity> entry = dbContext.Entry(entity);
19             if (entry.State == EntityState.Detached)
20             {
21                 dbSet.Attach(entity);
22                 entry.State = EntityState.Modified;
23             }
24         }
25         catch (InvalidOperationException)
26         {
27             TEntity oldEntity = dbSet.Find(entity.Id);
28             dbContext.Entry(oldEntity).CurrentValues.SetValues(entity);
29         }
30     }
31 }

2.针对 DTO 的 增、改、删 业务操作API

  在业务层实现对实体的增加,更新操作的时候,如果业务层接收的是 Dto 数据,需要对 Dto 的数据进行合法性检查,再将 Dto 通过 数据映射组件 AutoMapper 创建或更新相应类型的实体数据模型 Model,然后再按需求对 Model 的导航属性进行更新,再提交保存。在进行删除操作的时候,需要使用传入的主键 Id 检索相应的实体信息,并检查删除操作的可行性,再提交到上下文中进行删除操作,并删除其他相关数据。在这些针对实体的业务操作中,存在着很多相似的重复代码,这种重复代码的存在,会极大降低系统的可维护性。因此,在 数据仓储操作 中设计了一组专门针对 Dto 的业务操作API,利用 无返回委托 Action<T> 与 有返回委托 Func<T, RT> 来向底层传递 各实体业务操作的变化点的业务逻辑,以达到对 Dto 业务重复代码的彻底重构。

  1 /// <summary>
  2 /// 以DTO为载体批量插入实体
  3 /// </summary>
  4 /// <typeparam name="TAddDto">添加DTO类型</typeparam>
  5 /// <param name="dtos">添加DTO信息集合</param>
  6 /// <param name="checkAction">添加信息合法性检查委托</param>
  7 /// <param name="updateFunc">由DTO到实体的转换委托</param>
  8 /// <returns>业务操作结果</returns>
  9 public OperationResult Insert<TAddDto>(ICollection<TAddDto> dtos, Action<TAddDto> checkAction = null, Func<TAddDto, TEntity, TEntity> updateFunc = null)
 10     where TAddDto : IAddDto
 11 {
 12     dtos.CheckNotNull("dtos");
 13     List<string> names = new List<string>();
 14     foreach (var dto in dtos)
 15     {
 16         TEntity entity = Mapper.Map<TEntity>(dto);
 17         try
 18         {
 19             if (checkAction != null)
 20             {
 21                 checkAction(dto);
 22             }
 23             if (updateFunc != null)
 24             {
 25                 entity = updateFunc(dto, entity);
 26             }
 27         }
 28         catch (Exception e)
 29         {
 30             return new OperationResult(OperationResultType.Error, e.Message);
 31         }
 32         _dbSet.Add(entity);
 33         string name = GetNameValue(dto);
 34         if (name != null)
 35         {
 36             names.Add(name);
 37         }
 38     }
 39     int count = SaveChanges();
 40     return count > 0
 41         ? new OperationResult(OperationResultType.Success,
 42             names.Count > 0
 43                 ? "信息“{0}”添加成功".FormatWith(names.ExpandAndToString())
 44                 : "{0}个信息添加成功".FormatWith(dtos.Count))
 45         : new OperationResult(OperationResultType.NoChanged);
 46 }
 47
 48 /// <summary>
 49 /// 以DTO为载体批量更新实体
 50 /// </summary>
 51 /// <typeparam name="TEditDto">更新DTO类型</typeparam>
 52 /// <param name="dtos">更新DTO信息集合</param>
 53 /// <param name="checkAction">更新信息合法性检查委托</param>
 54 /// <param name="updateFunc">由DTO到实体的转换委托</param>
 55 /// <returns>业务操作结果</returns>
 56 public OperationResult Update<TEditDto>(ICollection<TEditDto> dtos, Action<TEditDto> checkAction = null, Func<TEditDto, TEntity, TEntity> updateFunc = null)
 57     where TEditDto : IEditDto<TKey>
 58 {
 59     dtos.CheckNotNull("dtos" );
 60     List<string> names = new List<string>();
 61     foreach (var dto in dtos)
 62     {
 63         TEntity entity = _dbSet.Find(dto.Id);
 64         if (entity == null)
 65         {
 66             return new OperationResult(OperationResultType.QueryNull);
 67         }
 68         entity = Mapper.Map(dto, entity);
 69         try
 70         {
 71             if (checkAction != null)
 72             {
 73                 checkAction(dto);
 74             }
 75             if (updateFunc != null)
 76             {
 77                 entity = updateFunc(dto, entity);
 78             }
 79         }
 80         catch (Exception e)
 81         {
 82             return new OperationResult(OperationResultType.Error, e.Message);
 83         }
 84         ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity);
 85         string name = GetNameValue(dto);
 86         if (name != null)
 87         {
 88             names.Add(name);
 89         }
 90     }
 91     int count = SaveChanges();
 92     return count > 0
 93         ? new OperationResult(OperationResultType.Success,
 94             names.Count > 0
 95                 ? "信息“{0}”更新成功".FormatWith(names.ExpandAndToString())
 96                 : "{0}个信息更新成功".FormatWith(dtos.Count))
 97         : new OperationResult(OperationResultType.NoChanged);
 98 }
 99
100 /// <summary>
101 /// 以标识集合批量删除实体
102 /// </summary>
103 /// <param name="ids">标识集合</param>
104 /// <param name="checkAction">删除前置检查委托</param>
105 /// <param name="deleteFunc">删除委托,用于删除关联信息</param>
106 /// <returns>业务操作结果</returns>
107 public OperationResult Delete(ICollection<TKey> ids, Action<TEntity> checkAction = null, Func<TEntity, TEntity> deleteFunc = null)
108 {
109     ids.CheckNotNull("ids" );
110     List<string> names = new List<string>();
111     foreach (var id in ids)
112     {
113         TEntity entity = _dbSet.Find(id);
114         try
115         {
116             if (checkAction != null)
117             {
118                 checkAction(entity);
119             }
120             if (deleteFunc != null)
121             {
122                 entity = deleteFunc(entity);
123             }
124         }
125         catch (Exception e)
126         {
127             return new OperationResult(OperationResultType.Error, e.Message);
128         }
129         _dbSet.Remove(entity);
130         string name = GetNameValue(entity);
131         if (name != null)
132         {
133             names.Add(name);
134         }
135     }
136     int count = SaveChanges();
137     return count > 0
138         ? new OperationResult(OperationResultType.Success,
139             names.Count > 0
140                 ? "信息“{0}”删除成功".FormatWith(names.ExpandAndToString())
141                 : "{0}个信息删除成功".FormatWith(ids.Count))
142         : new OperationResult(OperationResultType.NoChanged);
143 }

上面的代码,使用了委托 Action , Func<T> 来封装业务逻辑中的变化点(比如实体合法性检查,给实体的导航属性赋值等),而将公共代码提取出来下沉到底层中,这是一个很好的封装思路。

  使用上面的封装,我们在业务实现时只需要编号非常核心的几行代码,即可完成一个业务的操作,例如:

  1 /// <summary>
  2 /// 添加组织机构信息信息
  3 /// </summary>
  4 /// <param name="dtos">要添加的组织机构信息DTO信息</param>
  5 /// <returns>业务操作结果</returns>
  6 public OperationResult AddOrganizations(params OrganizationDto[] dtos)
  7 {
  8     dtos.CheckNotNull("dtos");
  9     List<Organization> organizations = new List<Organization>();
 10     OperationResult result = _organizationRepository.Insert(dtos,
 11         dto =>
 12         {
 13             if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name))
 14             {
 15                 throw new Exception("组织机构名称“{0}”已存在,不能重复添加。".FormatWith(dto.Name));
 16             }
 17         },
 18         (dto, entity) =>
 19         {
 20             if (dto.ParentId.HasValue && dto.ParentId.Value > 0)
 21             {
 22                 Organization parent = _organizationRepository.GetByKey(dto.ParentId.Value);
 23                 if (parent == null)
 24                 {
 25                     throw new Exception("指定父组织机构不存在。");
 26                 }
 27                 entity.Parent = parent;
 28             }
 29             organizations.Add(entity);
 30             return entity;
 31         });
 32     if (result.ResultType == OperationResultType.Success)
 33     {
 34         int[] ids = organizations.Select(m => m.Id).ToArray();
 35         RefreshOrganizationsTreePath(ids);
 36     }
 37     return result;
 38 }
 39
 40 /// <summary>
 41 /// 更新组织机构信息信息
 42 /// </summary>
 43 /// <param name="dtos">包含更新信息的组织机构信息DTO信息</param>
 44 /// <returns>业务操作结果</returns>
 45 public OperationResult EditOrganizations(params OrganizationDto[] dtos)
 46 {
 47     dtos.CheckNotNull("dtos");
 48     List<Organization> organizations = new List<Organization>();
 49     OperationResult result = _organizationRepository.Update(dtos,
 50         dto =>
 51         {
 52             if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name, dto.Id))
 53             {
 54                 throw new Exception("组织机构名称“{0}”已存在,不能重复添加。".FormatWith(dto.Name));
 55             }
 56         },
 57         (dto, entity) =>
 58         {
 59             if (!dto.ParentId.HasValue || dto.ParentId == 0)
 60             {
 61                 entity.Parent = null;
 62             }
 63             else if (entity.Parent != null && entity.Parent.Id != dto.ParentId)
 64             {
 65                 Organization parent = _organizationRepository.GetByKey(dto.Id);
 66                 if (parent == null)
 67                 {
 68                     throw new Exception("指定父组织机构不存在。");
 69                 }
 70                 entity.Parent = parent;
 71             }
 72             organizations.Add(entity);
 73             return entity;
 74         });
 75     if (result.ResultType == OperationResultType.Success)
 76     {
 77         int[] ids = organizations.Select(m => m.Id).ToArray();
 78         RefreshOrganizationsTreePath(ids);
 79     }
 80     return result;
 81 }
 82
 83 /// <summary>
 84 /// 删除组织机构信息信息
 85 /// </summary>
 86 /// <param name="ids">要删除的组织机构信息编号</param>
 87 /// <returns>业务操作结果</returns>
 88 public OperationResult DeleteOrganizations(params int[] ids)
 89 {
 90     ids.CheckNotNull("ids");
 91     OperationResult result = _organizationRepository.Delete(ids,
 92         entity =>
 93         {
 94             if (entity.Children.Any())
 95             {
 96                 throw new Exception("组织机构“{0}”的子级不为空,不能删除。".FormatWith(entity.Name));
 97             }
 98         });
 99     return result;
100 }

  如上面的代码所示,只需要分别去实现 Action 与 Func,进行最核心的业务代码实现,而其他的事,底层已经完全做了,是不是很简洁

3.数据查询 API

  通过IQueryable<T>查询数据源,能满足大部分数据查询的需求,但某些 EntityFramework 的特定查询需求,还是应该单独定义 数据查询API,以更好的保障不丢失 EntityFramework 的数据查询自由度。在这里主要定义了 通过主键查找实体、使用 Include 包含指定导航属性 的数据查询API:

 1 /// <summary>
 2 /// 实体存在性检查
 3 /// </summary>
 4 /// <param name="predicate">查询条件谓语表达式</param>
 5 /// <param name="id">编辑的实体标识</param>
 6 /// <returns>是否存在</returns>
 7 public bool ExistsCheck(Expression<Func<TEntity, bool>> predicate, TKey id = default(TKey))
 8 {
 9     TKey defaultId = default(TKey);
10     var entity = _dbSet.Where(predicate).Select(m => new { m.Id }).SingleOrDefault();
11     bool exists = id.Equals(defaultId) ? entity != null : entity != null && entity.Id.Equals(defaultId);
12     return exists;
13 }
14
15 /// <summary>
16 /// 查找指定主键的实体
17 /// </summary>
18 /// <param name="key">实体主键</param>
19 /// <returns>符合主键的实体,不存在时返回null</returns>
20 public TEntity GetByKey(TKey key)
21 {
22     CheckEntityKey(key, "key");
23     return _dbSet.Find(key);
24 }
25
26 /// <summary>
27 /// 获取贪婪加载导航属性的查询数据集
28 /// </summary>
29 /// <param name="path">属性表达式,表示要贪婪加载的导航属性</param>
30 /// <returns>查询数据集</returns>
31 public IQueryable<TEntity> GetInclude<TProperty>(Expression<Func<TEntity, TProperty>> path)
32 {
33     path.CheckNotNull("path");
34     return _dbSet.Include(path);
35 }
36
37 /// <summary>
38 /// 获取贪婪加载多个导航属性的查询数据集
39 /// </summary>
40 /// <param name="paths">要贪婪加载的导航属性名称数组</param>
41 /// <returns>查询数据集</returns>
42 public IQueryable<TEntity> GetIncludes(params string[] paths)
43 {
44     paths.CheckNotNull("paths");
45     IQueryable<TEntity> source = _dbSet;
46     foreach (var path in paths)
47     {
48         source = source.Include(path);
49     }
50     return source;
51 }

  至此,EntityFramework 数据层的搭建已基本完成,有了数据层,就可以着手进行 实际业务 上的工作了。

六、开源说明

 (一)github.com

  OSharp项目已在github.com上开源,地址为:https://github.com/i66soft/osharp,欢迎阅读代码,欢迎 Fork,如果您认同 OSharp 项目的思想,欢迎参与 OSharp 项目的开发。

  在Visual Studio 2013中,可直接获取 OSharp 的最新源代码,获取方式如下,地址为:https://github.com/i66soft/osharp.git

  

 (二)nuget

  OSharp的相关类库已经发布到nuget上,欢迎试用,直接在nuget上搜索 “osharp” 关键字即可找到
  

系列导航

  1. 【开源】OSharp框架解说系列(1):总体设计
  2. 【开源】OSharp框架解说系列(2.1):EasyUI的后台界面搭建及极致重构
  3. 【开源】OSharp框架解说系列(2.2):EasyUI复杂布局及数据操作
  4. 【开源】OSharp框架解说系列(3):扩展方法
  5. 【开源】OSharp框架解说系列(4):架构分层及IoC
  6. 【开源】OSharp框架解说系列(5.1):EntityFramework数据层设计
  7. 【开源】OSharp框架解说系列(5.2):EntityFramework数据层实现
时间: 2024-11-08 01:32:48

【开源】OSharp框架解说系列(5.2):EntityFramework数据层实现的相关文章

【开源】OSharp框架解说系列(1):总体设计

〇.前言 哈,距离前一个系列<MVC实用构架设计>的烂尾篇(2013年9月1日)已经跨了两个年头了,今天是2015年1月9日,日期已经相映,让我们开启新的航程吧. 前一个系列讲的主要是我对架构设计的理解以及怎样用好EntityFramework的一些想法,在技术细节上并没有太多的考究.不幸的是,不少同学把这个架构当作框架来用了,里边留的很多坑,坑苦了很多人,真是误人子弟,深表愧疚.于是重新整理代码,整理思路,鼓捣出了这个我们将要详解的开源框架:OSharp.这次,我们真的深入地说框架了,而不是

【开源】OSharp框架解说系列(3):扩展方法

〇.前言 扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型. 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用. 对于用 C# 和 Visual Basic 编写的客户端代码,调用扩展方法与调用在类型中实际定义的方法之间没有明显的差异. 最常见的扩展方法是 LINQ 标准查询运算符,它将查询功能添加到现有的 System.Collections.IEnumerable 和 System.Collections.Generic.

【开源】OSharp框架解说系列(4):架构分层及IoC

〇.前言 前面构造了一个后台管理的界面布局,下面开始讲解整个项目的分层设计. 关于分层,网上已经存在相当多的讨论了,这也是一个程序员初学架构设计最先会碰到的问题. 该不该分层? 怎样分层? 层与层之间是否需要解耦?是否需要设计接口?接口是否是多余的? 看完OSharp的分层设计,我想,你应该多少能得到一些启示. 注:OSharp 开发框架的前身是<MVC实体架构设计>系列中讲到的那个架构示例,所以有很多知识点那个系列讲到了,就不会在这个系列再重复了,如果有什么觉得不太明白的可以参考<MV

【开源】OSharp框架解说系列(6.1):日志系统设计

〇.前言 日志记录对于一个系统而言,重要性不言而喻.日志记录功能在系统开发阶段,往往容易被忽略.因为开发阶段,程序可以调试,可以反复的运行以查找问题.但在系统进入正常的运行维护阶段,特别是在进行审计统计的时候,追踪问题的时候,在追溯责任的时候,在系统出错的时候等等场景中,日志记录才会显示出它不可替代的作用.记录的日志,平时看似累赘,只有在需要的时候,才会拍大腿后悔当初为什么不把日志记录得详细些. 日志系统,是一个非常基础的系统,但由于需求的复杂性,各个场景需要的日志分类,来源,输出方式各有不同,

【开源】OSharp框架解说系列(2.1):EasyUI的后台界面搭建及极致重构

〇.前言 要了解一个东西长什么样,至少得让我们能看到,才能提出针对性的见解.所以,为了言之有物,而不是凭空漫谈,我们先从UI说起,后台管理页面的UI我们将使用应用比较普遍的easyui框架. 以前在用easyui的时候,每个页面都得从0做起,或者不厌其烦地由以前的页面通过“复制-粘贴”的方式来修改,久页久之,就会造成页面庞大且难以维护.其实,前端的html,javascript代码与后端的代码是一样的,通过一定的组织,把重复的代码抽离出来,同样也通过达到很好的复用率.而MVC的天生的Layout

【开源】OSharp框架解说系列(2.2):EasyUI复杂布局及数据操作

一.目录 一.目录 二.EasyUI复杂布局 三.EasyUI动态工具栏 四.EasyUI增删改操作 五.开源说明 系列导航 二.EasyUI复杂布局 接上篇,前面我们已经定义了一个 datagrid父视图 _DataGridLayout.cshtml,实现一个表格是相当的容易.但是,实际业务中,并非所有的数据列表并非只是单一的datagrid列表,还可能需要把datagrid与其他组件配合使用,比如角色信息是来源于各个组织机构的,就需要增加一个组织机构的分类,来更好的管理各种角色.最终效果图如

OSharp3.0框架解说系列:新版本说明及新功能规划预览

前言 时间过得真快,小半年又过去了. OSharp在github.com开源已经半年了,半年时间里,我们发现开源并没有给OSharp带来什么发展,关注的人不多,提交Bug的人更少,至于愿意参与到项目中来,给OSharp提交代码的人,0. 大环境如此,我也没什么可说的. 一个人的开源,开的不是源,是寂寞. 为了OSharp项目能继续发展下去,也为了团队的积极性(大家都懂的,如果只有你一个人在贡献,别人都只索取,你的热情坚持不了多久的),我们做了一个决定…… OSharp3.0不再开源 从OShar

OSharp3.0框架解说系列(6.2):操作日志与数据日志

前言 在<[开源]OSharp框架解说系列(6.1):日志系统设计>中,我们已经设计并实现了一个可扩展的日志系统,只要定义好输出端的Adapter,就可以以任意形式输出日志信息. 在日志开发中,有些日志记录需求是常规需要的,比如操作日志,数据变更日志,系统异常日志等,我们希望把这些常规需求都集成到OSharp框架当中.有了内置的支持,在做开发的时候,只需要很简单的配置,就可以实现相关需求. 关于三类日志,这里先简要描述一下: 操作日志:粗略描述系统用户(如管理员.业务人员.会员等)对系统的业务

【开源】OSharp3.3框架解说系列:开发计划与进度

OSharp是什么? OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现.与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现.依赖注入.ORM.对象映射.日志.缓存等等功能,都只定义了一套最基础最通用的抽象封装,提供了一套统一的API.约定与规则,并定义了部分执行流程,主要是让项目在一定的规范下进行开发.所有的功能实现端,都是通过现有的成熟的第三方组件来实现的,除了EntityFramework之外,所有的第三方实现都可以轻