Entity Framework Code Firstでのデータアクセスレイヤーの実装





ご挨拶!



このトピックでは、Entity Frameworkに関連するデータアクセスレベル、次にEF、タスクの内容と解決方法について説明します。 投稿から送信されたすべてのコード、および添付のデモプロジェクトは 、MITリベラルライセンスの下公開されます。つまり、好きなようにコードを使用できます。

提示されたコード全体が完全なソリューションであり、かなり大きなロシアの会社のプロジェクトで2年以上使用されていますが、それでも負荷の高いシステムには適していないことを強調したいと思います。



カットの下の詳細。



タスク


アプリケーションを作成するとき、データアクセスレイヤーに関連するいくつかのタスクがありました。

1.すべてのデータの変更を記録する必要があります。これには、どのユーザーに関する情報も含まれます。

2. リポジトリパターンの使用

3.オブジェクトの変更を制御します。つまり、データベース内の1つのオブジェクトのみを更新する場合は、1つのオブジェクトのみを更新する必要があります。

私が説明します:

デフォルトでは、EFは特定のコンテキスト内のすべてのオブジェクトに対する変更を追跡しますが、NHibernateとは異なり、1つのオブジェクトを保存する方法はありません。 この状況には、さまざまな種類の不快なエラーが伴います。 たとえば、ユーザーは2つのオブジェクトを同時に編集しますが、保存するのは1つだけです。 これら2つのオブジェクトが同じデータベースコンテキストに関連付けられている場合、EFは両方のオブジェクトへの変更を保存します。



解決策


コードがたくさんあるので、最も興味深い点にコメントを追加します。

おそらく、最も重要なオブジェクトであるデータベースコンテキストから始めましょう。

標準の簡略化された形式では、データベースオブジェクトのリストです。

UsersContext
namespace TestApp.Models { public partial class UsersContext : DbContext { public UsersContext() : base("Name=UsersContext") { } public DbSet<User> Users { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new UserMap()); } } }
      
      







次のインターフェイスを使用して拡張します。

IDbContext
  public interface IDbContext { IQueryable<T> Find<T>() where T : class; void MarkAsAdded<T>(T entity) where T : class; void MarkAsDeleted<T>(T entity) where T : class; void MarkAsModified<T>(T entity) where T : class; void Commit(bool withLogging); //      void Rollback(); //       void EnableTracking(bool isEnable); EntityState GetEntityState<T>(T entity) where T : class; void SetEntityState<T>(T entity, EntityState state) where T : class; //         DbChangeTracker GetChangeTracker(); DbEntityEntry GetDbEntry<T>(T entity) where T : class; }
      
      







結果の変更されたDbContext:

DemoAppDbContext
 namespace DataAccess.DbContexts { public class DemoAppDbContext : DbContext, IDbContext { public static User CurrentUser { get; set; } private readonly ILogger _logger; #region Context Entities public DbSet<EntityChange> EntityChanges { get; set; } public DbSet<User> Users { get; set; } #endregion static DemoAppDbContext() { //  Database.SetInitializer(new CreateDBContextInitializer()); } //       public static void Seed(DemoAppDbContext context) { //     var defaultUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin" }; context.Users.Add(defaultUser); context.SaveChanges(); } public DemoAppDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { //   _logger = new Logger(this); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new EntityChangeMap()); modelBuilder.Configurations.Add(new UserMap()); } public void MarkAsAdded<T>(T entity) where T : class { Entry(entity).State = EntityState.Added; Set<T>().Add(entity); } public void MarkAsDeleted<T>(T entity) where T : class { Attach(entity); Entry(entity).State = EntityState.Deleted; Set<T>().Remove(entity); } public void MarkAsModified<T>(T entity) where T : class { Attach(entity); Entry(entity).State = EntityState.Modified; } public void Attach<T>(T entity) where T : class { if (Entry(entity).State == EntityState.Detached) { Set<T>().Attach(entity); } } public void Commit(bool withLogging) { BeforeCommit(); if (withLogging) { _logger.Run(); } SaveChanges(); } private void BeforeCommit() { UndoExistAddedEntitys(); } // ,      ,        private void UndoExistAddedEntitys() { IEnumerable<DbEntityEntry> dbEntityEntries = GetChangeTracker().Entries().Where(x => x.State == EntityState.Added); foreach (var dbEntityEntry in dbEntityEntries) { if (GetKeyValue(dbEntityEntry.Entity) > 0) { SetEntityState(dbEntityEntry.Entity, EntityState.Unchanged); } } } //      public void Rollback() { ChangeTracker.Entries().ToList().ForEach(x => x.Reload()); } public void EnableTracking(bool isEnable) { Configuration.AutoDetectChangesEnabled = isEnable; } public void SetEntityState<T>(T entity, EntityState state) where T : class { Entry(entity).State = state; } public DbChangeTracker GetChangeTracker() { return ChangeTracker; } public EntityState GetEntityState<T>(T entity) where T : class { return Entry(entity).State; } public IQueryable<T> Find<T>() where T : class { return Set<T>(); } public DbEntityEntry GetDbEntry<T>(T entity) where T : class { return Entry(entity); } public static int GetKeyValue<T>(T entity) where T : class { var dbEntity = entity as IDbEntity; if (dbEntity == null) throw new ArgumentException("Entity should be IDbEntity type - " + entity.GetType().Name); return dbEntity.GetPrimaryKey(); } } }
      
      







データベースオブジェクトとの相互作用は、各オブジェクトに固有のリポジ​​トリを通じて発生します。 すべてのリポジトリは、基本的なCRUD機能を提供する基本クラスを継承します

IRepository
  interface IRepository<T> where T : class { DemoAppDbContext CreateDatabaseContext(); List<T> GetAll(); T Find(int entityId); T SaveOrUpdate(T entity); T Add(T entity); T Update(T entity); void Delete(T entity); //    DbEntityValidationResult Validate(T entity); //     string ValidateAndReturnErrorString(T entity, out bool isValid); }
      
      







IRepositoryの実装:

ベースリポジトリ
 namespace DataAccess.Repositories { public abstract class BaseRepository<T> : IRepository<T> where T : class { private readonly IContextManager _contextManager; protected BaseRepository(IContextManager contextManager) { _contextManager = contextManager; } public DbEntityValidationResult Validate(T entity) { using (var context = CreateDatabaseContext()) { return context.Entry(entity).GetValidationResult(); } } public string ValidateAndReturnErrorString(T entity, out bool isValid) { using (var context = CreateDatabaseContext()) { DbEntityValidationResult dbEntityValidationResult = context.Entry(entity).GetValidationResult(); isValid = dbEntityValidationResult.IsValid; if (!dbEntityValidationResult.IsValid) { return DbValidationMessageParser.GetErrorMessage(dbEntityValidationResult); } return string.Empty; } } //    .   using public DemoAppDbContext CreateDatabaseContext() { return _contextManager.CreateDatabaseContext(); } public List<T> GetAll() { using (var context = CreateDatabaseContext()) { return context.Set<T>().ToList(); } } public T Find(int entityId) { using (var context = CreateDatabaseContext()) { return context.Set<T>().Find(entityId); } } //  .    ,       protected virtual void BeforeSave(T entity, DemoAppDbContext db) { } public T SaveOrUpdate(T entity) { var iDbEntity = entity as IDbEntity; if (iDbEntity == null) throw new ArgumentException("entity should be IDbEntity type", "entity"); return iDbEntity.GetPrimaryKey() == 0 ? Add(entity) : Update(entity); } public T Add(T entity) { using (var context = CreateDatabaseContext()) { BeforeSave(entity, context); context.MarkAsAdded(entity); context.Commit(true); } return entity; } public T Update(T entity) { using (var context = CreateDatabaseContext()) { var iDbEntity = entity as IDbEntity; if (iDbEntity == null) throw new ArgumentException("entity should be IDbEntity type", "entity"); var attachedEntity = context.Set<T>().Find(iDbEntity.GetPrimaryKey()); context.Entry(attachedEntity).CurrentValues.SetValues(entity); BeforeSave(attachedEntity, context); context.Commit(true); } return entity; } public void Delete(T entity) { using (var context = CreateDatabaseContext()) { context.MarkAsDeleted(entity); context.Commit(true); } } } }
      
      







ユーザーデータベースオブジェクト:

ユーザー
 namespace DataAccess.Models { public class User : IDbEntity { public User() { this.EntityChanges = new List<EntityChange>(); } public int UserId { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Login")] [StringLength(50, ErrorMessage = @"Login    50- ")] public string Login { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Email")] [StringLength(50, ErrorMessage = @"Email    50- ")] public string Email { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Name")] [StringLength(50, ErrorMessage = @"    50- ")] public string Name { get; set; } public bool IsBlocked { get; set; } public virtual ICollection<EntityChange> EntityChanges { get; set; } public override string ToString() { return string.Format(": User; :{0}, UserId:{1} ", Name, UserId); } public int GetPrimaryKey() { return UserId; } } }
      
      







Userオブジェクトのリポジトリには、いくつかの追加メソッドがあり、基本クラスの標準CRUD機能を拡張します。

ユーザーリポジトリ
 namespace DataAccess.Repositories { public class UsersRepository : BaseRepository<User> { public UsersRepository(IContextManager contextManager) : base(contextManager) { } public User FindByLogin(string login) { using (var db = CreateDatabaseContext()) { return db.Set<User>().FirstOrDefault(u => u.Login == login); } } public bool ExistUser(string login) { using (var db = CreateDatabaseContext()) { return db.Set<User>().Count(u => u.Login == login) > 0; } } public User GetByUserId(int userId) { using (var db = CreateDatabaseContext()) { return db.Set<User>().SingleOrDefault(c => c.UserId == userId); } } public User GetFirst() { using (var db = CreateDatabaseContext()) { return db.Set<User>().First(); } } } }
      
      







私の場合、すべてのリポジトリは一度初期化され、最も簡単な自己記述型のサービスロケータRepositoryContainerに追加されます。 これにより、テストを記述できるようになりました。

RepositoryContainer
 namespace DataAccess.Container { public class RepositoryContainer { private readonly IContainer _repositoryContainer = new Container(); public static readonly RepositoryContainer Instance = new RepositoryContainer(); private RepositoryContainer() { } public T Resolve<T>() where T : class { return _repositoryContainer.Resolve<T>(); } public void Register<T>(T entity) where T : class { _repositoryContainer.Register(entity); } } } namespace DataAccess.Container { public static class RepositoryContainerFactory { public static void RegisterAllRepositories(IContextManager dbContext) { RepositoryContainer.Instance.Register(dbContext); RepositoryContainer.Instance.Register(new EntityChangesRepository(dbContext)); RepositoryContainer.Instance.Register(new UsersRepository(dbContext)); } } }
      
      







すべてのリポジトリに対して、初期化中にIContextManagerオブジェクトが渡されます。これは、いくつかのコンテキストを操作し、それらを一元的に作成できるようにするために行われます。

IContextManager
 namespace DataAccess.Interfaces { public interface IContextManager { DemoAppDbContext CreateDatabaseContext(); } }
      
      







ContextManagerの実装:

ContextManager
 using DataAccess.Interfaces; namespace DataAccess.DbContexts { public class ContextManager : IContextManager { private readonly string _connectionString; public ContextManager(string connectionString) { _connectionString = connectionString; } public DemoAppDbContext CreateDatabaseContext() { return new DemoAppDbContext(_connectionString); } } }
      
      







ILoggerインターフェイスを実装するオブジェクトでロギングが発生します。

ILogger
 namespace DataAccess.Interfaces { internal interface ILogger { void Run(); } }
      
      







ILoggerインターフェイスの実装

ロガー
  public class Logger : ILogger { Dictionary<EntityState, string> _operationTypes; private readonly IDbContext _dbContext; public Logger(IDbContext dbContext) { _dbContext = dbContext; InitOperationTypes(); } public void Run() { LogChangedEntities(EntityState.Added); LogChangedEntities(EntityState.Modified); LogChangedEntities(EntityState.Deleted); } private void InitOperationTypes() { _operationTypes = new Dictionary<EntityState, string> { {EntityState.Added, ""}, {EntityState.Deleted, ""}, {EntityState.Modified, ""} }; } private string GetOperationName(EntityState entityState) { return _operationTypes[entityState]; } private void LogChangedEntities(EntityState entityState) { IEnumerable<DbEntityEntry> dbEntityEntries = _dbContext.GetChangeTracker().Entries().Where(x => x.State == entityState); foreach (var dbEntityEntry in dbEntityEntries) { LogChangedEntitie(dbEntityEntry, entityState); } } private void LogChangedEntitie(DbEntityEntry dbEntityEntry, EntityState entityState) { string operationHash = HashGenerator.GenerateHash(10); int enitityId = DemoAppDbContext.GetKeyValue(dbEntityEntry.Entity); Type type = dbEntityEntry.Entity.GetType(); IEnumerable<string> propertyNames = entityState == EntityState.Deleted ? dbEntityEntry.OriginalValues.PropertyNames : dbEntityEntry.CurrentValues.PropertyNames; foreach (var propertyName in propertyNames) { DbPropertyEntry property = dbEntityEntry.Property(propertyName); if (entityState == EntityState.Modified && !property.IsModified) continue; _dbContext.MarkAsAdded(new EntityChange { UserId = DemoAppDbContext.CurrentUser.UserId, Created = DateTime.Now, OperationHash = operationHash, EntityName = string.Empty, EntityType = type.ToString(), EntityId = enitityId.ToString(), PropertyName = propertyName, OriginalValue = entityState != EntityState.Added && property.OriginalValue != null ? property.OriginalValue.ToString() : string.Empty, ModifyValue = entityState != EntityState.Deleted && property.CurrentValue != null ? property.CurrentValue.ToString() : string.Empty, OperationType = GetOperationName(entityState), }); } } }
      
      







使用する


データベースの操作を開始するには、アプリケーションでリポジトリファクトリを初期化する必要があります。

 RepositoryContainerFactory.RegisterAllRepositories(new ContextManager(Settings.Default.DBConnectionString));
      
      





その後、認証を行って現在のユーザーを指定する必要があります。 これは、履歴でこれまたはその変更を行ったユーザーに関する情報を保存するために必要です。 デモプロジェクトでは、この点は省略されています。

InitDefaultUser
 private void InitDefaultUser() { User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst(); DemoAppDbContext.CurrentUser = defaultUser; }
      
      







リポジトリメソッドは、サービスロケーターaからインスタンスを取得することで呼び出されます。 次の例では、呼び出しはUsersRepositoryリポジトリのGetFirst()メソッドに送られます。

 User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst();
      
      





新しいユーザーの追加:

 var newUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin"}; RepositoryContainer.Instance.Resolve<UsersRepository>().SaveOrUpdate(newUser);
      
      





オブジェクトを保存する前の検証


検証とエラーのリストの取得:

 var newUser = new User { Email = "UserEmail@email.ru", IsBlocked = false, }; DbEntityValidationResult dbEntityValidationResult = RepositoryContainer.Instance.Resolve<UsersRepository>().Validate(newUser);
      
      





エラーのある行を取得する:

 var newUser = new User { Email = "UserEmail@email.ru", IsBlocked = false, }; bool isValid=true; string errors = RepositoryContainer.Instance.Resolve<UsersRepository>().ValidateAndReturnErrorString(newUser, out isValid); if (!isValid) { MessageBox.Show(errors, "Error..", MessageBoxButtons.OK, MessageBoxIcon.Error); }
      
      





デモプロジェクト


Yandexドライブhttp://yadi.sk/d/P9XDDznpMj6p8で完全に機能するドラフトを取得できます。

操作には、インストール済みのMSSQL DBMSが必要であることに注意してください。

MSSQL Expressを使用している場合、接続文字列を修正する必要があります

  <value>Data Source=.\; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value>
      
      







  <value>Data Source=.\SQLEXPRESS; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value>
      
      





あとがき


上記のコードはすべて、タスクに対する私のソリューションです。 正しくないかもしれませんし、最適ではないかもしれませんが、それでも数年前からプロジェクトの1つでうまく機能しています。

かつて、私はこのシステムの作成にかなりの時間と労力を費やし、私の結果が誰かに役立つことを願っています。



みんなありがとう!



All Articles